Compare commits

..
1117 Commits
Author SHA1 Message Date
naturallaw777 2b8ee5ff26 updated readme 2026-07-15 15:25:14 -05:00
Sovran Systems b4990d70ef Merge pull request #327 from naturallaw777/copilot/remove-restart-sidebar-action
Move Reboot from sidebar to compact header button
2026-07-15 18:59:25 +00:00
copilot-swe-agent[bot] 59b734995b feat: move Reboot button from sidebar to header, between role badge and Sign Out 2026-07-15 18:58:10 +00:00
copilot-swe-agent[bot] 599405ce18 Initial plan 2026-07-15 18:55:54 +00:00
Sovran Systems 4b31255f13 Merge pull request #326 from naturallaw777/copilot/add-system-restart-action
feat: implement consistent whole-system restart UX across the Hub
2026-07-15 18:26:26 +00:00
copilot-swe-agent[bot] 6b79c212a8 refactor: improve variable name in openRestartConfirmDialog for clarity 2026-07-15 16:51:52 +00:00
copilot-swe-agent[bot] c3950547b0 feat: implement consistent restart UX across Sovran_SystemsOS Hub
- Add Restart Entire System sidebar action with amber treatment and divider
- Add shared restart confirmation dialog with conflict detection
- Update reboot overlay: new title, body copy, status progression, error card
- Improve doReboot(): failure handling, aria-live status messages
- Standardize 'restart required' / 'Restart Entire System' terminology
- Update security.js reboot flow to use shared doReboot()
- Update installer.py button label
- Add .btn-restart-amber, .restart-conflict-box, .sidebar-restart-btn CSS
2026-07-15 16:50:01 +00:00
copilot-swe-agent[bot] b28d6dd32c Initial plan 2026-07-15 16:41:20 +00:00
Sovran Systems 7ac3775a96 Merge pull request #325 from naturallaw777/copilot/implement-server-side-loopback-solution
feat: server-side loopback overrides + Hub diagnostic fixes for NAT loopback
2026-07-15 16:21:10 +00:00
copilot-swe-agent[bot] de9b069a88 fix: address code review feedback
- Use set -eu (not set -euf) in sovran-hosts-update.sh
- Add sync note for domain validation regex between shell and Python
- Rename `seen` to `unique_addresses` in _resolve_all_addresses
- Improve loopback override UI message with external DNS check guidance
- Add docstring note about first-address display in _resolve_all_addresses"
2026-07-15 15:26:43 +00:00
copilot-swe-agent[bot] 2cb0c734d8 feat: server-side loopback overrides and Hub diagnostic fixes
- Add modules/core/local-domain-loopback.nix: systemd service and
  activation script that write configured service domains to a
  Sovran-managed block in /etc/hosts (127.0.0.1 / ::1) so requests
  originating on this computer reach Caddy without NAT loopback.

- Import local-domain-loopback.nix in modules/modules.nix.

- server.py: add _validate_domain_value, _is_loopback_address,
  _resolve_all_addresses, _trigger_hosts_update helpers.

- server.py: update _check_domain_reachable to use --resolve so
  reachability is checked locally via Caddy, not via NAT loopback.

- server.py: update _evaluate_domain_checklist, api_services inline DNS
  check, and api_domains_check to recognise loopback resolution as an
  intentional local override rather than a DNS mismatch.

- server.py: call _trigger_hosts_update from api_domains_set after
  saving a service domain so the /etc/hosts entry is applied immediately.

- Add app/tests/test_loopback_diagnostics.py with 47 tests covering
  domain validation, loopback detection, diagnostic checklist logic,
  composite health, and api_domains_check."
2026-07-15 15:24:16 +00:00
copilot-swe-agent[bot] 6f908513e3 Initial plan 2026-07-15 15:14:53 +00:00
Sovran Systems 53f59aa388 Merge pull request #324 from naturallaw777/copilot/fix-rdp-boot-time-setup
fix(rdp): prepend /run/wrappers/bin to PATH and add pkexec preflight check
2026-07-14 16:02:03 +00:00
copilot-swe-agent[bot] 69e996ff98 fix(rdp): prepend /run/wrappers/bin to PATH and add pkexec preflight check
GRD 50.x invokes pkexec internally for every grdctl --system call, even
when the caller is root.  An isolated systemd script PATH does not include
/run/wrappers/bin automatically, causing exit 70 at boot.

Changes:
- Prepend /run/wrappers/bin to PATH at script start so every subsequent
  grdctl --system resolves the NixOS setuid pkexec wrapper.
- Add an explicit preflight check (test -x /run/wrappers/bin/pkexec) with
  a clear error message before the first grdctl_system call.
- pkgs.polkit remains absent from the service path.
- Update test_setup_runs_grdctl_directly_as_root to reflect that pkexec
  now appears in the preflight check (not as a direct invocation).
- Add test_run_wrappers_bin_prepended_to_path and test_pkexec_preflight_check.
2026-07-14 15:54:40 +00:00
copilot-swe-agent[bot] 6e4d0d22a1 Initial plan 2026-07-14 15:51:34 +00:00
Sovran Systems 881587c42a Merge pull request #323 from naturallaw777/copilot/fix-gnome-remote-desktop-boot-setup
fix(rdp): run grdctl --system directly as root, drop runuser/polkit/util-linux
2026-07-14 15:20:22 +00:00
copilot-swe-agent[bot] 1bb7d1c680 style: fix spelling authorisation → authorization in test comment 2026-07-14 00:29:26 +00:00
copilot-swe-agent[bot] fd5f651f2c fix(rdp): run grdctl --system directly as root, remove runuser/polkit/util-linux
Root cause: gnome-remote-desktop-setup.service ran as root but dropped
privileges via `runuser -u gnome-remote-desktop`. Non-root grdctl --system
attempts authorisation through pkexec. The Nix-store pkexec binary is not
setuid, so every system-mode credential call silently failed while the script
still printed "configured successfully". GNOME Remote Desktop then started
without applied credentials, causing Remmina to loop at the login dialog.

Fix:
- Remove `runuser -u gnome-remote-desktop --` from grdctl_system helper;
  the root-run oneshot service can call grdctl --system directly.
- Remove pkgs.polkit and pkgs.util-linux from the setup service path as
  neither polkit nor runuser is needed any more.
- Update tests: rename test_setup_runs_grdctl_as_gnome_remote_desktop_user
  to test_setup_runs_grdctl_directly_as_root and assert that runuser,
  pkexec, and sudo are absent; add
  test_privilege_escalation_packages_absent_from_setup_path.

All 21 app/tests pass.
2026-07-14 00:28:17 +00:00
copilot-swe-agent[bot] b4317c9589 Initial plan 2026-07-14 00:26:18 +00:00
Sovran Systems 07f3e4cef9 Merge pull request #322 from naturallaw777/copilot/fix-rdp-credentials-activation
Fix GNOME Remote Desktop boot-time credential activation for Hub-managed RDP
2026-07-13 23:56:48 +00:00
copilot-swe-agent[bot] 7592bda57a fix: apply hub-managed GRD credentials at boot 2026-07-13 23:54:08 +00:00
copilot-swe-agent[bot] 47f496efcf fix: tighten GRD boot credential setup 2026-07-13 23:48:12 +00:00
copilot-swe-agent[bot] 4a2c3a8eb4 Initial plan 2026-07-13 23:25:36 +00:00
Sovran Systems e9e7451a8e Merge pull request #321 from naturallaw777/copilot/update-element-calling-nix
element-calling: universal LiveKit interface detection, full JWT routing, homeserver config
2026-07-13 18:26:16 +00:00
copilot-swe-agent[bot] 06c0cfbb78 feat(element-calling): universal LiveKit interface detection, JWT/Caddy fixes 2026-07-13 18:24:43 +00:00
copilot-swe-agent[bot] 37b0369361 Initial plan 2026-07-13 18:20:58 +00:00
naturallaw777 81a974d715 nixpkgs update and btc client update 2026-07-13 13:02:42 -05:00
naturallaw777 ff70668f38 Merge remote-tracking branch 'github/main' into staging-dev 2026-07-13 12:46:51 -05:00
Sovran Systems fbe431555e Merge pull request #320 from naturallaw777/copilot/update-hub-workflow-to-stage-os-updates
Stage Hub full OS updates for next reboot (boot-first) instead of live switch
2026-07-13 17:46:35 +00:00
copilot-swe-agent[bot] bd4c4f8716 Adjust update status docstring ordering 2026-07-13 17:41:36 +00:00
copilot-swe-agent[bot] 201cdb5bf9 Polish staged-update test and log header formatting 2026-07-13 17:40:29 +00:00
copilot-swe-agent[bot] 7ba4adb376 Stage Hub full updates for next reboot 2026-07-13 17:39:14 +00:00
copilot-swe-agent[bot] a4fc880c38 Initial plan 2026-07-13 17:36:29 +00:00
naturallaw777 edf6294315 Update nixpkgs 2026-07-04 11:25:52 -05:00
Sovran Systems b4dd074894 Merge pull request #319 from naturallaw777/copilot/implement-desktop-only-safety-fixes
fix(desktop): harden Desktop Only role — nix-bitcoin compat, force-off server services, conditional Caddy
2026-07-03 23:39:22 +00:00
copilot-swe-agent[bot] e81f6643fc fix: desktop-only safety fixes (nix-bitcoin compat, mkForce, conditional caddy)
1. Add nix-bitcoin.generateSecrets = lib.mkDefault true global compat
   default in role-logic.nix so Desktop Only systems can evaluate while
   nix-bitcoin is still globally imported by the flake.

2. Harden Desktop Only role: change all server/node service and feature
   disables from lib.mkDefault false to lib.mkForce false so they cannot
   be overridden by custom.nix or option defaults.
   - sovran_systemsOS.services: synapse, bitcoin, vaultwarden, wordpress, nextcloud
   - sovran_systemsOS.features: haven, mempool, element-calling, bitcoin-core
   - sovran_systemsOS.web.btcpayserver

3. Make Caddy conditional in caddy.nix:
   enable = needsHttpsPorts || extraVhosts != ""
   so Caddy does not run on Desktop Only installs with no web services.
2026-07-03 23:35:30 +00:00
copilot-swe-agent[bot] afe51c4abd Initial plan 2026-07-03 23:33:55 +00:00
Sovran Systems a101e73c0e Merge pull request #318 from naturallaw777/copilot/fix-template-response-error
Fix TemplateResponse calls for Starlette 1.1.0+ compatibility
2026-06-30 17:38:27 +00:00
copilot-swe-agent[bot] 1a5c6aca08 Fix TemplateResponse calls to use Starlette 1.1.0+ keyword-argument style 2026-06-30 17:37:21 +00:00
copilot-swe-agent[bot] 00f76f33fd Initial plan 2026-06-30 17:35:17 +00:00
naturallaw777 0a92b8f57a Update nixpkgs 2026-06-30 12:14:53 -05:00
naturallaw777 8345c97664 updated nixpkgs and btc clients 2026-06-25 16:26:43 -05:00
Sovran Systems 1f61eb8c7e Merge pull request #317 from naturallaw777/copilot/update-hub-router-port-forwarding-ui
Clarify Hub router forwarding copy and surface internal IP in Element Call flows
2026-06-24 17:26:11 -05:00
Sovran Systems 625a307a8d Merge pull request #316 from naturallaw777/copilot/fix-legacy-ssh-key-handling
Repair legacy factory SSH keys when Hub passphrase changes
2026-06-24 17:25:57 -05:00
copilot-swe-agent[bot] c2f3f048b9 fix: simplify internal IP copy handling 2026-06-24 22:19:14 +00:00
copilot-swe-agent[bot] bd3dbcb057 fix: clarify router forwarding IP guidance 2026-06-24 22:17:13 +00:00
copilot-swe-agent[bot] 7f975bc4f1 chore: use flock for ssh bootstrap repair 2026-06-24 22:16:44 +00:00
copilot-swe-agent[bot] 31abf40722 chore: serialize ssh bootstrap key repairs 2026-06-24 22:16:05 +00:00
copilot-swe-agent[bot] 439021f798 chore: harden ssh bootstrap script 2026-06-24 22:15:24 +00:00
copilot-swe-agent[bot] 181465c376 chore: log legacy ssh key regeneration 2026-06-24 22:14:42 +00:00
copilot-swe-agent[bot] 6ec28b1ad7 Initial plan 2026-06-24 22:14:35 +00:00
copilot-swe-agent[bot] db1a88ab2e fix: repair legacy factory ssh key passphrases 2026-06-24 22:14:06 +00:00
copilot-swe-agent[bot] aa148fe435 Initial plan 2026-06-24 22:12:39 +00:00
Sovran Systems f4590ff653 Merge pull request #315 from naturallaw777/copilot/update-sovran-systemsos-port-forwarding-ui
Align port-forwarding UX to local-readiness semantics across service detail, Step 4 checklist, and onboarding
2026-06-24 16:55:07 -05:00
copilot-swe-agent[bot] 22402cb4fd Align router setup wording and local port statuses 2026-06-24 19:06:12 +00:00
copilot-swe-agent[bot] 1868a58ee0 Initial plan 2026-06-24 19:03:36 +00:00
naturallaw777 50cbd2fa28 added onlyoffice 2026-06-24 13:19:41 -05:00
Sovran Systems 81e34a4adb Merge pull request #314 from naturallaw777/copilot/fix-livekit-service-permissions
fix: deliver LiveKit config via LoadCredential to resolve DynamicUser permission denied
2026-06-23 21:04:51 -05:00
copilot-swe-agent[bot] 302eb43233 fix: deliver livekit config via LoadCredential to fix DynamicUser permission denied 2026-06-24 01:21:05 +00:00
copilot-swe-agent[bot] 949391ed44 Initial plan 2026-06-24 01:19:37 +00:00
Sovran Systems 5041e5202f Merge pull request #313 from naturallaw777/fix/livekit-embedded-turn
fix(element-calling): enable LiveKit embedded TURN with runtime matri…
2026-06-23 20:04:38 -05:00
Sovran Systems 8baefe1bfd fix(element-calling): enable LiveKit embedded TURN with runtime matrix domain + cert
LiveKit was exiting cleanly with "TURN domain required" because turn.enabled
was set in the build-time config but turn.domain was never provided to the
process (the old livekit-runtime-config.service wrote a YAML that nothing
read). A clean exit (status 0) meant Restart=on-failure never restarted it,
so the Hub reported the service as Inactive.

This replaces the dead runtime-config oneshot with livekit-turn-setup.service,
which at runtime:
  - reads the matrix domain from /var/lib/domains/matrix (no hardcoding)
  - copies Caddy's already-issued matrix cert/key into /var/lib/livekit
  - generates a complete LiveKit config (incl. turn.domain + TLS cert/key)
    at /run/livekit/livekit.yaml

The livekit.service ExecStart is overridden to load that runtime config
(mirroring the existing Caddy ExecStart override pattern in
modules/core/caddy.nix), since turn.domain is only known at runtime. The cert
is delivered via LoadCredential so it is readable under DynamicUser=true
without weakening the sandbox.

Also aligns the RTC media port range (rtc.port_range_start/end = 30000-40000)
so it matches the forwarded ports, and drops the now-redundant manual
30000-40000 firewall ranges (covered by services.livekit settings/openFirewall).
2026-06-23 20:03:26 -05:00
Sovran Systems d8108dae0f Merge pull request #312 from naturallaw777/copilot/rewrite-njalla-domain-setup
Rewrite Njal.la domain-setup instructions: remove IP box, clarify Name-field host-only rule, support subdomain-or-separate-domain
2026-06-22 19:46:10 -05:00
copilot-swe-agent[bot] 3eb347da06 Rewrite Njal.la domain-setup instructions (no IP box, Name-field clarification, subdomain-or-domain) 2026-06-23 00:44:39 +00:00
copilot-swe-agent[bot] 8d15b71c06 Initial plan 2026-06-23 00:40:49 +00:00
naturallaw777 a28ad04b55 removed uneeded livekit-cli 2026-06-18 16:51:15 -05:00
naturallaw777 6720b602ba nixpkgs update 2026-06-18 16:05:02 -05:00
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
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]andnaturallaw777 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]andnaturallaw777 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
naturallaw777 bc4b4630a3 overall nixpkgs update 2026-05-21 17:02:38 -05:00
Sovran Systems 342f60ce0d Delete docs directory 2026-05-21 16:50:48 -05:00
Sovran Systems 8c8e8f43a2 Merge pull request #299 from naturallaw777/copilot/update-installer-pinning-to-stable
Pin GUI installer deployed flake to `stable` instead of `staging-dev`
2026-05-21 09:39:37 -05:00
copilot-swe-agent[bot]andnaturallaw777 559e0218eb fix(installer): pin deployed flake and log message to stable
Agent-Logs-Url: https://github.com/naturallaw777/sovran-systems/sessions/4648ebc7-45b1-4fd2-8636-29c15ee484fe

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-05-21 14:33:28 +00:00
copilot-swe-agent[bot] fd2e12ced1 Initial plan 2026-05-21 14:28:00 +00:00
Sovran Systems efdf1e05d0 Merge pull request #298 from naturallaw777/copilot/add-prerequisites-notice-installer
Add Server + Desktop prerequisites notice to installer role-selection screen
2026-05-20 20:40:08 -05:00
copilot-swe-agent[bot]andnaturallaw777 cd3ab47aa0 Use theme-safe installer notice styling
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a71df5f0-f463-4c08-b54d-946d97d0aafd

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-05-21 01:38:36 +00:00
copilot-swe-agent[bot]andnaturallaw777 c12a680d27 Add installer prerequisites notice
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a71df5f0-f463-4c08-b54d-946d97d0aafd

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-05-21 01:37:54 +00:00
copilot-swe-agent[bot] c728eee924 Initial plan 2026-05-21 01:32:55 +00:00
Sovran Systems ca704b24a2 Merge pull request #297 from naturallaw777/copilot/add-flatpak-retry-policy
Harden `flatpak-repo` boot unit against transient Flathub fetch failures
2026-05-17 09:08:01 -05:00
copilot-swe-agent[bot]andnaturallaw777 db068ba994 Harden flatpak-repo systemd service retries
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/42ce6628-372d-4bd8-82c5-b10097fc8003

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-05-17 14:06:58 +00:00
copilot-swe-agent[bot] 53ee31c5d2 Initial plan 2026-05-17 14:05:39 +00:00
naturallaw777 283b439d59 nixpkgs update 2026-05-14 10:17:11 -05:00
Sovran Systems 9e09f9eb40 Merge pull request #296 from naturallaw777/copilot/fix-switch-inhibitors-check
Fix switchInhibitors fallback detection race in sovran-hub update/rebuild scripts
2026-05-09 10:07:41 -05:00
copilot-swe-agent[bot]andnaturallaw777 08bfa73e74 fix: detect switchInhibitors from captured nixos-rebuild output
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/637e0a15-b70f-44b0-abe8-8ba3dd25a359

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-05-09 15:00:10 +00:00
copilot-swe-agent[bot] 2b76a766ad Initial plan 2026-05-09 14:58:55 +00:00
Sovran Systems d245e2ce0b Merge pull request #295 from naturallaw777/copilot/update-wipe-paths-array
Expand security reset wipe scope to include nix-bitcoin and Bisq state
2026-05-08 16:00:04 -05:00
copilot-swe-agent[bot]andnaturallaw777 bb2603bea0 Add nix-bitcoin and Bisq data paths to security reset wipe list
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8f922dd0-a5b0-42c6-af40-4bbd78a29ffa

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-05-08 20:55:29 +00:00
copilot-swe-agent[bot] 8f96625c26 Initial plan 2026-05-08 20:53:48 +00:00
naturallaw777 9c0ddf0dbe blocked rxrpc kernal moduel 2026-05-08 15:20:18 -05:00
naturallaw777 2a352c35f9 updated nixpkgs 2026-05-08 08:04:35 -05:00
Sovran Systems 56b965b847 Merge pull request #294 from naturallaw777/copilot/update-sparrow-wallet-auto-connect
Fix Sparrow wallet defaulting to public Electrum server on first launch
2026-05-03 17:38:03 -05:00
copilot-swe-agent[bot]andnaturallaw777 8e5bb766a6 fix: add mode ONLINE to Sparrow config so Electrum backend is active on first launch
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/69818c91-2127-4392-8a39-76953e17497c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-05-03 22:37:14 +00:00
copilot-swe-agent[bot] 646d877c4d Initial plan 2026-05-03 22:33:54 +00:00
Sovran Systems 045c5d6a12 Merge pull request #293 from naturallaw777/copilot/make-custom-nix-readable-writable
Make custom.nix writable on new installs
2026-04-30 12:47:20 -05:00
copilot-swe-agent[bot]andnaturallaw777 f0f690eae4 Make custom.nix read-write (644) on all new installs
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/1606cde2-b484-4570-a64e-649f80384367

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 17:45:45 +00:00
copilot-swe-agent[bot] 459536d478 Initial plan 2026-04-30 17:44:53 +00:00
naturallaw777 6c5f261d8a fix for gnome keyring 2026-04-30 11:42:35 -05:00
Sovran Systems 91cc0152ba Merge pull request #292 from naturallaw777/copilot/move-tmpfiles-rules-to-user-level
Fix GNOME Keyring permission corruption on fresh installs: move tmpfiles to user level
2026-04-30 11:33:45 -05:00
copilot-swe-agent[bot]andnaturallaw777 bfc60eeb2c Fix GNOME Keyring permission issue: move tmpfiles rules to user level
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3ed85d6b-ada9-48e1-941f-1150e1491157

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 16:32:36 +00:00
copilot-swe-agent[bot] 976d8f3609 Initial plan 2026-04-30 16:31:14 +00:00
naturallaw777 10a1d0f7ba fix for gnome keyring 2026-04-30 10:46:58 -05:00
Sovran Systems 3a8e9a2dd0 Merge pull request #291 from naturallaw777/copilot/make-brave-default-browser
Make Brave the default browser on fresh installs
2026-04-30 09:33:32 -05:00
copilot-swe-agent[bot]andnaturallaw777 6872c8d820 feat: make Brave the default browser on fresh installs
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/fbb8cbcc-6f16-419a-b732-2457c1e67384

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 14:28:24 +00:00
copilot-swe-agent[bot] 175f48ef37 Initial plan 2026-04-30 14:26:43 +00:00
Sovran Systems 49912a2760 Merge pull request #290 from naturallaw777/copilot/refactor-gnome-keyring-setup
Refactor GNOME Keyring management to native NixOS tmpfiles
2026-04-30 09:17:14 -05:00
copilot-swe-agent[bot]andnaturallaw777 c450dcab9e refactor: use systemd.tmpfiles for GNOME Keyring, simplify reset scripts
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/71dab9c7-081f-4e45-80c2-080e88ae6207

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 13:52:27 +00:00
copilot-swe-agent[bot] 953fb04671 Initial plan 2026-04-30 13:48:25 +00:00
Sovran Systems c02655a840 Merge pull request #289 from naturallaw777/copilot/fix-gnome-keyring-initialization-issues
fix: seed GNOME Keyring default pointer on fresh installs, resets, and migrations
2026-04-30 08:24:10 -05:00
copilot-swe-agent[bot]andnaturallaw777 f87e9982b0 fix: seed GNOME Keyring default pointer on fresh installs, resets, and migrations
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c3f5b4ac-12ed-4ff9-ac7c-f07be1f178d9

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 13:14:10 +00:00
copilot-swe-agent[bot] 02e662454c Initial plan 2026-04-30 13:10:40 +00:00
Sovran Systems 4d8eaf71ca Merge pull request #288 from naturallaw777/copilot/update-credential-labels-rtl-mempool
Improve Tor vs. Local Network credential labels across Bitcoin services
2026-04-30 03:40:20 -05:00
copilot-swe-agent[bot]andnaturallaw777 a0f42d3e7b feat: improve credential labels for Tor/Local access across all services
- RTL: rename 'Tor Access' → 'Tor Address — Access from anywhere via Tor Browser'
         rename 'Local Network' → 'Local Network — Access on your home network only'
         add 'How to Access' explanation credential
- Mempool: same label improvements + 'How to Access' credential
- Bitcoin Knots, Bitcoin Core, Electrs: update 'Tor Address' label to include
  'Access from anywhere via Tor Browser' for consistency

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/63c3edb0-9fbf-4dd8-91e5-404ff6e4097d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 08:38:27 +00:00
copilot-swe-agent[bot] ef683a6aa9 Initial plan 2026-04-30 08:36:41 +00:00
Sovran Systems 02a9dbf39c Merge pull request #286 from naturallaw777/copilot/remove-connection-url-zeus-connect
Zeus Connect Hub UI: show QR code only, suppress raw URL text
2026-04-29 22:55:27 -05:00
copilot-swe-agent[bot]andnaturallaw777 6d72f70fe5 Fix Zeus Connect: show only QR code, hide raw URL text
- modules/core/sovran-hub.nix: rename credential label from 'Scan QR Code' to 'QR Code'
- server.py: forward qronly flag in _resolve_credential so JS can hide the URL text/copy button

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0292564f-8e75-4c34-b938-1a6c98f3ff0d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 03:50:17 +00:00
copilot-swe-agent[bot] c2887b60b2 Initial plan 2026-04-30 03:46:21 +00:00
Sovran Systems 9e76bad58a hub: upgrade modal — Njalla is the only supported domain provider 2026-04-29 22:31:22 -05:00
Sovran Systems b3f6efef8a hub: Zeus Connect — skip value/copy button when qronly is set 2026-04-29 22:24:24 -05:00
Sovran Systems 53813e775d hub: Zeus Connect — show QR code only, remove Connection URL text 2026-04-29 22:22:21 -05:00
Sovran Systems 060f81393c Merge pull request #284 from naturallaw777/copilot/fix-gdm-login-loop-pam-config
Fix GDM login loop: replace broken PAM hook with free-password-migration systemd service
2026-04-29 20:45:46 -05:00
Sovran Systems c1c0827604 Merge pull request #285 from naturallaw777/copilot/fix-legacy-migration-flow
Fix legacy migration flow: defer chpasswd to password-acknowledge
2026-04-29 20:45:20 -05:00
copilot-swe-agent[bot]andnaturallaw777 b5715e05c6 Fix legacy migration flow: move chpasswd to password-acknowledge endpoint
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/6ad42ef5-884b-4945-b49e-76b3e6c34088

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 01:42:01 +00:00
copilot-swe-agent[bot] 68c3aa95fd Initial plan 2026-04-30 01:39:53 +00:00
copilot-swe-agent[bot]andnaturallaw777 281b08dcd4 Fix GDM login loop: replace broken PAM hook with free-password-migration systemd service
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c958784d-bc79-4784-9ec6-6d52fd3f574e

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 01:27:01 +00:00
copilot-swe-agent[bot] ca1ff3ee20 Initial plan 2026-04-30 01:25:15 +00:00
Sovran Systems 1cd5bd4496 Merge pull request #283 from naturallaw777/copilot/fix-free-password-setup-script
fix(credentials): enforce boot ordering and error visibility for password-setup services
2026-04-29 19:54:14 -05:00
copilot-swe-agent[bot]andnaturallaw777 6512bf4356 fix: add set -euo pipefail and boot ordering to password-setup services
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/2f9c39e8-d673-4314-bff7-28f1fffd48a0

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 00:52:17 +00:00
copilot-swe-agent[bot] 7da0463dce Initial plan 2026-04-30 00:51:11 +00:00
Sovran Systems 466582bcdc Merge pull request #282 from naturallaw777/copilot/remove-plymouth-and-add-cpu-performance
Remove Plymouth entirely; add quiet boot params and cpu-performance module
2026-04-29 19:35:01 -05:00
copilot-swe-agent[bot]andnaturallaw777 c23ae5543d Remove Plymouth, add quiet boot params, add cpu-performance module
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/eda71495-cd38-4408-8d3b-b9d793f6445f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-30 00:33:32 +00:00
copilot-swe-agent[bot] 569e0de59d Initial plan 2026-04-30 00:31:49 +00:00
naturallaw777 4a7d9615db nix package update 2026-04-29 16:10:09 -05:00
naturallaw777 7c1b603200 updated bitcoind for better launch 2026-04-29 16:00:27 -05:00
Sovran Systems 4dae7836dd Merge pull request #279 from naturallaw777/copilot/fix-update-script-inhibitor-logic
Handle NixOS switchInhibitors: fall back to nixos-rebuild boot and surface reboot-required state in UI
2026-04-29 15:14:43 -05:00
copilot-swe-agent[bot]andnaturallaw777 e821da6c2a Handle NixOS switchInhibitors: detect reboot-required case and show correct UI state
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d72be7a1-ec3f-41da-9753-611b95bc9903

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-29 20:04:59 +00:00
copilot-swe-agent[bot] 17fbd5fd2c Initial plan 2026-04-29 20:00:45 +00:00
Sovran Systems 8712ac43c6 Merge pull request #278 from naturallaw777/copilot/fix-new-services-inactive-state
Auto-start newly enabled services after NixOS rebuild
2026-04-29 14:53:21 -05:00
copilot-swe-agent[bot]andnaturallaw777 38e4a296ee Auto-start newly enabled services after successful NixOS rebuild
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3d0aaa70-7eb3-4496-abe4-095e4c4d3dea

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-29 19:51:20 +00:00
copilot-swe-agent[bot] d1ef6ba1cd Initial plan 2026-04-29 19:48:47 +00:00
Sovran Systems ffd2029852 fix: disable BTCPayServer by default in node-only mode
The BTCPayServer hub entry was using `cfg.services.bitcoin` as its
`enabled` flag, which is `true` in node mode. This caused the Hub UI
to show BTCPayServer as enabled even though the underlying NixOS service
is correctly gated on `cfg.web.btcpayserver` (which defaults to `false`
for the node role via role-logic.nix).

Change the enabled field to `cfg.web.btcpayserver` so the Hub UI
accurately reflects the service state and BTCPayServer is disabled by
default on a fresh node-only install.
2026-04-28 17:49:25 -05:00
Sovran Systems 761af09166 Merge pull request #277 from naturallaw777/copilot/fix-deprecated-logind-options
Migrate `no-sleep` logind lid handling to `services.logind.settings.Login`
2026-04-22 09:28:54 -05:00
copilot-swe-agent[bot]andnaturallaw777 48d7e8a459 Fix deprecated logind lid switch options in no-sleep module
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8a4eee86-6cb7-411d-9e71-1bcfae42374e

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-22 14:24:05 +00:00
copilot-swe-agent[bot] d3327e05d4 Initial plan 2026-04-22 14:22:01 +00:00
Sovran Systems 2c8dd91cf0 Merge pull request #276 from naturallaw777/copilot/fix-no-sleep-module-roles
Scope `no-sleep` module to non-desktop roles
2026-04-22 07:39:52 -05:00
copilot-swe-agent[bot]andnaturallaw777 448c4b9094 Fix no-sleep module to skip desktop role
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d008099d-2fd3-4b86-a10c-ed7f9337e51c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-22 12:37:36 +00:00
copilot-swe-agent[bot] e147fd8f4d Initial plan 2026-04-22 12:34:17 +00:00
Sovran Systems b8feea3711 Merge pull request #275 from naturallaw777/copilot/fix-deprecated-services-logind-extraconfig
[WIP] Fix deprecated services.logind.extraConfig in modules/core/no-sleep.nix
2026-04-22 07:09:36 -05:00
copilot-swe-agent[bot]andnaturallaw777 0cc1f50aa4 fix: replace deprecated logind extraConfig with settings.Login
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a9bcbedf-7dfa-47e2-a9f5-b288ff5c5f42

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-22 12:09:00 +00:00
copilot-swe-agent[bot] 71d8eae6d8 Initial plan 2026-04-22 12:05:50 +00:00
Sovran Systems 878392d998 Merge pull request #274 from naturallaw777/copilot/add-no-sleep-nixos-module
Add always-loaded core no-sleep module to disable suspend/hibernate system-wide
2026-04-22 07:03:14 -05:00
copilot-swe-agent[bot]andnaturallaw777 d6471aad55 feat(core): add system-level no-sleep module
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b0e72301-13fd-4c14-9b3b-584e8c04267f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-22 12:00:45 +00:00
copilot-swe-agent[bot] 4b939affaf Initial plan 2026-04-22 11:58:07 +00:00
naturallaw777 c1b02e0562 bitcon knots fix 2026-04-18 10:08:20 -05:00
naturallaw777 6b962ff51d nixpkgs update 2026-04-18 09:45:57 -05:00
Sovran_Systems 3843f8ea22 Merge pull request #273 from naturallaw777/copilot/update-backup-script-to-backup-var-lib
[WIP] Update backup script to back up entire /var/lib directory
2026-04-18 08:50:45 -05:00
copilot-swe-agent[bot]andnaturallaw777 c85eea719d backup: harden desktop var-lib exclusions
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d8d4b876-dfc7-42fd-954c-a9e5b05dc497

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-18 13:49:57 +00:00
copilot-swe-agent[bot]andnaturallaw777 5309618747 backup: tighten rsync var-lib exclude patterns
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d8d4b876-dfc7-42fd-954c-a9e5b05dc497

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-18 13:49:07 +00:00
copilot-swe-agent[bot]andnaturallaw777 725aad3aac backup: include full /var/lib in manual backup stages
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d8d4b876-dfc7-42fd-954c-a9e5b05dc497

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-18 13:47:22 +00:00
copilot-swe-agent[bot] 070ab61131 Initial plan 2026-04-18 13:44:03 +00:00
naturallaw777 164f052b1f updated rpc bitcoinnd port 2026-04-17 18:05:24 -05:00
Sovran_Systems 8841a8d628 Merge pull request #272 from naturallaw777/copilot/fix-clear-site-data-header
Preserve Hub login session on `sovransystemsos.local` by narrowing `Clear-Site-Data`
2026-04-17 17:57:21 -05:00
copilot-swe-agent[bot]andnaturallaw777 d500d15e12 fix(caddy): preserve hub session cookie on mDNS vhost
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/45cc1510-356d-4d59-a6d2-b9b4903cff23

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-17 22:56:23 +00:00
copilot-swe-agent[bot] 16898e8eb9 Initial plan 2026-04-17 22:55:25 +00:00
Sovran_Systems c809045014 Merge pull request #271 from naturallaw777/copilot/fix-wordpress-permissions-ownership
[WIP] Fix WordPress directory ownership and permissions
2026-04-17 10:42:01 -05:00
copilot-swe-agent[bot]andnaturallaw777 158d369371 Fix WordPress ownership and tighten permissions to match php-fpm pool
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3c7ded55-8f08-46f7-af17-6bbbdadba84b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-17 15:41:13 +00:00
copilot-swe-agent[bot] c40db26e6f Initial plan 2026-04-17 15:38:41 +00:00
Sovran_Systems 42305f7f22 Merge pull request #270 from naturallaw777/copilot/remove-orphaned-mypool
[WIP] Clean up PHP-FPM pool architecture and expose custom PHP properly
2026-04-17 08:08:29 -05:00
copilot-swe-agent[bot]andnaturallaw777 539ede00cb refactor php-fpm pool wiring to shared phpPackage option
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/13105350-82a0-4135-b8a4-55016f202195

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-17 13:07:25 +00:00
copilot-swe-agent[bot] 5324344eed Initial plan 2026-04-17 13:05:11 +00:00
Sovran_Systems d2c9dd1fbd Merge pull request #269 from naturallaw777/copilot/fix-nextcloud-wordpress-inactive-status
Use canonical domain files for pre-existing Nextcloud/WordPress credentials and fix Nextcloud reset command
2026-04-17 07:21:22 -05:00
copilot-swe-agent[bot]andnaturallaw777 a1db5773fc Use canonical domain files in detect-existing credentials
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/f9e2b5f9-b25b-4ab9-a3cf-5b8bd4ea22de

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-17 12:17:17 +00:00
copilot-swe-agent[bot] 5e33a250d5 Initial plan 2026-04-17 12:15:03 +00:00
Sovran_Systems 7e0cda17f3 Remove localhost from url_preview_ip_ranger_whitelist 2026-04-16 23:49:04 -05:00
Sovran_Systems 15821207dc Merge pull request #268 from naturallaw777/copilot/fix-php-fpm-unit-mismatch
Restore Nextcloud/WordPress hub visibility for pre-existing installs (dedicated PHP-FPM pools + detect-existing credential/domain recovery)
2026-04-16 23:07:50 -05:00
copilot-swe-agent[bot]andnaturallaw777 cdb93ad8dc fix: detect existing Nextcloud/WordPress installs and add dedicated php-fpm pools
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/6d5b7710-ee06-40ff-8975-f8edca8b879f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-17 03:51:10 +00:00
copilot-swe-agent[bot] 0c596fb396 Initial plan 2026-04-17 03:48:26 +00:00
Sovran_Systems a3c1b849f2 Merge pull request #267 from naturallaw777/copilot/implement-password-reset-flow
Add migration-safe free password handoff for desktop roles (GDM + onboarding)
2026-04-16 22:39:16 -05:00
copilot-swe-agent[bot]andnaturallaw777 7262694425 fix migration checklist to mark noted only after acknowledgement
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/59fc567c-4bd4-44ab-a2ff-8e74854030e5

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-17 03:34:09 +00:00
copilot-swe-agent[bot]andnaturallaw777 ff1defcaab refactor onboarding migration state flags
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/59fc567c-4bd4-44ab-a2ff-8e74854030e5

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-17 03:32:42 +00:00
copilot-swe-agent[bot]andnaturallaw777 6ac9a7cd4c fix migration-safe free password flow for desktop roles
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/59fc567c-4bd4-44ab-a2ff-8e74854030e5

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-17 03:30:26 +00:00
copilot-swe-agent[bot] cb9172d069 Initial plan 2026-04-17 03:24:31 +00:00
Sovran_Systems 92dd718362 Merge pull request #266 from naturallaw777/copilot/remove-restart-service-button
[WIP] Remove Restart Service button from Nextcloud and WordPress service modals
2026-04-16 12:54:05 -05:00
copilot-swe-agent[bot]andnaturallaw777 7d15b67463 fix: hide restart troubleshooting for nextcloud and wordpress php-fpm services
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/fb1bb511-22f7-4b0b-b07e-2bc59ee468ac

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-16 17:53:01 +00:00
copilot-swe-agent[bot] 12b2d85fb4 Initial plan 2026-04-16 17:50:02 +00:00
Sovran_Systems 8657bdc23a Merge pull request #265 from naturallaw777/copilot/remove-bisq-auto-link-tile
[WIP] Remove Bisq Auto-Link tile and associated code
2026-04-16 12:49:31 -05:00
copilot-swe-agent[bot]andnaturallaw777 59cbc8d4e9 remove bisq auto-link tile and autoconnect service
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/50ccdba4-2fbf-4c08-b7ae-7d1b92f7a75e

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-16 17:47:29 +00:00
copilot-swe-agent[bot] 80fea3301b Initial plan 2026-04-16 17:40:57 +00:00
Sovran_Systems 91a3e68119 Remove server configuration from Bitcoin ecosystem 2026-04-16 12:21:19 -05:00
Sovran_Systems bda9c3cd0e Merge pull request #263 from naturallaw777/copilot/add-restart-button-to-service-modal
Add safe service restart action to Service Detail modal
2026-04-16 12:18:02 -05:00
Sovran_Systems a84e958182 Refactor wallet-autoconnect configuration
Removed waiting mechanism for bitcoind RPC readiness and updated btcNodes configuration.
2026-04-16 12:10:27 -05:00
Sovran_Systems 21e0f284b6 Merge pull request #264 from naturallaw777/copilot/fix-reboot-button-issue
[WIP] Fix reboot button functionality after update
2026-04-16 12:06:58 -05:00
copilot-swe-agent[bot]andnaturallaw777 e83b4ff5b1 fix(web): exempt reboot endpoint from auth middleware
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c4b5b663-c6a6-4c78-a788-9dd47ef85628

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-16 17:06:00 +00:00
copilot-swe-agent[bot] 0445a1c1cc Initial plan 2026-04-16 17:04:06 +00:00
naturallaw777 dc1d89b441 updated hub to proper bitcoinnd port 2026-04-16 11:30:02 -05:00
naturallaw777 0da964bfca updated electrs and bitciond 2026-04-16 11:17:33 -05:00
copilot-swe-agent[bot]andnaturallaw777 b5e89c38f8 Improve restart fallback error message
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8e6c98f7-8b24-4ec0-944b-0310e0989495

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-16 15:19:16 +00:00
copilot-swe-agent[bot]andnaturallaw777 c37816d257 Address review nits for restart flow
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8e6c98f7-8b24-4ec0-944b-0310e0989495

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-16 15:18:34 +00:00
copilot-swe-agent[bot]andnaturallaw777 fce4608647 Add service restart API and modal restart action
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8e6c98f7-8b24-4ec0-944b-0310e0989495

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-16 15:13:07 +00:00
copilot-swe-agent[bot] 8fd08057d8 Initial plan 2026-04-16 14:58:54 +00:00
Sovran_Systems 0563c6b96b Merge pull request #262 from naturallaw777/copilot/fix-bisq-hub-node-connection
Align Bisq and Hub local Bitcoin node endpoint with nix-bitcoin default port (8335)
2026-04-15 20:12:23 -05:00
copilot-swe-agent[bot]andnaturallaw777 b29ed2cce7 Fix Bisq and Hub local node port to 8335
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/05dac8a9-797a-49d0-9b41-4b4e5be56ecf

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-16 01:11:10 +00:00
copilot-swe-agent[bot] f8ecbf3ee3 Initial plan 2026-04-16 01:09:40 +00:00
naturallaw777 20aa66a160 updated electrs for localhost 2026-04-15 19:18:17 -05:00
naturallaw777 976d3b0fa7 updated bitcoind and eletrs for localhost 2026-04-15 19:13:33 -05:00
Sovran_Systems 2e9d989444 Merge pull request #261 from naturallaw777/copilot/check-timechain-data-drive
Preserve existing Bitcoin timechain data drive during OS reinstall
2026-04-15 15:25:05 -05:00
copilot-swe-agent[bot]andnaturallaw777 38207e8b2f Harden GUI timechain detection temp mount handling
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8a51f052-83d0-4079-8338-5cfdbb849aa2

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 20:18:04 +00:00
copilot-swe-agent[bot]andnaturallaw777 5fe2ecd56d Refine preservation detection messaging and label fallback
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8a51f052-83d0-4079-8338-5cfdbb849aa2

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 20:15:59 +00:00
copilot-swe-agent[bot]andnaturallaw777 846e2af705 Preserve existing Bitcoin data drive during reinstall
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8a51f052-83d0-4079-8338-5cfdbb849aa2

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 20:13:18 +00:00
copilot-swe-agent[bot] c8eb452a70 Initial plan 2026-04-15 20:11:07 +00:00
Sovran_Systems 46b8c23578 Merge pull request #260 from naturallaw777/copilot/fix-health-status-discrepancy
[WIP] Fix health status discrepancy between service tile and modal
2026-04-15 15:02:26 -05:00
copilot-swe-agent[bot]andnaturallaw777 db32796675 Add DNS mismatch check to tile health computation
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b23873c9-fca8-4e98-8300-003c3302aee4

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 20:02:01 +00:00
copilot-swe-agent[bot] ecd5ecd659 Initial plan 2026-04-15 19:59:35 +00:00
Sovran_Systems 99f86e1cda Merge pull request #259 from naturallaw777/copilot/fix-service-tile-status
Fix domain service tile health precedence and remove blocking Step 3 reachability from service detail
2026-04-15 14:03:46 -05:00
copilot-swe-agent[bot]andnaturallaw777 630cfef690 Fix domain service health precedence and cached checklist reachability
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d3af0b56-5b37-4eaa-a4fc-e7ffa2872c21

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 19:00:47 +00:00
copilot-swe-agent[bot] 6c0afc0e6b Initial plan 2026-04-15 18:55:17 +00:00
Sovran_Systems 709bd51413 Merge pull request #258 from naturallaw777/copilot/fix-nextcloud-dotfiles-issue
Fix Nextcloud core integrity check by preserving dotfiles during install
2026-04-15 13:41:01 -05:00
copilot-swe-agent[bot]andnaturallaw777 37370fd12f fix(nextcloud): copy extracted dotfiles during init install
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/bf1371c0-14ee-477b-9d30-baf97d8f853c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:37:38 +00:00
Sovran_Systems 1e2b11b235 Merge pull request #257 from naturallaw777/copilot/optimize-dns-resolution-delay
Remove blocking DNS checks from `/api/services` by making domain health cache-only
2026-04-15 13:37:15 -05:00
copilot-swe-agent[bot] d636e0fa38 Initial plan 2026-04-15 18:35:51 +00:00
copilot-swe-agent[bot]andnaturallaw777 31c7b796f8 Use cached domain reachability only in api_services health
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3212805f-2cc0-4576-8cda-c3c303f0de47

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:34:18 +00:00
copilot-swe-agent[bot] 8a57734a42 Initial plan 2026-04-15 18:31:55 +00:00
Sovran_Systems 0b76a257ce Merge pull request #256 from naturallaw777/copilot/fix-security-setup-warnings
Nextcloud first-launch hardening: clear Security & Setup warnings via init-time OCC + PHP-FPM override
2026-04-15 13:22:06 -05:00
copilot-swe-agent[bot]andnaturallaw777 7a0a43dfd3 Add server_id guard and AppAPI rationale
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e94844f0-187d-4b52-9302-7e61d3e5804a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:19:49 +00:00
copilot-swe-agent[bot]andnaturallaw777 0d318d60ac Harden server_id setup and app_api disable flow
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e94844f0-187d-4b52-9302-7e61d3e5804a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:19:13 +00:00
copilot-swe-agent[bot]andnaturallaw777 25fe8844e5 Refine server_id generation and AppAPI disable guard
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e94844f0-187d-4b52-9302-7e61d3e5804a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:18:32 +00:00
copilot-swe-agent[bot]andnaturallaw777 d468678d00 Fix Nextcloud first-launch security/setup warnings
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e94844f0-187d-4b52-9302-7e61d3e5804a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:17:37 +00:00
Sovran_Systems 0af4c391e8 Merge pull request #255 from naturallaw777/copilot/fix-tile-loading-spinner
Show per-tile “Checking…” state while domain reachability cache warms up
2026-04-15 13:17:15 -05:00
copilot-swe-agent[bot]andnaturallaw777 5bb8af7a3e refactor: reuse cached reachability lookup in service health
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3208f380-e8fe-4f12-b83c-723ecee6cd4c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:15:30 +00:00
copilot-swe-agent[bot] c86cb9afe0 Initial plan 2026-04-15 18:15:23 +00:00
copilot-swe-agent[bot]andnaturallaw777 9c34eb0694 feat: add checking state for domain reachability on service tiles
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3208f380-e8fe-4f12-b83c-723ecee6cd4c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:12:13 +00:00
copilot-swe-agent[bot] 337f858a7a Initial plan 2026-04-15 18:08:40 +00:00
Sovran_Systems 063c76f8ce Merge pull request #254 from naturallaw777/copilot/update-wallpaper-application-logic
Apply GNOME wallpaper by version on all machines while preserving one-time theme bootstrap
2026-04-15 13:07:09 -05:00
copilot-swe-agent[bot]andnaturallaw777 a0e110b376 fix(desktop): harden wallpaper version stamp read
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3c085026-21a9-4afb-b39f-1d04f1ddd49f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:04:02 +00:00
copilot-swe-agent[bot]andnaturallaw777 8cf43fd3d1 chore(desktop): simplify wallpaper path usage in init script
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3c085026-21a9-4afb-b39f-1d04f1ddd49f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:03:14 +00:00
copilot-swe-agent[bot]andnaturallaw777 6f63e0f4d0 fix(desktop): apply wallpaper on version changes before legacy guards
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3c085026-21a9-4afb-b39f-1d04f1ddd49f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 18:02:30 +00:00
copilot-swe-agent[bot] d458d8c07a Initial plan 2026-04-15 18:00:21 +00:00
Sovran_Systems 6c7b1587b3 Merge pull request #252 from naturallaw777/copilot/fix-root-directory-ownership
Adjust Nextcloud ownership model and data directory initialization path
2026-04-15 12:41:52 -05:00
Sovran_Systems be8d5ccf16 Merge pull request #253 from naturallaw777/copilot/fix-reboot-functionality-issue
Prevent Hub auto-restart from interrupting machine reboot
2026-04-15 12:39:58 -05:00
copilot-swe-agent[bot]andnaturallaw777 18c60bf085 Add reboot conflict and SIGTERM restart prevention for hub service
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/83e39fad-8cf8-4008-8977-a07a77b2f7a3

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 17:38:12 +00:00
copilot-swe-agent[bot] d3d90f6e94 Initial plan 2026-04-15 17:35:39 +00:00
copilot-swe-agent[bot]andnaturallaw777 d874c97b2f Adjust Nextcloud ownership and data directory handling
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c9190fb9-a4ac-42d9-b85d-2b9367c1a901

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 17:27:19 +00:00
copilot-swe-agent[bot] 2e93514a4d Initial plan 2026-04-15 17:25:34 +00:00
Sovran_Systems 990ded6d1d Merge pull request #251 from naturallaw777/copilot/fix-service-status-checks
Normalize health status for enabled-but-inactive domain services in services + detail APIs
2026-04-15 12:14:48 -05:00
Sovran_Systems 4e501548ac Merge pull request #250 from naturallaw777/copilot/fix-hub-reboot-functionality
Fix Hub reboot path to use forced systemd reboot unit
2026-04-15 12:12:30 -05:00
copilot-swe-agent[bot]andnaturallaw777 2073303b18 Refine inactive branch variable naming in services health logic
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0baad662-d798-4d3e-a079-eefece637ab7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 17:10:45 +00:00
copilot-swe-agent[bot]andnaturallaw777 1651f8de37 Clean up inactive health variable naming in service detail
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0baad662-d798-4d3e-a079-eefece637ab7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 17:09:21 +00:00
copilot-swe-agent[bot]andnaturallaw777 1b2c0f2c1c Fix inactive domain services health to show needs_attention on domain/port issues
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0baad662-d798-4d3e-a079-eefece637ab7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 17:08:02 +00:00
copilot-swe-agent[bot]andnaturallaw777 40c2d17833 fix: route hub reboot through forced systemd reboot unit
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c72ca380-983e-4811-98f7-98f883ef46dc

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 17:07:07 +00:00
copilot-swe-agent[bot] 2ff983f5f4 Initial plan 2026-04-15 17:05:40 +00:00
copilot-swe-agent[bot] fc6f58b00e Initial plan 2026-04-15 17:04:40 +00:00
Sovran_Systems 09c4249cae Merge pull request #248 from naturallaw777/copilot/fix-port-forwarding-notification
Make enable-feature port requirements modal context-aware (show only closed ports)
2026-04-15 11:41:06 -05:00
Sovran_Systems 8be2a4fe44 Merge pull request #249 from naturallaw777/copilot/fix-reboot-flow-issue
Fix Hub reboot flow to invoke system reboot directly
2026-04-15 11:39:14 -05:00
copilot-swe-agent[bot]andnaturallaw777 d973fae4db Handle non-OK port status responses in enable flow
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e1d94cfc-9b91-48a3-99e3-64d7609ba710

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:38:07 +00:00
copilot-swe-agent[bot]andnaturallaw777 05c08532b3 Log port status fetch failures before fallback modal
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e1d94cfc-9b91-48a3-99e3-64d7609ba710

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:36:53 +00:00
copilot-swe-agent[bot]andnaturallaw777 8970e8a689 fix: call reboot binary directly and drop reboot oneshot unit
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/1d9cb014-ee8b-44f1-9638-67e38cc2417b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:35:43 +00:00
copilot-swe-agent[bot]andnaturallaw777 8d97184105 Make port requirement modal check only closed ports
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e1d94cfc-9b91-48a3-99e3-64d7609ba710

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:34:56 +00:00
copilot-swe-agent[bot] 1ce4a2a520 Initial plan 2026-04-15 16:33:30 +00:00
copilot-swe-agent[bot] 97a868e0f9 Initial plan 2026-04-15 16:31:03 +00:00
Sovran_Systems 50a2fc0807 Merge pull request #247 from naturallaw777/copilot/fix-tile-health-discrepancy
Align `/api/services` tile health with full domain diagnostics via background reachability cache
2026-04-15 11:15:02 -05:00
Sovran_Systems b6c7c039b2 Merge pull request #246 from naturallaw777/copilot/fix-port-status-indicator
Remove misleading port-status diagnostics from Enable Feature port-forwarding modal
2026-04-15 11:13:20 -05:00
copilot-swe-agent[bot]andnaturallaw777 49e8a96aab Ensure background checker cancellation is not swallowed
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/038b6d9a-0298-41d7-949f-40069cd3320f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:10:51 +00:00
copilot-swe-agent[bot]andnaturallaw777 19273e6d10 refactor: simplify modal handler wiring for rerender safety
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/15310f2a-9bf2-4813-b2be-7462cb923c9c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:10:48 +00:00
copilot-swe-agent[bot]andnaturallaw777 bb07fbd2c3 Add safe startup/shutdown lifecycle for domain reachability task
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/038b6d9a-0298-41d7-949f-40069cd3320f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:09:39 +00:00
copilot-swe-agent[bot]andnaturallaw777 6ea8810881 chore: log network fetch failures in port requirements modal
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/15310f2a-9bf2-4813-b2be-7462cb923c9c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:09:23 +00:00
copilot-swe-agent[bot]andnaturallaw777 587f2a09f8 Refine background checker constants and repeated failure logging
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/038b6d9a-0298-41d7-949f-40069cd3320f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:08:25 +00:00
copilot-swe-agent[bot]andnaturallaw777 1d15997745 fix: remove misleading port status from enable-feature modal
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/15310f2a-9bf2-4813-b2be-7462cb923c9c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:07:22 +00:00
copilot-swe-agent[bot]andnaturallaw777 da0c79d479 Add background domain reachability cache for service tile health
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/038b6d9a-0298-41d7-949f-40069cd3320f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 16:07:00 +00:00
copilot-swe-agent[bot] 4119a4ef61 Initial plan 2026-04-15 16:04:21 +00:00
copilot-swe-agent[bot] 604eb11584 Initial plan 2026-04-15 16:01:35 +00:00
Sovran_Systems 7986de0b63 Merge pull request #245 from naturallaw777/copilot/fix-reboot-issue-in-sovran-hub
Decouple Hub-triggered reboot from `sovran-hub-web` cgroup via dedicated systemd unit
2026-04-15 10:40:37 -05:00
copilot-swe-agent[bot]andnaturallaw777 3f345dbc02 fix: detach reboot via dedicated systemd oneshot unit
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0338009f-7d7f-4c99-94c1-32cb9b68b5e0

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 15:37:40 +00:00
copilot-swe-agent[bot] 9998306a0c Initial plan 2026-04-15 15:32:38 +00:00
Sovran_Systems 42e2e3dd16 Merge pull request #244 from naturallaw777/copilot/optimize-api-services-response-time
Decouple `/api/services` tile health from slow curl-based domain diagnostics
2026-04-15 10:24:33 -05:00
copilot-swe-agent[bot]andnaturallaw777 6b44c03fd8 perf: avoid curl-based domain checks in /api/services tile health
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/f8a8cbe6-164d-4ddc-a248-e535a2fad801

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 15:17:09 +00:00
Sovran_Systems 1931a99e65 Merge pull request #243 from naturallaw777/copilot/add-domain-setup-modals
Split domain setup UX into distinct Configure vs Reconfigure modal flows
2026-04-15 10:11:28 -05:00
copilot-swe-agent[bot] 4ce6341eb3 Initial plan 2026-04-15 15:11:07 +00:00
copilot-swe-agent[bot]andnaturallaw777 4301629606 feat: add dedicated domain reconfigure modal flow
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/34ab0742-1af8-46e9-9b12-a480c93366f1

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 15:07:23 +00:00
copilot-swe-agent[bot] cf39e28921 Initial plan 2026-04-15 15:01:26 +00:00
Sovran_Systems b252014158 Merge pull request #242 from naturallaw777/copilot/update-domain-health-check-system
[WIP] Update domain health check system to improve diagnostics
2026-04-15 07:47:19 -05:00
copilot-swe-agent[bot]andnaturallaw777 86942ebc33 feat: replace domain port table with sequential domain diagnostics
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/93de7af8-10f9-438e-b9bc-8c6e9d39d787

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 12:45:02 +00:00
copilot-swe-agent[bot] 13f15cb845 Initial plan 2026-04-15 12:34:39 +00:00
Sovran_Systems 5c19de6fb8 Merge pull request #241 from naturallaw777/copilot/fix-reboot-command-path
Use absolute NixOS `systemctl` path for Hub reboot command
2026-04-14 20:49:52 -05:00
copilot-swe-agent[bot]andnaturallaw777 dfcc3858f0 fix: use absolute systemctl path for reboot command
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a2a440f0-d3a9-47ba-9278-98cac7789dfa

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 01:44:59 +00:00
copilot-swe-agent[bot] bebca8f1ab Initial plan 2026-04-15 01:43:37 +00:00
Sovran_Systems 30b3f14292 Merge pull request #240 from naturallaw777/copilot/fix-domain-health-checks
Align domain health across dashboard/detail and cache external IP server-side
2026-04-14 20:36:12 -05:00
copilot-swe-agent[bot]andnaturallaw777 f24c9c45b2 fix: cache external ip and align domain health checks
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/01540c60-35d3-481a-8558-945a81d86976

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-15 01:31:47 +00:00
copilot-swe-agent[bot] 04587efad3 Initial plan 2026-04-15 01:30:18 +00:00
Sovran_Systems 023b00d297 Merge pull request #239 from naturallaw777/copilot/fix-sovran-hub-service-path
[WIP] Fix sovran-hub service PATH for required binaries
2026-04-14 17:01:45 -05:00
copilot-swe-agent[bot]andnaturallaw777 7ec47abe17 fix(hub): add network utility binaries to sovran-hub-web service PATH
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/cc568566-7619-4546-af51-5173b55440a6

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-14 22:01:21 +00:00
copilot-swe-agent[bot] 9c47c99645 Initial plan 2026-04-14 22:00:01 +00:00
Sovran_Systems cba3d1d092 Merge pull request #238 from naturallaw777/copilot/fix-nft-regex-firewall-allowed-ports
[WIP] Fix nft regex to correctly capture allowed ports
2026-04-14 16:45:01 -05:00
copilot-swe-agent[bot]andnaturallaw777 a135e652bc Fix hub port parsing and status checks for accurate open-port reporting
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/1101b3b2-686b-4023-8229-1b9258214546

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-14 21:44:42 +00:00
copilot-swe-agent[bot] 6c2cbd5b3b Initial plan 2026-04-14 21:42:15 +00:00
Sovran_Systems 7a28b138a9 Merge pull request #236 from naturallaw777/copilot/fix-listening-ports-detection
[WIP] Fix listening ports detection in the Hub dashboard
2026-04-14 16:20:37 -05:00
copilot-swe-agent[bot]andnaturallaw777 a687c05f6c Fix _get_listening_ports() to reliably detect wildcard-bound ports (80/443)
Rewrite the ss output parser to:
- Skip header lines (State/Netid) explicitly
- Only process LISTEN/UNCONN state lines
- Always read parts[3] for local address (the ss column layout is fixed)
- Defensively skip wildcard (*) port values

The previous fix (PR #235) tried both parts[3] and parts[4], but reading
parts[4] (peer address column) was unnecessary. The ss LISTEN output always
places the local address at index 3 when split by whitespace, for all address
formats: 0.0.0.0:PORT, *:PORT, [::]:PORT, 127.0.0.1:PORT.

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/f7ab1d7c-d624-4f1a-9e62-5a9ce4fd4446

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-14 21:20:03 +00:00
copilot-swe-agent[bot] 8c4a8e4313 Initial plan 2026-04-14 21:16:39 +00:00
Sovran_Systems 2d4a3fcdf2 Merge pull request #235 from naturallaw777/copilot/fix-ss-output-parsing-issue
[WIP] Fix parsing of local address in _get_listening_ports function
2026-04-14 16:06:15 -05:00
copilot-swe-agent[bot]andnaturallaw777 adad79c7e8 fix: parse both parts[3] and parts[4] in _get_listening_ports() for ss output
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/380a4877-aaea-47ea-8998-4c60ff6d49d2

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-14 21:05:43 +00:00
copilot-swe-agent[bot] 2e6d88daec Initial plan 2026-04-14 21:03:53 +00:00
naturallaw777 2cd9d7cf20 updated elementcalling firewall typo 2026-04-14 13:30:27 -05:00
naturallaw777 8500e1de05 updated elementcalling firewall 2026-04-14 13:28:35 -05:00
Sovran_Systems fefc7ff81a Merge pull request #234 from naturallaw777/copilot/open-turn-ports-for-livekit
fix(element-call): open TURN firewall ports 5349/TCP and 3478/UDP
2026-04-14 13:24:18 -05:00
copilot-swe-agent[bot]andnaturallaw777 1727755942 fix: open TURN firewall ports 5349 (TCP) and 3478 (UDP) in element-calling.nix
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/484cfc63-13c7-4008-8a94-cff4d554c27c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-14 18:21:21 +00:00
copilot-swe-agent[bot] c6bfe1200c Initial plan 2026-04-14 18:20:38 +00:00
naturallaw777 5095052a53 nixpkgs update 2026-04-14 08:50:54 -05:00
naturallaw777 cc9b41fd37 cleaned up code 2026-04-14 08:49:26 -05:00
Sovran_Systems 8dedd59cc0 Merge pull request #233 from naturallaw777/copilot/fix-99053422-1193689005-4a507209-0427-496c-b028-fcf5e0d153d0
[WIP] Move hardcoded firewall ports from configuration.nix to their respective modules
2026-04-14 08:46:21 -05:00
copilot-swe-agent[bot]andnaturallaw777 57d12aab9e Move firewall ports to their respective service modules
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e4dbc0e0-e273-4e3e-a1ec-059ae9b06a50

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-14 13:45:51 +00:00
copilot-swe-agent[bot] 99413a5dbe Initial plan 2026-04-14 13:44:07 +00:00
naturallaw777 a23e9f5c45 update readme 2026-04-13 19:59:42 -05:00
Sovran_Systems cfadd90d24 Merge pull request #232 from naturallaw777/copilot/fix-upgrade-to-server-desktop
Fix node→server+desktop upgrade: defer rebuild until after onboarding collects domains/SSL/ports
2026-04-13 19:48:20 -05:00
copilot-swe-agent[bot]andnaturallaw777 ac47f39117 Fix node-to-server upgrade: reboot before rebuild so onboarding collects domains first
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8d0387a6-f66c-4fe8-8df1-0abf657b2fba

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-14 00:46:50 +00:00
copilot-swe-agent[bot] 55cd583569 Initial plan 2026-04-14 00:43:21 +00:00
naturallaw777 77bdf710c7 update readme 2026-04-13 19:05:47 -05:00
Sovran_Systems 60723689e1 Merge pull request #231 from naturallaw777/copilot/fix-reboot-button-issues
[WIP] Fix reboot button functionality after system update
2026-04-13 19:03:06 -05:00
copilot-swe-agent[bot]andnaturallaw777 7576c0fe85 Fix reboot flow: add /api/ping, fix waitForServerReboot polling, fix security.js handler
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ee8673cf-ad65-4f65-b5c8-2f170e78022f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-14 00:02:45 +00:00
copilot-swe-agent[bot] c5bbb5220e Initial plan 2026-04-13 23:59:21 +00:00
naturallaw777 57dcf312bc update readme 2026-04-13 18:50:04 -05:00
Sovran_Systems 9fe6e108a9 Merge pull request #230 from naturallaw777/copilot/fix-reboot-button-local-computer
Fix reboot button hang and overlay spin-forever on local machine and LAN
2026-04-13 18:42:36 -05:00
copilot-swe-agent[bot]andnaturallaw777 a6dc3fd647 Fix reboot button and overlay bugs on local machine and LAN
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c8d3bf30-c3ea-40e7-8da0-b4baa28eaf36

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 23:41:33 +00:00
copilot-swe-agent[bot] e33f4b570a Initial plan 2026-04-13 23:38:45 +00:00
naturallaw777 18b454e07b updated caddy icon 2026-04-13 18:24:58 -05:00
Sovran_Systems 20b1486547 Merge pull request #229 from naturallaw777/copilot/fix-reboot-issue-fetch-call
fix: use apiFetch in doReboot() to send session credentials with reboot request
2026-04-13 18:17:47 -05:00
copilot-swe-agent[bot]andnaturallaw777 068c78bd27 fix: use apiFetch instead of fetch in doReboot() to include session credentials
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/34f637cc-b8b9-42e1-bdeb-e4b252fae648

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 23:16:13 +00:00
copilot-swe-agent[bot] 32b2ee7117 Initial plan 2026-04-13 23:15:32 +00:00
Sovran_Systems 9b5786fce1 Merge pull request #227 from naturallaw777/copilot/fix-btcpay-default-state
[WIP] Fix BTCPay Server default state in Node mode
2026-04-13 17:56:01 -05:00
Sovran_Systems 9c15b458c4 Merge pull request #228 from naturallaw777/copilot/fix-reboot-overlay-stuck
[WIP] Fix reboot overlay getting stuck after system update
2026-04-13 17:55:44 -05:00
copilot-swe-agent[bot]andnaturallaw777 b86fe94d82 Fix: BTCPay off by default in Node role, Caddy conditional ACME/ports
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/2e2b84a8-c5e9-4eea-8bee-fc587bb3a6fa

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 22:55:37 +00:00
copilot-swe-agent[bot]andnaturallaw777 7b7947db9d fix: robust reboot detection with AbortController timeout and serverWentDown flag
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8b653481-74f2-450f-a543-c94eb664645a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 22:55:18 +00:00
copilot-swe-agent[bot] 38b45013b3 Initial plan 2026-04-13 22:54:20 +00:00
copilot-swe-agent[bot] 2db344f91f Initial plan 2026-04-13 22:52:35 +00:00
Sovran_Systems a8d182ffc5 Merge pull request #226 from naturallaw777/copilot/remove-sidebar-upgrade-btn-class
Remove constant accent highlight from "Upgrade to Full Server" sidebar button
2026-04-13 17:38:09 -05:00
copilot-swe-agent[bot]andnaturallaw777 75d3aca1b6 Remove sidebar-upgrade-btn highlight; blend Upgrade button with other sidebar buttons
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/13a21819-24b1-4aee-b9fa-1e6830992e53

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 22:37:06 +00:00
copilot-swe-agent[bot] 21be0af9c1 Initial plan 2026-04-13 22:36:23 +00:00
Sovran_Systems f2a94fa1b5 Merge pull request #225 from naturallaw777/copilot/rename-sparrow-bisq-auto-link
Rename Sparrow/Bisq "Auto-Connect" to "Auto-Link" in Hub
2026-04-13 17:33:47 -05:00
copilot-swe-agent[bot]andnaturallaw777 a086ab689e Rename Sparrow/Bisq Auto-Connect to Auto-Link in sovran-hub.nix
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c1eaaa7d-cfa4-4202-b37c-e6d0c1e49fcd

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 22:32:04 +00:00
copilot-swe-agent[bot] 5b0babed1f Initial plan 2026-04-13 22:31:26 +00:00
Sovran_Systems df88f7c30a Merge pull request #224 from naturallaw777/copilot/update-wallpaper-installation
[WIP] Update wallpaper installation to only include ultrawide version
2026-04-13 17:26:00 -05:00
copilot-swe-agent[bot]andnaturallaw777 fc5432398f Remove standard wallpaper and screen-detection logic, use ultrawide only
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3d5ffac9-c152-4ea7-ba54-cb024dc4acae

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 22:25:36 +00:00
copilot-swe-agent[bot] bf9f596bb3 Initial plan 2026-04-13 22:24:18 +00:00
Sovran_Systems 40dd601e21 Merge pull request #223 from naturallaw777/copilot/replace-locks-with-initialization-script
Replace dconf locks with first-login theme initialization script
2026-04-13 17:20:40 -05:00
copilot-swe-agent[bot]andnaturallaw777 2f67a91b70 feat: replace dconf locks with first-login sovran-theme-init script
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/bb7956f3-e618-4998-8f80-4437478df0f9

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 22:14:15 +00:00
copilot-swe-agent[bot] 5e4e2c8d71 Initial plan 2026-04-13 22:11:54 +00:00
Sovran_Systems e47a238cc8 Merge pull request #222 from naturallaw777/copilot/update-password-labels-clarity
Clarify System Passwords labels and Change Password modal copy in Sovran Hub
2026-04-13 17:00:53 -05:00
copilot-swe-agent[bot]andnaturallaw777 5678b69d4f fix: clarify password labels and change password UI in Sovran Hub
- sovran-hub.nix: rename credential labels for clarity
  - "Free Account — Password" → "Free Account / Hub Login — Password"
  - "Root Password" → "Administrator (root) Password"
  - "SSH Passphrase" → "SSH Passphrase — use via: ssh root@localhost"
- service-detail.js: update Change Password button text
  - "Change Password" → "Change Free Account Password"
- service-detail.js: update Change Password modal
  - Title: "Change Free Account & Hub Login Password"
  - Description: adds Hub login warning
  - Warning note: warns about desktop AND Hub login change
  - Success message: "Free account & Hub login password changed successfully."

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/2ac6f3cf-cf94-47e9-86ac-1321cd5ff728

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 21:59:35 +00:00
copilot-swe-agent[bot] 6126fbf0ca Initial plan 2026-04-13 21:58:15 +00:00
Sovran_Systems 7244f559c1 Merge pull request #221 from naturallaw777/copilot/update-sovran-hub-desktop-file
fix: match StartupWMClass to Brave's actual Wayland app_id for dock icon
2026-04-12 22:59:04 -05:00
copilot-swe-agent[bot]andnaturallaw777 892305e416 fix: set StartupWMClass to actual Brave Wayland app_id, remove ignored flags
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/fa122077-31dc-41d8-8a54-720d747d4dda

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 03:58:23 +00:00
copilot-swe-agent[bot] 6da8730453 Initial plan 2026-04-13 03:57:26 +00:00
Sovran_Systems 08b3e5645e Merge pull request #220 from naturallaw777/copilot/fix-brave-wrapper-flags
[WIP] Restore critical Brave flags in hub-brave-wrapper
2026-04-12 20:20:49 -05:00
copilot-swe-agent[bot]andnaturallaw777 852098439e Fix: Restore missing Brave Wayland app-id flags and revert StartupWMClass
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ca8bd019-d5f6-42e5-bc39-1de367224ae5

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 01:20:29 +00:00
copilot-swe-agent[bot] be6766871a Initial plan 2026-04-13 01:19:35 +00:00
Sovran_Systems 0e1dbb8809 Merge pull request #219 from naturallaw777/copilot/fix-hub-autolaunch-script
fix: add coreutils to hub-autolaunch-script PATH
2026-04-12 20:14:21 -05:00
copilot-swe-agent[bot]andnaturallaw777 d82d871b88 fix: add coreutils to hub-autolaunch-script PATH so seq and touch are available
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4265b3b7-44b2-4209-8364-19b9d44d4f99

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 01:09:05 +00:00
copilot-swe-agent[bot] 9d81af72ff Initial plan 2026-04-13 01:08:24 +00:00
Sovran_Systems 40180b5cb7 Merge pull request #218 from naturallaw777/copilot/fix-login-ux-issue
Auto-login on desktop launch: skip duplicate password prompt
2026-04-12 20:04:07 -05:00
copilot-swe-agent[bot]andnaturallaw777 b25c077835 Add localhost-only /auto-login endpoint and update Brave launch URL
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a4089cd6-1729-441f-adbf-1fb1c990a4f5

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 01:01:58 +00:00
copilot-swe-agent[bot] 97a7a9163e Initial plan 2026-04-13 00:59:53 +00:00
Sovran_Systems 75e79f2a16 Merge pull request #217 from naturallaw777/copilot/fix-sovran-hub-icon-matching
[WIP] Fix Sovran Hub icon matching in GNOME dock
2026-04-12 19:52:39 -05:00
copilot-swe-agent[bot]andnaturallaw777 340c1cd0f5 Fix GNOME dock icon matching for Sovran Hub on Wayland
- Change StartupWMClass from 'sovran-hub' to 'brave-localhost__-Default' in
  the .desktop file so GNOME Shell matches the window to the correct launcher
- Remove --gtk-application-id and --wayland-app-id flags from hub-brave-wrapper
  since Brave ignores them in --app= mode on Wayland

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/daa85aaf-5b87-448d-8336-d94dc2dfe727

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 00:52:07 +00:00
copilot-swe-agent[bot] 211a32db45 Initial plan 2026-04-13 00:50:55 +00:00
Sovran_Systems 1da6fb9cd6 Merge pull request #216 from naturallaw777/copilot/fix-sovran-hub-icon-issue-again
Fix Sovran Hub dock icon: set Wayland app_id to match .desktop StartupWMClass
2026-04-12 19:34:17 -05:00
copilot-swe-agent[bot]andnaturallaw777 5bdcba8a90 Add --gtk-application-id and --wayland-app-id flags to fix GNOME dock icon on Wayland
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e02d4ee6-56de-49d6-8852-3368232d8d77

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 00:26:34 +00:00
copilot-swe-agent[bot] 9f47fd6ba3 Initial plan 2026-04-13 00:25:41 +00:00
Sovran_Systems 0f8cdc9376 Merge pull request #215 from naturallaw777/copilot/fix-infinite-recursion-error
fix(nixos): break circular dependency between hub-brave-wrapper and sovran-hub-web
2026-04-12 19:16:57 -05:00
copilot-swe-agent[bot]andnaturallaw777 aaa2743fcc fix: break circular dependency in hub-brave-wrapper by using stable system path
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7e18de52-3666-415d-b5cb-eff532805a89

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 00:15:12 +00:00
copilot-swe-agent[bot] 377bb63122 Initial plan 2026-04-13 00:14:35 +00:00
Sovran_Systems a2269c927e Merge pull request #214 from naturallaw777/copilot/fix-sovran-hub-icon-issue
[WIP] Fix Sovran Hub icon not matching Brave window
2026-04-12 19:08:56 -05:00
copilot-swe-agent[bot]andnaturallaw777 314123fcd8 fix: use stable profile dir and GNOME hints in hub-brave-wrapper
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/005f3030-3821-4677-8744-f76770fbbc25

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-13 00:08:13 +00:00
copilot-swe-agent[bot] e6c76e636e Initial plan 2026-04-13 00:07:01 +00:00
Sovran_Systems 5084d6ebb8 Merge pull request #213 from naturallaw777/copilot/remove-chroot-chpasswd-block
[WIP] Remove chroot/chpasswd block from installer script
2026-04-12 18:10:37 -05:00
copilot-swe-agent[bot]andnaturallaw777 b610e76659 Fix chpasswd: remove broken chroot from installer, defer to first-boot service
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e55c1e70-0958-4d77-a222-52dccc9459b2

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 23:10:19 +00:00
copilot-swe-agent[bot] 3fd000ac4e Initial plan 2026-04-12 23:08:49 +00:00
Sovran_Systems 3a59944967 Merge pull request #212 from naturallaw777/copilot/update-installer-password-prompt
[WIP] Update installer to show generated login password
2026-04-12 16:38:34 -05:00
copilot-swe-agent[bot]andnaturallaw777 533c981a70 fix(installer): generate diceware password during install and display before reboot
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ed1c266b-2f38-4831-9ba0-fa0f59cd162b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 21:38:18 +00:00
copilot-swe-agent[bot] 92b46d1bba Initial plan 2026-04-12 21:35:00 +00:00
Sovran_Systems 317157de2d Merge pull request #211 from naturallaw777/copilot/fix-headscale-cli-syntax
[WIP] Fix Headscale CLI syntax issues in documentation
2026-04-12 15:54:01 -05:00
copilot-swe-agent[bot]andnaturallaw777 543a9df0bf feat: add sovran-provisioner.nix and fix headscale 0.28.0 CLI syntax in docs
- Create modules/core/sovran-provisioner.nix with Flask provisioner API,
  Headscale 0.28.0 config, Caddy reverse proxy, auto-bootstrap service,
  and firewall rules. Python script uses get_user_id() + -u <id> syntax.
- Fix docs/remote-deploy-headscale.md:
  - nodes register now uses -u <id> instead of --user <name>
  - preauthkeys create one-liner uses -u <id> -e 2h -o json
  - preauthkeys list/expire updated to 0.28.0 syntax (no --user on list)
  - tailscale up in Part 2 now includes --accept-dns=false
  - Add Troubleshooting section: VPN conflicts, RATELIMIT logs,
    connection refused, user ID lookup

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/25b789a6-8b2c-4e42-afd4-f8e8e5c61f2c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 20:53:10 +00:00
copilot-swe-agent[bot] 8f792bb192 Initial plan 2026-04-12 20:50:10 +00:00
Sovran_Systems 7ba223a25a Merge pull request #209 from naturallaw777/copilot/update-gnome-app-folders
[WIP] Update GNOME app folders to organize missing apps
2026-04-12 14:08:24 -05:00
copilot-swe-agent[bot]andnaturallaw777 9db77b84bd Add missing apps to Office and Terminal GNOME app folders
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8d9e25f6-c812-4f06-8a8b-d19728caea05

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 19:08:08 +00:00
copilot-swe-agent[bot] 8af03e53db Initial plan 2026-04-12 19:07:20 +00:00
Sovran_Systems e190cce593 Merge pull request #208 from naturallaw777/copilot/fix-ui-hang-on-update
fix: exempt update/rebuild status endpoints from auth to resolve post-restart UI hang
2026-04-12 13:49:07 -05:00
copilot-swe-agent[bot]andnaturallaw777 c498064e80 fix: exempt update/rebuild status endpoints from auth to fix post-restart polling hang
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d6d0347e-37d0-48bf-8e38-b7828ce4bb3f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 18:48:00 +00:00
copilot-swe-agent[bot] 56652c37f4 Initial plan 2026-04-12 18:47:12 +00:00
Sovran_Systems 8565cda7b4 Merge pull request #207 from naturallaw777/copilot/fix-security-reset-overlay-issues
Fix security reset overlay: backdrop blur + close support modal on activation
2026-04-12 13:38:59 -05:00
copilot-swe-agent[bot]andnaturallaw777 18124ff2a1 Fix broken UI on security reset overlay: add backdrop-filter and close support modal
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/747e607a-1980-434c-9278-344ea75b8bc1

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 18:34:38 +00:00
copilot-swe-agent[bot] d61211e300 Initial plan 2026-04-12 18:33:50 +00:00
Sovran_Systems 17e6d0d180 Merge pull request #206 from naturallaw777/copilot/update-gnome-power-settings
[WIP] Update GNOME power management settings for server roles
2026-04-12 13:18:24 -05:00
copilot-swe-agent[bot]andnaturallaw777 46a112a8e1 feat: disable GNOME auto-suspend, screen dim, and screen lock defaults
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0ef47453-73af-4c18-a63f-d4bcccce2f37

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 18:17:52 +00:00
copilot-swe-agent[bot] a8efb3d880 Initial plan 2026-04-12 18:16:38 +00:00
Sovran_Systems be0eebdb8b Merge pull request #205 from naturallaw777/copilot/fix-security-banner-issue
Fix security banner reappearing after global security reset
2026-04-12 13:13:18 -05:00
copilot-swe-agent[bot]andnaturallaw777 8caee2ec22 fix: write SECURITY_BANNER_DISMISSED_FLAG after security reset to prevent banner reappearing after reboot
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8a33795f-2791-4029-98c3-1d703054404f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 18:08:22 +00:00
copilot-swe-agent[bot] fe51d69700 Initial plan 2026-04-12 18:06:28 +00:00
Sovran_Systems 116197e9fb Merge pull request #204 from naturallaw777/copilot/add-login-page-for-hub-authentication
Add password authentication to Sovran Hub web interface
2026-04-12 10:42:05 -05:00
copilot-swe-agent[bot]andnaturallaw777 56e1da93c1 Address code review feedback: improve session secret generation, document rate-limit design
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/afb996f6-f6f5-4d4a-9f99-e46e3f89b4d7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 15:40:25 +00:00
copilot-swe-agent[bot]andnaturallaw777 02e40e6634 Add Hub web authentication: login page, session middleware, logout button
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/afb996f6-f6f5-4d4a-9f99-e46e3f89b4d7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 15:37:35 +00:00
copilot-swe-agent[bot] 650d693849 Initial plan 2026-04-12 15:31:33 +00:00
Sovran_Systems 1dac6a0527 Merge pull request #203 from naturallaw777/copilot/fix-gnome-keyring-authentication
fix: disable auto-login, diceware passwords, decoupled security reset UX
2026-04-12 10:12:12 -05:00
copilot-swe-agent[bot]andnaturallaw777 17f89fa773 fix: disable auto-login, diceware passwords, improved security reset UX, fix GNOME keyring
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/82a54a25-4844-4a41-afcc-c034cebbd6ed

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 15:08:25 +00:00
copilot-swe-agent[bot] 38acee7319 Initial plan 2026-04-12 15:01:51 +00:00
naturallaw777 7cba8e6258 readme update 2026-04-12 09:26:15 -05:00
Sovran_Systems ec641b1b9b Merge pull request #202 from naturallaw777/copilot/fix-sovran-hub-update-issue
Fix update process killed mid-run by nixos-rebuild switch
2026-04-12 09:23:04 -05:00
copilot-swe-agent[bot]andnaturallaw777 008a003fa1 fix: prevent nixos-rebuild from killing in-flight update; improve stale status recovery
Part A (modules/core/sovran-hub.nix):
- Add restartIfChanged=false and stopIfChanged=false to sovran-hub-update service
- Add restartIfChanged=false and stopIfChanged=false to sovran-hub-rebuild service
These prevent nixos-rebuild switch from terminating an in-flight update mid-execution.

Part B (app/sovran_systemsos_web/server.py):
- Replace _recover_stale_status() with improved version
- Use MainPID + os.kill() to guard against transient is-active lies during daemon-reload
- Use ExecMainStatus (actual exit code) instead of Result (may be stale from prior run)

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/63bf2cd5-9c02-4542-8926-44aa9ed63bf0

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 13:47:49 +00:00
copilot-swe-agent[bot] 8310028546 Initial plan 2026-04-12 13:45:19 +00:00
naturallaw777 ac235f2e38 readme update 2026-04-12 08:32:54 -05:00
naturallaw777 e3ecdfafb7 readme update 2026-04-12 08:30:11 -05:00
Sovran_Systems 37bc0c6192 Merge pull request #201 from naturallaw777/copilot/fix-update-modal-message
fix: check_for_updates() tri-state return to unblock updates on inconclusive checks
2026-04-12 07:59:45 -05:00
copilot-swe-agent[bot]andnaturallaw777 536b3bfa78 fix: tri-state check_for_updates() to prevent blocking updates on inconclusive checks
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/6bdd26ad-7b2f-455c-8b34-6be3de48bd9a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 12:58:42 +00:00
copilot-swe-agent[bot] 57de0d31a7 Initial plan 2026-04-12 12:56:17 +00:00
Sovran_Systems bd1acfa266 Merge pull request #200 from naturallaw777/copilot/fix-dconf-settings-priority
[WIP] Fix dconf settings priority for GNOME custom configurations
2026-04-12 07:25:48 -05:00
copilot-swe-agent[bot]andnaturallaw777 a05ca90b2d Add dconf locks to enforce GNOME custom settings on new installs
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/5363d209-197f-4011-ac43-2e5ae3f9931f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 12:25:19 +00:00
copilot-swe-agent[bot] ece73148cd Initial plan 2026-04-12 12:24:20 +00:00
Sovran_Systems 3ead52583f Merge pull request #199 from naturallaw777/copilot/fix-stale-status-logging
Fix update modal UX when hub restarts mid-update
2026-04-12 07:17:59 -05:00
copilot-swe-agent[bot]andnaturallaw777 c7005c93b5 fix: user-friendly stale recovery messages and complete log on reconnect
- _recover_stale_status(): returns True when corrected; changes message from
  internal '[Hub] Stale RUNNING...' to user-friendly text
- _startup_recover_stale_status(): sets _update_recovery_happened flag when
  update recovery happens at startup
- api_updates_status(): uses offset=0 when recovery happened so frontend
  receives the full log, not just a stale delta
- pollUpdateStatus(): when reconnecting after server-down with update done,
  resets offset to 0, re-fetches full log, shows '[Server restarted — update
  completed successfully.]' instead of '[Server reconnected]'

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/90b535d1-bc3b-4147-9d62-3c7a93b1c8e4

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 12:16:22 +00:00
copilot-swe-agent[bot] d2d2ed58a6 Initial plan 2026-04-12 12:05:34 +00:00
Sovran_Systems a48fe1c882 Merge pull request #198 from naturallaw777/copilot/add-gnome-keyring-unlock-service
Unlock GNOME Keyring on session start using stored free-user password
2026-04-12 06:49:28 -05:00
copilot-swe-agent[bot]andnaturallaw777 d07ea9a227 Add gnome-keyring-unlock service and update change-free-password to re-key keyring
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/311643b0-e3d5-4ee5-a8f8-da5baa59cab8

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 11:47:42 +00:00
copilot-swe-agent[bot] 0f77c6834c Initial plan 2026-04-12 11:45:49 +00:00
naturallaw777 f1188abff5 readme update 2026-04-12 00:11:46 -05:00
Sovran_Systems f66de58b78 Merge pull request #197 from naturallaw777/copilot/fix-update-modal-bad-ux
[WIP] Fix update modal to show system status correctly
2026-04-12 00:01:37 -05:00
copilot-swe-agent[bot]andnaturallaw777 90d423e94b fix: remove _recover_stale_status() from api_updates_run, verify unit active before trusting RUNNING status
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3a855cab-5d0d-4c32-984c-5c88d922934e

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 05:00:03 +00:00
copilot-swe-agent[bot] e1a06079fd Initial plan 2026-04-12 04:58:42 +00:00
Sovran_Systems f0f9f6854c Merge pull request #196 from naturallaw777/copilot/fix-sparrow-desktop-references
fix: correct Sparrow desktop entry filename in dock favorites and Bitcoin app folder
2026-04-11 23:49:10 -05:00
copilot-swe-agent[bot]andnaturallaw777 73dd4fbb4b fix: correct Sparrow desktop file name from sparrow-desktop.desktop to sparrow.desktop
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/9085bdd6-ea69-4652-b862-dbc96b85eed0

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 04:48:33 +00:00
copilot-swe-agent[bot] 2edc4c1829 Initial plan 2026-04-12 04:47:49 +00:00
Sovran_Systems 9dab44f7e3 Merge pull request #195 from naturallaw777/copilot/fix-disabled-bitcoin-tile-sync
Fix disabled Bitcoin tile incorrectly showing "Syncing Timechain" when another variant is active
2026-04-11 23:36:19 -05:00
copilot-swe-agent[bot]andnaturallaw777 c8fb773be4 Fix disabled Bitcoin tile incorrectly showing Syncing Timechain
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b87b574a-c97e-45c0-a4c6-396fe3c9c418

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 04:33:38 +00:00
Sovran_Systems 5d43f7e0d7 Merge pull request #194 from naturallaw777/copilot/fix-update-system-ux-issue
Fix false "Update complete" + Reboot button when no updates are available
2026-04-11 23:30:26 -05:00
copilot-swe-agent[bot]andnaturallaw777 31f1e16a3c Fix stale update state causing false Update complete when no updates available
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/1cc7ff30-4a1b-46f7-a20a-2ec0cbdfe291

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 04:29:11 +00:00
copilot-swe-agent[bot] 458b8fae0b Initial plan 2026-04-12 04:28:13 +00:00
copilot-swe-agent[bot] 6e5863ed48 Initial plan 2026-04-12 04:26:11 +00:00
Sovran_Systems 70d5286f87 Merge pull request #193 from naturallaw777/copilot/fix-update-system-flow
fix: skip update flow when no updates are available
2026-04-11 23:14:11 -05:00
copilot-swe-agent[bot]andnaturallaw777 ad6cf6c498 fix: show already up to date instead of full update flow when no updates available
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/fb575403-f4f0-41bb-8fb1-12f7d9874009

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 04:13:13 +00:00
copilot-swe-agent[bot] 51537c47b3 Initial plan 2026-04-12 04:09:58 +00:00
Sovran_Systems 82ecf581c3 Merge pull request #192 from naturallaw777/copilot/fix-iso-build-warning-root-password-options
Fix ISO build warning: multiple root password options set
2026-04-11 21:20:24 -05:00
copilot-swe-agent[bot]andnaturallaw777 a5ff38786c Fix ISO build warning: use lib.mkForce to override root password options
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/aa57a263-33e8-4379-9cc3-379125371b46

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 02:14:14 +00:00
copilot-swe-agent[bot] eb3463cdfe Initial plan 2026-04-12 02:13:26 +00:00
Sovran_Systems ad4337c12f Merge pull request #191 from naturallaw777/copilot/fix-update-modal-race-condition
fix: correct stale RUNNING update status after hub restart mid-update
2026-04-11 20:51:19 -05:00
copilot-swe-agent[bot]andnaturallaw777 d5b16da57e fix: detect and correct stale RUNNING update status on poll and startup
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3c41cfb3-08f3-4e27-900c-7312a9204d4c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 01:47:20 +00:00
copilot-swe-agent[bot] 1a7ed3cb6c Initial plan 2026-04-12 01:43:05 +00:00
naturallaw777 1ecd1e1470 nixpkgs update 2026-04-11 20:21:24 -05:00
naturallaw777 7e1c7b0dbd fixed icon color 2026-04-11 20:12:12 -05:00
Sovran_Systems ab1150266b Merge pull request #190 from naturallaw777/copilot/update-vaultwarden-icon-color
[WIP] Change Vaultwarden icon color to white
2026-04-11 20:10:12 -05:00
copilot-swe-agent[bot]andnaturallaw777 97ceb0c0ce Change Vaultwarden icon stroke color from black to white
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8377658e-a339-407d-9813-b3ada2b710a5

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 01:09:51 +00:00
copilot-swe-agent[bot] e41b78897d Initial plan 2026-04-12 01:09:00 +00:00
naturallaw777 67678b7927 cleaned up repo 2026-04-11 20:02:43 -05:00
Sovran_Systems 903be87154 Merge pull request #189 from naturallaw777/copilot/fix-headscale-database-deprecation
Fix deprecated Headscale database settings for nixos-unstable
2026-04-11 19:54:36 -05:00
copilot-swe-agent[bot]andnaturallaw777 b767b6d53b fix: replace deprecated db_type/db_path with database block in headscale settings
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/690ff811-901e-4539-b11b-998bc120186f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 00:49:03 +00:00
copilot-swe-agent[bot] 35e56afa0f Initial plan 2026-04-12 00:48:14 +00:00
Sovran_Systems aae1aa8779 Merge pull request #188 from naturallaw777/copilot/cleanup-remove-reverse-ssh-tunnel-code
Remove reverse SSH tunnel, fix enrollToken footgun, fix RDP/deploy-key docs
2026-04-11 19:21:04 -05:00
copilot-swe-agent[bot]andnaturallaw777 3ca15d0da4 Cleanup: Remove reverse SSH tunnel code, fix documentation accuracy
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3941ead1-cb20-4686-92bb-46e447791ae3

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-12 00:19:25 +00:00
copilot-swe-agent[bot] af14622e45 Initial plan 2026-04-12 00:14:43 +00:00
Sovran_Systems 8f8cc15ff4 Merge pull request #187 from naturallaw777/copilot/build-remote-deployment-system
Add Headscale-based remote deployment system with auto-provisioning ISO support
2026-04-11 18:37:58 -05:00
copilot-swe-agent[bot]andnaturallaw777 8f97aa416f Build remote deployment system using Headscale (self-hosted Tailscale)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7fa16927-250f-4af4-bb11-e22ef7b2c997

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-11 23:33:35 +00:00
copilot-swe-agent[bot] 9ec8618f7d Initial plan 2026-04-11 23:27:26 +00:00
Sovran_Systems 163afcc7e3 Merge pull request #186 from naturallaw777/copilot/add-remote-deployment-mode
feat: Remote deployment mode for headless install and post-install access
2026-04-11 17:31:14 -05:00
copilot-swe-agent[bot]andnaturallaw777 6fc66ba13f feat: add remote deployment mode (remote-deploy.nix, headless installer, ISO SSH/mDNS)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8e2ed0be-2db9-4437-81d7-c6efec45d6db

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-11 22:27:55 +00:00
copilot-swe-agent[bot] 357624193b Initial plan 2026-04-11 22:23:50 +00:00
Sovran_Systems 5a3432a93b Merge pull request #185 from naturallaw777/copilot/add-svg-wallpapers
Replace fetchurl wallpaper with in-repo SVGs + one-shot resolution-aware init
2026-04-11 16:57:20 -05:00
copilot-swe-agent[bot]andnaturallaw777 ccdde31654 Add SVG wallpapers and rewrite wallpaper system with resolution detection
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/2649badc-c159-40bd-b569-5be0feb18f74

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-11 21:40:42 +00:00
copilot-swe-agent[bot] 9c8f359c0d Initial plan 2026-04-11 21:37:04 +00:00
Sovran_Systems e9b94123b5 Merge pull request #184 from naturallaw777/copilot/add-legacy-cleanup-module
Add legacy Sovran_Systems cleanup on nixos-rebuild switch
2026-04-11 16:15:04 -05:00
copilot-swe-agent[bot]andnaturallaw777 6aa7a5a40b Add legacy-cleanup.nix to remove deprecated Sovran_Systems artifacts
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0c97ec90-556f-4bc9-86fe-c54022414704

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-11 21:14:19 +00:00
copilot-swe-agent[bot] 1a742578d1 Initial plan 2026-04-11 21:12:49 +00:00
Sovran_Systems 0af6a7cf66 Updated landing page to highlight ways to use Sovran_SystemsOS and added donation information. 2026-04-09 17:58:41 -05:00
Sovran_Systems bd9f889982 Merge pull request #183 from naturallaw777/copilot/add-glass-effect-to-ui
[WIP] Add glass effect to hub and onboarding UI surfaces
2026-04-09 17:38:32 -05:00
copilot-swe-agent[bot]andnaturallaw777 1242f0bc0b Add glass/frosted-glass effect to hub and onboarding UI surfaces
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/47ed7298-e001-4ae0-9d35-7dd1e869d836

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 22:38:12 +00:00
copilot-swe-agent[bot] 1bda7e9920 Initial plan 2026-04-09 22:33:20 +00:00
Sovran_Systems 03b711247e Merge pull request #182 from naturallaw777/copilot/align-sovran-hub-theme
Align hub visual theme with website: Inter font stack and softer green backgrounds
2026-04-09 17:26:48 -05:00
copilot-swe-agent[bot]andnaturallaw777 86672c3c28 Align hub CSS with website: Inter font stack and softer green backgrounds
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4472f4ff-0bf5-4150-997b-adb0a9b54898

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 22:25:53 +00:00
copilot-swe-agent[bot] 47049a94bd Initial plan 2026-04-09 22:24:36 +00:00
Sovran_Systems 7bda7abeb8 Merge pull request #181 from naturallaw777/copilot/soften-green-colors-hub-ui
[WIP] Update green colors for a calmer UI experience
2026-04-09 17:15:50 -05:00
copilot-swe-agent[bot]andnaturallaw777 db093a04ad style: soften green accent colors across hub CSS files
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8c50fde4-c2f8-49f8-953b-1a9e066041e5

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 22:15:27 +00:00
copilot-swe-agent[bot] df353d1318 Initial plan 2026-04-09 22:11:49 +00:00
Sovran_Systems 78252b9d07 Merge pull request #180 from naturallaw777/copilot/update-sovran-hub-theme
Hub theme v8: shift surfaces to near-black, keep green for accents only
2026-04-09 14:52:22 -05:00
copilot-swe-agent[bot]andnaturallaw777 db3ff345cf feat: rebalance Hub theme to near-black with green accents (v8)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7d5cc1b4-de38-4abc-be28-fc279e97d7b1

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 19:50:39 +00:00
copilot-swe-agent[bot] d398faa0ed Initial plan 2026-04-09 19:45:43 +00:00
Sovran_Systems c3558b4505 Merge pull request #179 from naturallaw777/copilot/update-sovran-hub-visual-theme
[WIP] Update visual theme of Sovran Hub to align with new app icon
2026-04-09 14:37:39 -05:00
copilot-swe-agent[bot]andnaturallaw777 7f135da474 Re-theme Sovran Hub UI to deep green palette matching new icon
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7aff0b39-5cd6-4008-b1d0-1519a7c0793d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 19:36:10 +00:00
copilot-swe-agent[bot] 7fdee314c8 Initial plan 2026-04-09 19:30:28 +00:00
Sovran_Systems cfd9d4c284 Merge pull request #178 from naturallaw777/copilot/update-sovran-hub-branding-again
[WIP] Update Sovran Hub branding with new icon and styling
2026-04-09 14:24:33 -05:00
copilot-swe-agent[bot]andnaturallaw777 1efd5c2086 Update Sovran Hub branding to finalized Rounded + Gradient Plus Arc + Core icon
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/309d3e00-70ba-4927-88c7-0a72cc5ed660

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 19:24:16 +00:00
copilot-swe-agent[bot] bd5ae05e20 Initial plan 2026-04-09 19:22:49 +00:00
Sovran_Systems bcdef677b7 Merge pull request #177 from naturallaw777/copilot/update-sovran-hub-branding
[WIP] Update Sovran Hub branding to new Arc + Core icon
2026-04-09 14:12:03 -05:00
copilot-swe-agent[bot]andnaturallaw777 785e5539b3 Replace sovran-hub-icon.svg with Arc+Core design and update header logo reference
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/9cda1def-017d-452a-8df6-43a6adbf021c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 19:11:09 +00:00
copilot-swe-agent[bot] 21ea5e5303 Initial plan 2026-04-09 19:09:56 +00:00
Sovran_Systems 04a675cb84 Merge pull request #176 from naturallaw777/copilot/add-scoped-sudo-rules
[WIP] Add scoped sudo rules for sovran-support user
2026-04-09 13:46:34 -05:00
copilot-swe-agent[bot]andnaturallaw777 b331c49b61 Add scoped sudo rules to tech-support.nix for sovran-support user
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e108b70d-de49-4d19-87a7-f093df3b05d3

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 18:46:13 +00:00
copilot-swe-agent[bot] 21723a6860 Initial plan 2026-04-09 18:44:42 +00:00
naturallaw777 3d646fec0a key 2026-04-09 12:59:11 -05:00
Sovran_Systems 4f997b1c4a Merge pull request #174 from naturallaw777/copilot/fix-security-reset-user-experience
[WIP] Improve user feedback during security reset process
2026-04-09 12:30:51 -05:00
copilot-swe-agent[bot]andnaturallaw777 0fb532d46f Fix security reset UX: full-screen overlay, CSS class bug, and auto-reconnect
- Add security-reset-overlay HTML element to index.html that shows immediately
  when the user confirms "Erase & Reset", before the synchronous API call runs
- Add .security-reset-overlay CSS to security.css (reuses reboot-card styles,
  adds fade-in animation, z-index 1000 to sit above all other content)
- Fix reboot overlay class bug: classList.add("open") → classList.add("visible")
  so the overlay actually renders per the .reboot-overlay.visible CSS rule
- Show overlay step text "Erasing data and resetting credentials…" during wipe,
  update to "Reset complete. Rebooting now…" when API returns
- Call waitForServerReboot() (globally defined in update.js) after reset so the
  page auto-reloads when the system comes back online
- Hide the security-reset-overlay and re-enable the button on error

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/185d0b41-d54d-4ea2-93d6-bfb7c15b8aed

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 17:29:46 +00:00
copilot-swe-agent[bot] 5bb2c67b1b Initial plan 2026-04-09 17:26:39 +00:00
Sovran_Systems e8b1195c66 Merge pull request #173 from naturallaw777/copilot/fix-onboarding-timezone-locale
[WIP] Fix onboarding timezone and locale step for NixOS
2026-04-09 12:22:35 -05:00
copilot-swe-agent[bot]andnaturallaw777 a415431d93 Fix NixOS timezone/locale: use declarative custom.nix config + nixos-rebuild instead of timedatectl/localectl
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/334ffeb7-2160-4938-bc4e-fb7693a1154f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 17:22:06 +00:00
copilot-swe-agent[bot] 885fc7f099 Initial plan 2026-04-09 17:17:07 +00:00
Sovran_Systems 95440529ef Merge pull request #172 from naturallaw777/copilot/fix-timezone-locale-endpoints
[WIP] Fix timezone and locale endpoints to remove unnecessary sudo
2026-04-09 06:00:45 -05:00
copilot-swe-agent[bot]andnaturallaw777 8ea133a2a1 Remove sudo from timedatectl and localectl subprocess calls
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/925a72d0-2cdd-4940-b338-07772f3f8a68

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 11:00:18 +00:00
copilot-swe-agent[bot] 701c400916 Initial plan 2026-04-09 10:49:18 +00:00
Sovran_Systems b4ca6fe7fe Merge pull request #171 from naturallaw777/copilot/fix-verify-integrity-flag
[WIP] Fix integrity verification by removing no-build-output flag
2026-04-08 23:05:55 -05:00
copilot-swe-agent[bot]andnaturallaw777 1843c98e98 Remove invalid --no-build-output flag from nix build command
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/2989f7c9-f544-4a2e-b073-7d9518b41e60

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 04:05:27 +00:00
copilot-swe-agent[bot] 2cfb23e670 Initial plan 2026-04-09 04:04:38 +00:00
Sovran_Systems 54fb17c9c8 Merge pull request #170 from naturallaw777/copilot/fix-nixos-rebuild-usage-error
[WIP] Fix nixos-rebuild command by replacing it with nix build
2026-04-08 22:53:38 -05:00
copilot-swe-agent[bot]andnaturallaw777 61cee57d4e Fix verify-integrity: replace nixos-rebuild with nix build (nixos-rebuild does not support -o)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3c0ca796-fe76-4985-9956-5915b2993b08

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 03:53:21 +00:00
copilot-swe-agent[bot] dca53835f4 Initial plan 2026-04-09 03:51:28 +00:00
Sovran_Systems 385a3cb215 Merge pull request #169 from naturallaw777/copilot/fix-nixos-rebuild-result-symlink
[WIP] Fix missing result symlink in verify integrity check
2026-04-08 22:44:25 -05:00
copilot-swe-agent[bot]andnaturallaw777 2bf9c6657b Fix nixos-rebuild result symlink by passing explicit -o flag
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/52242cb6-8038-446d-bafb-9fe6666b31b9

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 03:43:45 +00:00
copilot-swe-agent[bot] d77dde4020 Initial plan 2026-04-09 03:43:01 +00:00
Sovran_Systems 967df9664d Merge pull request #168 from naturallaw777/copilot/fix-nixos-rebuild-command
Fix "Running System Match" always failing due to unsupported --print-out-paths flag
2026-04-08 22:24:39 -05:00
copilot-swe-agent[bot]andnaturallaw777 489e326ccc Fix Verify System Integrity: use temp dir + result symlink instead of --print-out-paths
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b90b9352-56a0-4987-822b-ea4b9d4fdf92

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 03:23:42 +00:00
copilot-swe-agent[bot] 5cd9b6bb3d Initial plan 2026-04-09 03:21:30 +00:00
Sovran_Systems fa71a7da97 Merge pull request #167 from naturallaw777/copilot/fix-system-integrity-check
Fix: Running System Match always fails due to cwd-relative result symlink
2026-04-08 22:03:36 -05:00
copilot-swe-agent[bot]andnaturallaw777 5bd5c03e2f Fix: Use --print-out-paths instead of ./result symlink in verify-integrity endpoint
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b365659c-e6c9-45bf-9b12-b89addfbbbdd

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 02:59:32 +00:00
copilot-swe-agent[bot] bf9e82cd79 Initial plan 2026-04-09 02:58:35 +00:00
Sovran_Systems a18ef1447a Merge pull request #166 from naturallaw777/copilot/fix-subprocess-command-paths
[WIP] Fix subprocess command paths in verify-integrity endpoint
2026-04-08 21:08:58 -05:00
copilot-swe-agent[bot]andnaturallaw777 ecfd2e9f51 fix: use absolute paths for nix and nixos-rebuild in verify-integrity endpoint
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7de38316-a649-4395-bfb4-c12a07741078

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 02:08:29 +00:00
copilot-swe-agent[bot] f8bdc1cb15 Initial plan 2026-04-09 02:06:47 +00:00
Sovran_Systems 431773f6d5 Merge pull request #165 from naturallaw777/copilot/implement-random-password-generation
[WIP] Implement random password generation for free user
2026-04-08 20:59:14 -05:00
copilot-swe-agent[bot]andnaturallaw777 2fae4ccc79 Implement security overhaul: remove seal/legacy system, add Security modal and random passwords
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/6e7593c4-f741-4ddc-9bce-8c558a4af014

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 01:58:42 +00:00
copilot-swe-agent[bot] 477d265de8 Initial plan 2026-04-09 01:49:35 +00:00
Sovran_Systems 6b14f811d6 Merge pull request #162 from naturallaw777/copilot/update-dconf-settings-app-folders
[WIP] Add GNOME app-folder dconf settings and remove updater from favorites
2026-04-08 19:18:51 -05:00
copilot-swe-agent[bot]andnaturallaw777 8e8082d2ae Add GNOME app-folder dconf settings and remove Sovran_SystemsOS_Updater from favorites
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ec32495a-2919-4b25-bd7c-459f9bdc3ba9

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 00:18:08 +00:00
copilot-swe-agent[bot] b073564cce Initial plan 2026-04-09 00:16:48 +00:00
Sovran_Systems 586eba824a Merge pull request #161 from naturallaw777/copilot/add-timezone-locale-onboarding-step
[WIP] Add timezone and locale selection to onboarding wizard
2026-04-08 19:14:08 -05:00
copilot-swe-agent[bot]andnaturallaw777 9e081bec05 Add timezone/locale onboarding step (new Step 2), renumber existing steps 2-5 to 3-6
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/47f2ee8f-bd6c-4151-bd2d-3e9283cb02c0

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 00:13:44 +00:00
Sovran_Systems 4eac3c3498 Merge pull request #160 from naturallaw777/copilot/fix-brave-keyring-prompt
fix: suppress Brave keyring prompt on Hub launch and GDM auto-login
2026-04-08 19:08:46 -05:00
copilot-swe-agent[bot] d9fba84243 Initial plan 2026-04-09 00:08:33 +00:00
copilot-swe-agent[bot]andnaturallaw777 f5d44e5c4b fix: suppress Brave keyring prompt via --password-store=basic and PAM GNOME Keyring auto-unlock
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/731f35e7-10c0-4641-8ec4-bd02f0dc98b4

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-09 00:03:09 +00:00
copilot-swe-agent[bot] b17ccce53a Initial plan 2026-04-09 00:02:10 +00:00
Sovran_Systems a9e4bc2656 Merge pull request #159 from naturallaw777/copilot/fix-gnome-remote-desktop-service-again
[WIP] Fix gnome-remote-desktop service block in rdp.nix
2026-04-08 17:11:41 -05:00
copilot-swe-agent[bot]andnaturallaw777 c4238f2590 fix(rdp): add wantedBy block and remove || true from grdctl enable
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/47126fb3-6167-424d-9599-cd75e6447717

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 22:11:23 +00:00
copilot-swe-agent[bot] 059eaefa0c Initial plan 2026-04-08 22:10:31 +00:00
Sovran_Systems cfa103f7b5 Merge pull request #158 from naturallaw777/copilot/revert-rdp-nix-to-914ad0e
revert: restore rdp.nix to pre-PR#147 working state
2026-04-08 16:30:59 -05:00
copilot-swe-agent[bot]andnaturallaw777 1bbf6094b3 revert: restore rdp.nix to pre-PR#147 working state with || true fix
- Remove systemd.services."gnome-remote-desktop".wantedBy = lib.mkForce []
  (was preventing the service from ever auto-starting)
- Remove systemctl start gnome-remote-desktop.service || true
  (was creating a systemd deadlock with before = ["gnome-remote-desktop.service"])
- Remove pkgs.systemd from setup script path
- Remove grdctl --system rdp disable-view-only || true
- Add || true to grdctl --system rdp enable (suppress harmless EROFS warning)

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/75c67389-947f-437d-95ba-427504935156

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 21:29:48 +00:00
copilot-swe-agent[bot] 73401353e4 Initial plan 2026-04-08 21:28:22 +00:00
Sovran_Systems 64c32dfb4f Merge pull request #157 from naturallaw777/copilot/fix-gnome-remote-desktop-issues
Fix RDP: revert to system-level grdctl with declarative service masking + explicit start
2026-04-08 16:04:39 -05:00
copilot-swe-agent[bot]andnaturallaw777 ebd41797f7 Fix RDP: revert to system-level approach with declarative service masking and explicit start
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/cedebc7f-683e-469d-bd91-a0b87495d055

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 21:02:49 +00:00
copilot-swe-agent[bot] 4c5e639cfa Initial plan 2026-04-08 21:00:10 +00:00
Sovran_Systems 4aaea32c39 Merge pull request #156 from naturallaw777/copilot/fix-gnome-remote-desktop-service
[WIP] Fix gnome-remote-desktop service handling on NixOS
2026-04-08 15:44:05 -05:00
copilot-swe-agent[bot]andnaturallaw777 e3916d48dd Fix RDP on NixOS: declarative service masking instead of grdctl --system rdp disable
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0701acec-7c63-419b-be17-57a912daedaf

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 20:43:35 +00:00
copilot-swe-agent[bot] f7af81eba4 Initial plan 2026-04-08 20:41:59 +00:00
Sovran_Systems 0bafcee8af Merge pull request #155 from naturallaw777/copilot/update-gnome-remote-desktop-config
Fix RDP "Session Already Running" by switching to user-session screen sharing
2026-04-08 13:19:23 -05:00
copilot-swe-agent[bot]andnaturallaw777 b77fb2ed70 Fix RDP Session Already Running by using user-session screen sharing
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ab7b63b5-2a0a-4933-9fb2-36ac793e9f1a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 18:18:04 +00:00
copilot-swe-agent[bot] 48926d1937 Initial plan 2026-04-08 18:15:54 +00:00
Sovran_Systems d3458da56b Merge pull request #154 from naturallaw777/copilot/fix-rdp-session-running-conflict
[WIP] Fix RDP 'Session Already Running' conflict using system-level screen sharing
2026-04-08 12:51:00 -05:00
copilot-swe-agent[bot]andnaturallaw777 45ee8da166 fix(rdp): remove session-level setup to fix Session Already Running conflict
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/9ba5618b-db30-41c3-8031-68b9a9e5448c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 17:50:40 +00:00
copilot-swe-agent[bot] f15e5616b7 Initial plan 2026-04-08 17:48:16 +00:00
Sovran_Systems 65e4086682 Merge pull request #153 from naturallaw777/copilot/fix-desktop-file-whitespace-bug
[WIP] Fix whitespace issue in desktop file causing unresponsive RDP session
2026-04-08 12:39:52 -05:00
copilot-swe-agent[bot]andnaturallaw777 0f1ebe339e Use warning message for system-level disable-view-only for consistency
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7683cf3e-15ca-4f1b-a485-5522fa4d6cd6

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 17:38:54 +00:00
copilot-swe-agent[bot]andnaturallaw777 4bbf7a3a67 Fix RDP frozen/unclickable screen: desktop whitespace, disable-view-only session+system
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7683cf3e-15ca-4f1b-a485-5522fa4d6cd6

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 17:38:24 +00:00
copilot-swe-agent[bot] 6012373501 Initial plan 2026-04-08 17:37:18 +00:00
naturallaw777 71a08ae4d3 updated locals 2026-04-08 12:18:52 -05:00
naturallaw777 41f17c205c updated for user timezonez and languges and removed unsued code 2026-04-08 12:12:47 -05:00
Sovran_Systems 72bec5b77f Merge pull request #152 from naturallaw777/copilot/fix-branding-in-installer
[WIP] Fix branding in iso/installer.py
2026-04-08 11:59:54 -05:00
copilot-swe-agent[bot]andnaturallaw777 bcd4a49942 Fix installer branding and replace complete screen with auto-reboot
- Replace all "Sovran SystemsOS" (with space) with "Sovran_SystemsOS"
  - Line 241: landing page title
  - Line 262: internet notice text
  - Line 328: welcome/role-selection hero title (was "Sovran Systems")
  - Line 910: install progress title
- Replace push_complete method: remove credentials screen (username,
  password, first-boot note, reboot button) and replace with a simple
  status page that says "Rebooting…" then auto-reboots after 3 seconds
  via GLib.timeout_add_seconds

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/2a39f2d5-6aef-42cf-a94a-e1db5c6a601a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 16:59:14 +00:00
copilot-swe-agent[bot] b47f0986d5 Initial plan 2026-04-08 16:57:47 +00:00
Sovran_Systems 4c67e3984f Merge pull request #151 from naturallaw777/copilot/remove-file-fixes-and-new-services
Remove legacy file_fixes_and_new_services system
2026-04-08 11:52:06 -05:00
copilot-swe-agent[bot]andnaturallaw777 08e8d6bf69 Remove legacy file_fixes_and_new_services system
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ae9f019a-3743-48b8-b251-feb17b1adbd4

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 16:50:58 +00:00
copilot-swe-agent[bot] 3365ab5639 Initial plan 2026-04-08 16:49:49 +00:00
Sovran_Systems 973d2cec92 Merge pull request #150 from naturallaw777/copilot/fix-domains-dir-ownership
Fix /var/lib/domains caddy ownership and WordPress ADMIN_EMAIL for bare domains
2026-04-08 11:11:55 -05:00
copilot-swe-agent[bot]andnaturallaw777 fb4c268b8e Fix /var/lib/domains ownership and WordPress ADMIN_EMAIL generation
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/755b414e-9b63-448b-a57c-41d0ca45b5eb

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 16:09:10 +00:00
copilot-swe-agent[bot] 3673ccf39b Initial plan 2026-04-08 16:00:59 +00:00
Sovran_Systems 21bf0ff03f Merge pull request #149 from naturallaw777/copilot/add-cache-busting-headers
[WIP] Add cache-busting and data-clearing HTTP headers
2026-04-08 09:52:40 -05:00
copilot-swe-agent[bot]andnaturallaw777 a2d2dac2b9 Add cache-busting and Clear-Site-Data headers for sovransystemsos.local browser access
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0e1cbb58-3e7f-412b-be95-8907caaab6f3

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 14:52:13 +00:00
Sovran_Systems 46f8eb5308 Merge pull request #148 from naturallaw777/copilot/create-brave-launcher-wrapper
Use ephemeral Brave profile for Hub desktop app to prevent data persistence
2026-04-08 09:48:08 -05:00
Sovran_Systems 907542b651 Merge pull request #147 from naturallaw777/copilot/fix-gnome-remote-desktop-capture
[WIP] Fix RDP frozen screen issue in GNOME Remote Desktop
2026-04-08 09:47:40 -05:00
copilot-swe-agent[bot]andnaturallaw777 5ab4021100 Fix RDP frozen screen: add session-level GNOME Remote Desktop configuration
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e099592f-2d1e-4894-a91c-b4ef9b4a5244

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 14:46:52 +00:00
copilot-swe-agent[bot]andnaturallaw777 73cd5faab0 Add Brave wrapper script for isolated, ephemeral Hub sessions
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ebc41311-f7da-40dd-b85b-87db3176a69a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 14:45:00 +00:00
copilot-swe-agent[bot] 0ac7ac4343 Initial plan 2026-04-08 14:43:14 +00:00
copilot-swe-agent[bot] 92734dd251 Initial plan 2026-04-08 14:36:01 +00:00
copilot-swe-agent[bot] 08c8b7d09c Initial plan 2026-04-08 14:33:44 +00:00
Sovran_Systems 914ad0edf8 Merge pull request #146 from naturallaw777/copilot/fix-dock-icon-size-issue
Fix dock icon whitespace and RDP freeze from brave --app= on Wayland
2026-04-08 08:46:10 -05:00
copilot-swe-agent[bot]andnaturallaw777 cfd416002d Fix dock icon size and RDP frozen screen regressions from PR #144
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/25eb7e56-2284-4030-a9dd-75f2f9a2917c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 13:43:57 +00:00
copilot-swe-agent[bot] 349de76b6b Initial plan 2026-04-08 13:40:51 +00:00
Sovran_Systems 9345e18259 Merge pull request #145 from naturallaw777/copilot/remove-sparrow-bisq-launch-feature
[WIP] Remove Sparrow and Bisq desktop launch feature from Hub tiles
2026-04-08 08:25:40 -05:00
copilot-swe-agent[bot]andnaturallaw777 360654fe58 Remove Sparrow and Bisq desktop launch feature from Hub tiles
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ffb330a3-9863-4f00-8476-67331a02a0b9

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 13:22:52 +00:00
Sovran_Systems 253ce8d16c Merge pull request #144 from naturallaw777/copilot/fix-dock-icon-size-issues
[WIP] Fix dock icon size issues for Sovran Hub
2026-04-08 08:17:43 -05:00
copilot-swe-agent[bot]andnaturallaw777 78b08758f1 fix: brave --app mode, StartupWMClass, and icon PNGs for Sovran Hub dock
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/6f932322-cc0e-4fff-aca1-b853770c0817

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 13:17:09 +00:00
copilot-swe-agent[bot] 9470ce74c1 Initial plan 2026-04-08 13:13:41 +00:00
copilot-swe-agent[bot] cb0bcdb94c Initial plan 2026-04-08 13:09:47 +00:00
Sovran_Systems 5580e8a8b7 Merge pull request #141 from naturallaw777/copilot/fix-wayland-launch-issue
[WIP] Fix application launch issue in Wayland GNOME
2026-04-08 07:43:34 -05:00
Sovran_Systems 212f2f86fc Merge pull request #142 from naturallaw777/copilot/remove-tooltip-from-disabled-tiles
Remove internal `custom.nix` tooltip from disabled Hub tiles
2026-04-08 07:43:20 -05:00
Sovran_Systems e6fefb2510 Merge pull request #143 from naturallaw777/copilot/update-confirmation-messages
[WIP] Update confirmation messages for Bitcoin feature toggles
2026-04-08 07:43:06 -05:00
copilot-swe-agent[bot]andnaturallaw777 5b10ab4823 Fix api_desktop_launch for Wayland-only GNOME: run as free user with correct session env vars
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/237927a7-a65c-4c67-b1e2-e5bfd1b3bef7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 12:42:39 +00:00
copilot-swe-agent[bot]andnaturallaw777 f021f56318 Update Bitcoin feature toggle confirmation messages to mention timechain preservation
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c318d542-bd1b-4fd8-a100-7ec8e5041623

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 12:41:45 +00:00
copilot-swe-agent[bot]andnaturallaw777 d5521ea681 Remove custom.nix tooltip from disabled tiles
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8bda7f98-8019-4dc6-8705-94cc21b53b23

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 12:40:54 +00:00
copilot-swe-agent[bot] 7781d6c849 Initial plan 2026-04-08 12:40:12 +00:00
copilot-swe-agent[bot] 4227024fba Initial plan 2026-04-08 12:39:45 +00:00
copilot-swe-agent[bot] 7243f4444f Initial plan 2026-04-08 12:34:30 +00:00
Sovran_Systems c8d24998e8 Merge pull request #140 from naturallaw777/copilot/add-icon-pngs-for-sovran-hub
[WIP] Add PNG icons for Sovran Hub desktop application
2026-04-08 07:26:30 -05:00
copilot-swe-agent[bot]andnaturallaw777 f0b7152c41 fix: rasterize sovran-hub icon to PNG at standard hicolor sizes
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e414bb3e-f166-48b2-bac9-ad36c24aceb6

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 12:25:36 +00:00
copilot-swe-agent[bot] 8d6a20d375 Initial plan 2026-04-08 12:22:13 +00:00
Sovran_Systems 8413093d43 Merge pull request #139 from naturallaw777/copilot/fix-flake-lock-issue
installer: pre-resolve flake lock to staging-dev instead of deleting it
2026-04-07 21:49:58 -05:00
copilot-swe-agent[bot]andnaturallaw777 1a8a1736bf fix: pre-resolve flake lock to staging-dev during installation
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/14550e27-a253-453b-b454-097575e924fa

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 02:48:59 +00:00
copilot-swe-agent[bot] 51c7d172b3 Initial plan 2026-04-08 02:46:49 +00:00
Sovran_Systems 6999ae5680 Merge pull request #138 from naturallaw777/copilot/fix-onboarding-wizard-issues
onboarding: remove scroll boxes, fix footer spacing, add per-field domain saves
2026-04-07 19:55:56 -05:00
copilot-swe-agent[bot]andnaturallaw777 0c3f74e7de Fix onboarding wizard: remove scroll boxes, fix footer spacing, add per-field save buttons
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0b500e06-d8c5-4745-9768-29523ffc99c6

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 00:55:08 +00:00
copilot-swe-agent[bot] d2703ff84b Initial plan 2026-04-08 00:51:40 +00:00
Sovran_Systems 1a9e0825fc Merge pull request #137 from naturallaw777/copilot/fix-onboarding-visual-consistency
Fix onboarding wizard: consistent card styling, footer spacing, and password description
2026-04-07 19:43:02 -05:00
copilot-swe-agent[bot]andnaturallaw777 284a861927 Fix onboarding wizard: consistent card styling, footer spacing, and password description
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ce004fc7-c96f-4765-bc21-87ce579352d0

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 00:39:45 +00:00
Sovran_Systems 02b4e6b5b4 Merge pull request #136 from naturallaw777/copilot/fix-domain-configuration-in-modal
Fix: Replace dead "Feature Manager" sidebar references with inline Configure Domain button
2026-04-07 19:39:43 -05:00
copilot-swe-agent[bot] 60084c292e Initial plan 2026-04-08 00:38:42 +00:00
copilot-swe-agent[bot]andnaturallaw777 fa22a080b9 fix: replace broken Feature Manager references with Configure Domain button
- server.py: add domain_name to /api/service-detail response
- service-detail.js: replace both Feature Manager references with Configure Domain / Reconfigure Domain buttons with click handlers
- tiles.css: add .svc-detail-domain-btn class for button spacing

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ae38c98e-28bb-4d1e-8dae-78ebde64ad44

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 00:37:40 +00:00
copilot-swe-agent[bot] 70f0af98f6 Initial plan 2026-04-08 00:33:37 +00:00
Sovran_Systems cd4df316ae Merge pull request #135 from naturallaw777/copilot/fix-bitcoind-i-o-error
Fix bitcoind/electrs I/O crash when second drive mounts after service start
2026-04-07 19:24:22 -05:00
copilot-swe-agent[bot]andnaturallaw777 ff55dce746 Add mount dependency for bitcoind and electrs systemd services
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/1def4c7b-d90d-4b0c-87a7-87dc729661b1

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-08 00:23:35 +00:00
copilot-swe-agent[bot] 5a86c03f74 Initial plan 2026-04-08 00:22:26 +00:00
naturallaw777 1c2df46ac4 updated installer.py 2026-04-07 17:59:53 -05:00
naturallaw777 8839620e63 updated caddy.nix 2026-04-07 17:36:26 -05:00
naturallaw777 c03126e8f8 .iso update 2026-04-07 17:02:43 -05:00
Sovran_Systems 10ef36859d Merge pull request #132 from naturallaw777/copilot/fix-ownership-permissions
Replace tmpfiles rules with systemd oneshot service for recursive ownership fix on second drive
2026-04-07 16:41:54 -05:00
Sovran_Systems 4acb75f2bd Merge pull request #133 from naturallaw777/copilot/update-deployed-flake-url
Point installer DEPLOYED_FLAKE at staging-dev branch
2026-04-07 16:41:20 -05:00
copilot-swe-agent[bot]andnaturallaw777 77e2fb2537 Fix installer DEPLOYED_FLAKE to point to staging-dev branch
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/43e96fac-1140-42e5-9981-00069570967c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 21:40:26 +00:00
copilot-swe-agent[bot] c7bbb97a68 Initial plan 2026-04-07 21:39:42 +00:00
copilot-swe-agent[bot]andnaturallaw777 6d1c360c02 Replace tmpfiles rules with systemd oneshot service for recursive chown on second drive
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/96b8f8fe-5a1d-42e5-8b2d-5dd5aee96044

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 21:29:33 +00:00
copilot-swe-agent[bot] 3b73eb3bd1 Initial plan 2026-04-07 21:28:36 +00:00
Sovran_Systems 6ffcc056ad Merge pull request #131 from naturallaw777/copilot/fix-sovran-legacy-security-check
Replace Python `crypt` module with `openssl passwd` (Python 3.13 compatibility)
2026-04-07 16:17:02 -05:00
copilot-swe-agent[bot]andnaturallaw777 742f680d0d fix: replace Python crypt module with openssl passwd for Python 3.13 compatibility
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/9544e3d5-f7f8-4299-9198-3b5f1f835d14

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 21:11:13 +00:00
copilot-swe-agent[bot] c872f1c6b0 Initial plan 2026-04-07 21:04:58 +00:00
Sovran_Systems bc5a40f143 Merge pull request #130 from naturallaw777/copilot/add-sovran-auto-seal-service
Add sovran-auto-seal: automatic first-boot seal with live-system safety guards
2026-04-07 15:48:25 -05:00
copilot-swe-agent[bot]andnaturallaw777 c2bd3f6273 Add sovran-auto-seal systemd service to factory-seal.nix
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/869df8d4-3811-4a1a-b026-e978d3a81589

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 20:43:15 +00:00
copilot-swe-agent[bot] 343dee3576 Initial plan 2026-04-07 20:40:53 +00:00
Sovran_Systems ebcafd3c6d Merge pull request #129 from naturallaw777/copilot/add-tmpfiles-rules-for-bitcoin-electrs
[WIP] Add tmpfiles rules for Bitcoin and Electrs data directories
2026-04-07 15:21:26 -05:00
copilot-swe-agent[bot]andnaturallaw777 5231b5ca4b Add systemd.tmpfiles.rules for Bitcoin/Electrs directory permissions
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ea46340b-7cf5-404b-9cef-b5ed1fcb2ecb

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 20:21:07 +00:00
Sovran_Systems 1195456bee Merge pull request #128 from naturallaw777/copilot/fix-flake-nix-references
[WIP] Fix flake.nix references after nixos-install cleanup
2026-04-07 15:21:02 -05:00
copilot-swe-agent[bot]andnaturallaw777 48de6b9821 fix(installer): improve error handling for deployed flake.nix write
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b7dfaecc-2b2e-4f5f-bb9a-f97ced90e76e

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 20:20:36 +00:00
copilot-swe-agent[bot] cd4a17fe31 Initial plan 2026-04-07 20:20:01 +00:00
copilot-swe-agent[bot]andnaturallaw777 d3a5b3e6ef fix(installer): write deployed flake.nix and remove flake.lock after install cleanup
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b7dfaecc-2b2e-4f5f-bb9a-f97ced90e76e

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 20:18:36 +00:00
copilot-swe-agent[bot] 3c4c6c7389 Initial plan 2026-04-07 20:16:57 +00:00
Sovran_Systems 876f728aa2 Merge pull request #127 from naturallaw777/copilot/update-api-password-check
Use /etc/shadow as authoritative source for factory default password detection
2026-04-07 13:55:53 -05:00
copilot-swe-agent[bot]andnaturallaw777 950a6dabd8 Use /etc/shadow as source of truth for factory default password detection
- server.py: add _is_free_password_default() helper that reads /etc/shadow
  and hashes known defaults ("free", "gosovransystems") via crypt module;
  update api_password_is_default to use it instead of reading the secrets file
- factory-seal.nix: replace file-based free-password check with shadow-based
  cryptographic check using python3 + crypt module; add pkgs.python3 to path;
  pass values via env vars to avoid shell expansion of hash $ characters

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/31e6fc93-8b4b-47af-9c47-568da0905301

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 18:50:16 +00:00
copilot-swe-agent[bot] 1d9589a186 Initial plan 2026-04-07 18:46:24 +00:00
Sovran_Systems b13fa7dc05 Merge pull request #126 from naturallaw777/copilot/fix-security-warning-reappearance
Fix legacy security warning reappearing on every reboot after password change
2026-04-07 13:29:32 -05:00
copilot-swe-agent[bot]andnaturallaw777 069f6c3ec7 Avoid storing password in variable to prevent process listing exposure
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c18311e4-609d-4edf-a2a1-a018baede373

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 18:27:32 +00:00
copilot-swe-agent[bot]andnaturallaw777 5a27b79b51 Fix security warning reappearing after every reboot
Add two early-exit checks in sovran-legacy-security-check before the
legacy fallthrough block:
1. Exit if /var/lib/sovran/onboarding-complete exists (Hub onboarding done)
2. Exit if /var/lib/secrets/free-password exists and is not "free" (password changed)

This prevents the boot-time service from overwriting the security-status
file that /api/change-password clears after a successful password change.

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c18311e4-609d-4edf-a2a1-a018baede373

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 18:26:54 +00:00
copilot-swe-agent[bot] 72453c80bf Initial plan 2026-04-07 18:25:47 +00:00
naturallaw777 14800ffb1e update flake 2026-04-07 13:14:21 -05:00
naturallaw777 e2f36d01bc update flake 2026-04-07 13:13:06 -05:00
naturallaw777 55b231b456 update flake and installer 2026-04-07 13:11:39 -05:00
Sovran_Systems b4b2607df1 Merge pull request #125 from naturallaw777/copilot/update-security-check-for-unsealed-state
[WIP] Update sovran-legacy-security-check to warn on unsealed state
2026-04-07 12:50:45 -05:00
copilot-swe-agent[bot]andnaturallaw777 ac9ba4776c Detect and warn when machine was set up without factory seal
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/169de2bb-0655-4504-a270-8c0341c0d3dd

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 17:48:38 +00:00
copilot-swe-agent[bot] 85aca0d022 Initial plan 2026-04-07 17:45:41 +00:00
Sovran_Systems 80c74b2d1a Merge pull request #124 from naturallaw777/copilot/add-password-creation-step-onboarding
Add password creation step to first-boot onboarding wizard
2026-04-07 12:45:34 -05:00
copilot-swe-agent[bot]andnaturallaw777 d28f224ad5 feat: add password creation step to onboarding wizard (#2)
- Add GET /api/security/password-is-default endpoint in server.py
- Add Step 2 (Create Your Password) to onboarding wizard HTML
- Renumber old steps: Domains→3, Ports→4, Complete→5
- Add 5th step dot indicator
- Update onboarding.js: TOTAL_STEPS=5, ROLE_SKIP_STEPS=[3,4] for desktop/node
- Add loadStep2/saveStep2 for password step with smart default detection
- Rename old step functions to loadStep3/saveStep3/loadStep4
- Add password form CSS styles in onboarding.css

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/74a30916-fb2d-4f1d-9763-e380b1aa5540

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 17:36:59 +00:00
copilot-swe-agent[bot] f2a808ed13 Initial plan 2026-04-07 17:29:46 +00:00
Sovran_Systems 4ef420651d Merge pull request #122 from naturallaw777/copilot/fix-installer-create-password-step
Fix installer password step: replace chroot+sh with direct chpasswd --root
2026-04-07 12:17:24 -05:00
copilot-swe-agent[bot]andnaturallaw777 65ce66a541 Fix chpasswd: run directly from host with --root /mnt, no chroot needed
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3ff98bf4-8f62-4c81-90fd-36854e88266f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 17:14:32 +00:00
copilot-swe-agent[bot] deae53b721 Initial plan 2026-04-07 17:13:16 +00:00
Sovran_Systems f459e83861 Merge pull request #121 from naturallaw777/copilot/fix-change-password-form-issues
Fix System Passwords change-password form: chpasswd path on NixOS, show/hide toggle, UX clarity
2026-04-07 12:03:23 -05:00
copilot-swe-agent[bot]andnaturallaw777 badab99242 Fix chpasswd path on NixOS, add password toggle/hints/validation in change-password form
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/de03873d-5cdb-4929-bd4a-4d306916b525

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 17:01:54 +00:00
copilot-swe-agent[bot] 84124ba1b1 Initial plan 2026-04-07 16:57:23 +00:00
Sovran_Systems 2ad0d2072d Merge pull request #119 from naturallaw777/copilot/fix-change-passwords-button
[WIP] Fix non-functional change passwords button in Hub
2026-04-07 11:45:15 -05:00
copilot-swe-agent[bot]andnaturallaw777 ff1632dcda Fix Change Passwords button: add API endpoint, system password modal, fix security banner link
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/bf43bea9-9f93-4f7b-b6fd-c76714e7f25b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 16:44:57 +00:00
Sovran_Systems 531b8c1d09 Merge pull request #120 from naturallaw777/copilot/fix-installer-password-step
[WIP] Fix installer failure at 'Create Password' step
2026-04-07 11:44:49 -05:00
copilot-swe-agent[bot]andnaturallaw777 a8128cef8d Fix chpasswd: find binary in Nix store and pipe password inline
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/630a25f6-417a-47de-b163-b519252b403c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 16:43:50 +00:00
copilot-swe-agent[bot] 3baffb2a69 Initial plan 2026-04-07 16:42:51 +00:00
copilot-swe-agent[bot] 06bdf999a6 Initial plan 2026-04-07 16:41:02 +00:00
Sovran_Systems 76ff1f4d4f Merge pull request #118 from naturallaw777/copilot/fix-update-status-handling
[WIP] Fix update status handling for interrupted builds
2026-04-07 11:29:49 -05:00
copilot-swe-agent[bot]andnaturallaw777 2360b4147c fix: recover stale RUNNING status files on Hub server startup
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/22f9df39-fb39-4ffb-8c6b-c7323a894bee

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 16:29:08 +00:00
copilot-swe-agent[bot] 37874ff58e Initial plan 2026-04-07 16:26:26 +00:00
Sovran_Systems aef13155fc Merge pull request #117 from naturallaw777/copilot/remove-security-warning-modal
[WIP] Remove legacy password warning modal and add inline message
2026-04-07 11:17:56 -05:00
copilot-swe-agent[bot]andnaturallaw777 1d4f104524 Replace security warning modal with inline banner in Preferences section
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e7946288-08c7-4081-85dd-6780f1eba17a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 16:17:23 +00:00
copilot-swe-agent[bot] 11ec4b4816 Initial plan 2026-04-07 16:14:42 +00:00
Sovran_Systems 2bd899848d Merge pull request #115 from naturallaw777/copilot/add-password-warning-screen
[WIP] Add old password warning screen for legacy machines
2026-04-07 10:50:11 -05:00
Sovran_Systems 18a6e8d24c Merge pull request #116 from naturallaw777/copilot/fix-installer-password-error
Fix installer password step: replace bare chroot with nixos-enter
2026-04-07 10:49:31 -05:00
copilot-swe-agent[bot]andnaturallaw777 13c686a8a1 feat: add legacy security warning API and UI modal for pre-factory-seal machines
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/f7c8f11b-873b-403f-ac55-8b5b7cd9f1fb

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 15:49:25 +00:00
copilot-swe-agent[bot]andnaturallaw777 7a172c0306 Fix chpasswd not found by using nixos-enter instead of bare chroot
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/1bb103de-c4a5-4701-b1b8-6aad670b97c3

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 15:45:30 +00:00
copilot-swe-agent[bot] 7fc04fcf20 Initial plan 2026-04-07 15:44:31 +00:00
copilot-swe-agent[bot] a40ea61415 Initial plan 2026-04-07 15:43:22 +00:00
naturallaw777 eba517d34d update flake 2026-04-07 10:13:24 -05:00
Sovran_Systems 38257492bd Merge pull request #113 from naturallaw777/copilot/remove-pdf-references
Rename credentials-pdf.nix → credentials.nix and remove all pdf references
2026-04-07 10:06:28 -05:00
naturallaw777 93592c984d removed erroniousfile 2026-04-07 10:05:37 -05:00
copilot-swe-agent[bot]andnaturallaw777 7a08bc0b2b Remove all PDF references: rename credentials-pdf.nix and update references
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/150954c9-65a0-4d5b-b8e2-08f301f07511

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 15:04:33 +00:00
copilot-swe-agent[bot] 25e8cac613 Initial plan 2026-04-07 15:02:58 +00:00
Sovran_Systems 02eaea85d8 Merge pull request #112 from naturallaw777/copilot/add-zeus-connect-setup-service
[WIP] Add zeus-connect-setup service to wallet autoconnect module
2026-04-07 09:54:16 -05:00
copilot-swe-agent[bot]andnaturallaw777 6c433d642d Add zeus-connect-setup service and timer to wallet-autoconnect.nix
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/6b3d9c59-40e1-45c1-93f9-a5ba6547567b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 14:52:40 +00:00
copilot-swe-agent[bot] 7aed3e09e8 Initial plan 2026-04-07 14:51:00 +00:00
Sovran_Systems e0e6ab0de6 Merge pull request #111 from naturallaw777/copilot/fix-wordpress-init-service
[WIP] Fix wordpress-init systemd service path issues
2026-04-07 09:47:04 -05:00
copilot-swe-agent[bot]andnaturallaw777 7a1cd8a6f6 fix(wordpress): use /run/wrappers/bin/su to fix su: command not found in wordpress-init service
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/24a9d2b1-6b09-41ac-bb3b-418f0ea2b2d7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 14:46:46 +00:00
copilot-swe-agent[bot] 9407d500c8 Initial plan 2026-04-07 14:44:38 +00:00
naturallaw777 9f1dd7def1 updated nextcloud.nix 2026-04-07 09:35:23 -05:00
Sovran_Systems 480f188d86 Merge pull request #110 from naturallaw777/copilot/remove-credentials-pdf-generator
Factory security: per-device SSH passphrase, factory seal command, customer password onboarding
2026-04-07 09:28:07 -05:00
naturallaw777 e2bd366bb3 updated nextcloud.nix 2026-04-07 09:27:25 -05:00
copilot-swe-agent[bot]andnaturallaw777 f80c8a0481 Factory security: per-device SSH passphrase, factory seal, password onboarding, remove PDF generator
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4222f228-615c-4303-8286-979264c6f782

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 14:23:59 +00:00
naturallaw777 7e996fffa1 updated nextcloud.nix 2026-04-07 09:11:13 -05:00
copilot-swe-agent[bot] d14e25c29f Initial plan 2026-04-07 13:58:07 +00:00
Sovran_Systems 1ed7ab9776 Merge pull request #109 from naturallaw777/copilot/add-extra-virtual-hosts-option
[WIP] Add NixOS option for extra Caddy virtual hosts
2026-04-07 08:07:17 -05:00
copilot-swe-agent[bot]andnaturallaw777 dd8867b52f feat: add sovran_systemsOS.caddy.extraVirtualHosts NixOS option
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e966dd20-b74e-4ec5-b4db-68aa06129162

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 13:06:35 +00:00
copilot-swe-agent[bot] 3668eb2829 Initial plan 2026-04-07 13:02:55 +00:00
Sovran_Systems e751dfc1b2 Merge pull request #108 from naturallaw777/copilot/fix-hub-detection-bug
[WIP] Fix hub detection bug in port status check
2026-04-07 07:48:04 -05:00
copilot-swe-agent[bot]andnaturallaw777 6c3bbbf72b Fix Hub false closed port detection: is_listening alone is sufficient; add nftables package
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b57cc894-c639-400e-93f0-c1dc5d48870b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 12:47:03 +00:00
copilot-swe-agent[bot] 9dcb45a017 Initial plan 2026-04-07 12:44:22 +00:00
Sovran_Systems b9069433b1 Merge pull request #107 from naturallaw777/copilot/add-icon-to-service-detail-modal
[WIP] Add service icon to modal header
2026-04-07 05:40:19 -05:00
copilot-swe-agent[bot]andnaturallaw777 739f6a08da Add service icon to modal header in openServiceDetailModal and openCredsModal
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3f26f03c-29fc-4d37-9d53-eebfb8a34c52

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 10:39:59 +00:00
copilot-swe-agent[bot] 2fc8b64964 Initial plan 2026-04-07 10:36:32 +00:00
Sovran_Systems 6e133b6b59 Merge pull request #106 from naturallaw777/copilot/add-sparrow-bisq-auto-connect-tiles
[WIP] Add tile descriptions and launch links for Sparrow and Bisq Auto-Connect
2026-04-07 05:36:23 -05:00
copilot-swe-agent[bot]andnaturallaw777 01e3e02a62 Add sparrow/bisq tile descriptions, desktop launch API, and frontend launch buttons
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/5a3d2f20-4635-442e-82ba-c0b7f4aeb96e

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 10:35:35 +00:00
copilot-swe-agent[bot] 85af70e2ee Initial plan 2026-04-07 10:32:41 +00:00
naturallaw777 b21d9bef87 updated branding for hub 2026-04-07 05:20:41 -05:00
Sovran_Systems 26b89dae76 Merge pull request #105 from naturallaw777/copilot/create-desktop-icon-svg
Separate Hub desktop/dock icon from web UI branding logo
2026-04-07 05:17:55 -05:00
copilot-swe-agent[bot]andnaturallaw777 b2a2ef70a4 fix: add SVG title element for accessibility in sovran-hub-icon.svg
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e3f466ae-eee1-4ba8-b93c-00fe04c7054d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 10:14:47 +00:00
copilot-swe-agent[bot]andnaturallaw777 8286e00eb3 feat: create dedicated desktop dock icon and update nix build to use it
- Add app/sovran_systemsos_web/static/sovran-hub-icon.svg: a new square
  256x256 app icon for the GNOME dock/dash. Uses the Sovran brand dark
  green (#0d3320) rounded-rectangle background, concentric arc rings in
  brand greens (#1C9954, #077233), and a white bold "S" letterform
  centered — visible at small sizes on both light and dark panels.
- Update modules/core/sovran-hub.nix line 266 to copy the new icon file
  to the hicolor icon path instead of reusing logo-light.svg.
- logo-light.svg is left untouched; it continues to serve the Hub web UI.

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e3f466ae-eee1-4ba8-b93c-00fe04c7054d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 10:14:04 +00:00
copilot-swe-agent[bot] c7487c9763 Initial plan 2026-04-07 10:11:52 +00:00
Sovran_Systems e910d0a8a7 Merge pull request #104 from naturallaw777/copilot/update-header-layout
[WIP] Update header layout for space efficiency
2026-04-07 05:09:12 -05:00
copilot-swe-agent[bot]andnaturallaw777 a3b9608887 fix: compact header layout - row direction, 80px logo, reduced padding
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4f63f512-d505-470b-9733-1054281a98d8

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 10:08:30 +00:00
copilot-swe-agent[bot] 09002cfe22 Initial plan 2026-04-07 10:07:15 +00:00
Sovran_Systems 8a3b5c031f Merge pull request #103 from naturallaw777/copilot/fix-bitcoin-version-formatting
[WIP] Fix bitcoin version formatting to retain BIP110 patch version
2026-04-07 04:56:08 -05:00
copilot-swe-agent[bot]andnaturallaw777 b441515f89 Fix _format_bitcoin_version to include BIP110 patch version in tile display
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/951363ee-aa21-479a-9d79-0c3b5f265bf7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 09:55:13 +00:00
copilot-swe-agent[bot] b7c1632bb8 Initial plan 2026-04-07 09:53:45 +00:00
naturallaw777 a7d40fb138 updated branding for hub 2026-04-07 04:26:09 -05:00
Sovran_Systems 8f4ec83104 Merge pull request #101 from naturallaw777/copilot/fix-sparrow-bisq-tile-icons
Fix Sparrow and Bisq tile icons rendering too small
2026-04-06 23:42:47 -05:00
Sovran_Systems fba4ab13cf Merge pull request #102 from naturallaw777/copilot/fix-version-display-non-bitcoin-services
Fix version display: strip versions from non-Bitcoin tiles, wire BIP110 version correctly
2026-04-06 23:42:33 -05:00
copilot-swe-agent[bot]andnaturallaw777 5ba1a256fe fix: tighten viewBox and remove transforms on bisq.svg and sparrow.svg icons
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/bed7824c-f988-4a33-a052-4a013aa1110d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 04:41:37 +00:00
copilot-swe-agent[bot]andnaturallaw777 5ee0ef4d58 Remove non-Bitcoin version detection; only bitcoind.service tiles show versions
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/75ae6f5b-ccf1-4051-b9ae-e07c9218227d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 04:38:42 +00:00
copilot-swe-agent[bot] 5a77a03ac0 Initial plan 2026-04-07 04:36:05 +00:00
copilot-swe-agent[bot] 5349c2408a Initial plan 2026-04-07 04:33:21 +00:00
naturallaw777 9ea4fb32f4 updated sparrow and bisq icon 2026-04-06 23:27:38 -05:00
Sovran_Systems 9e1673ef7f Merge pull request #100 from naturallaw777/copilot/fix-bitcoin-tile-version-display
[WIP] Fix version display on Bitcoin tiles in Hub dashboard
2026-04-06 23:22:40 -05:00
copilot-swe-agent[bot]andnaturallaw777 44a7b2a8ab fix: use bitcoind --version for Bitcoin tile version display (works during IBD/startup)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/63b5dc59-a630-4c14-a6a7-99a71ee517b7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 04:22:19 +00:00
Sovran_Systems aa24505314 Merge pull request #99 from naturallaw777/copilot/add-sparrow-and-bisq-icons
Add missing sparrow.svg and bisq.svg service tile icons
2026-04-06 23:20:23 -05:00
copilot-swe-agent[bot] d0bf878555 Initial plan 2026-04-07 04:18:20 +00:00
copilot-swe-agent[bot]andnaturallaw777 2e5be9816e Add missing sparrow.svg and bisq.svg service tile icons
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d7bbfadc-d5a8-4750-81b4-685ccb993d70

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 03:06:12 +00:00
copilot-swe-agent[bot] 87ecccff9e Initial plan 2026-04-07 02:58:50 +00:00
naturallaw777 d60a44b438 updated hub-icon 2026-04-06 21:54:47 -05:00
Sovran_Systems 9413bb6403 Merge pull request #98 from naturallaw777/copilot/fix-bip110-version-check
Fix BIP110 version label: detect via tile icon, not subversion string
2026-04-06 21:53:44 -05:00
copilot-swe-agent[bot]andnaturallaw777 28bcddb957 Fix BIP110 version display: detect by tile icon, not subversion string
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/23090422-e59c-4d7e-8d5e-6fd36b6cf337

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 02:50:02 +00:00
copilot-swe-agent[bot] 1737e93c68 Initial plan 2026-04-07 02:47:51 +00:00
Sovran_Systems 90fdbdea70 Merge pull request #97 from naturallaw777/copilot/update-bitcoin-tiles-version-display
Fix Bitcoin tile version: only active tile shows version, preserve BIP110 tag
2026-04-06 21:39:30 -05:00
copilot-swe-agent[bot]andnaturallaw777 90ffadf2ea Fix Bitcoin tile version: preserve bip110 tag, only show version on enabled tile
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/05d1b130-dd46-4132-8120-2df883325c2a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 02:35:32 +00:00
copilot-swe-agent[bot] f6c9080cea Initial plan 2026-04-07 02:33:12 +00:00
Sovran_Systems 8e40faad75 Merge pull request #96 from naturallaw777/copilot/add-bitcoind-to-sovran-hub-path
Add bitcoind to sovran-hub-web service PATH so Bitcoin version renders on Hub tiles
2026-04-06 21:27:06 -05:00
copilot-swe-agent[bot]andnaturallaw777 4978d44ba2 Add bitcoind to sovran-hub-web PATH so Bitcoin version shows on Hub tiles
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/b8aaba8d-2c51-40ca-9826-69b78060a840

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 02:26:16 +00:00
copilot-swe-agent[bot] 6fefed2909 Initial plan 2026-04-07 02:25:28 +00:00
naturallaw777 1a48266cde updated custom-template 2026-04-06 21:13:22 -05:00
Sovran_Systems 639d39108a Merge pull request #95 from naturallaw777/copilot/fix-version-detection-nixos
[WIP] Fix version detection for NixOS systemd services
2026-04-06 21:10:32 -05:00
copilot-swe-agent[bot]andnaturallaw777 185ed4e3d8 Further tighten regex: stricter version pattern, no underscores in name segments, precise trailing-dot strip
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d75fe7da-369a-40e9-913e-7dba45de21c3

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 02:10:15 +00:00
copilot-swe-agent[bot]andnaturallaw777 8240b9af3c Address review feedback: module-level wrapper suffix regex, allow digit-starting name segments
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d75fe7da-369a-40e9-913e-7dba45de21c3

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 02:08:23 +00:00
copilot-swe-agent[bot]andnaturallaw777 deb66c9cb7 Replace CLI-based version detection with Nix store path extraction via systemctl show
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d75fe7da-369a-40e9-913e-7dba45de21c3

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 02:06:13 +00:00
copilot-swe-agent[bot] 8a49a3d04e Initial plan 2026-04-07 02:03:49 +00:00
Sovran_Systems cdccb8138c Merge pull request #94 from naturallaw777/copilot/cleanup-etc-nixos-post-install
[WIP] Remove unnecessary files from /etc/nixos after installation
2026-04-06 21:03:03 -05:00
copilot-swe-agent[bot]andnaturallaw777 09a817f02d feat: clean up /mnt/etc/nixos after nixos-install, keep only 5 required files
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/08d1a4eb-697e-46d4-bb8e-71af6bb4316f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 02:02:19 +00:00
copilot-swe-agent[bot] 4edcf066ca Initial plan 2026-04-07 02:01:07 +00:00
Sovran_Systems 0d49b67c7b Merge pull request #93 from naturallaw777/copilot/add-version-info-for-services
Add version numbers to all service tiles on the Hub dashboard
2026-04-06 20:56:52 -05:00
copilot-swe-agent[bot]andnaturallaw777 24bf72ef69 feat: add version display for all service tiles on Hub dashboard
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/6b9b51e5-85a6-46ff-8683-120ecf3640da

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 01:55:41 +00:00
copilot-swe-agent[bot] 8459061968 Initial plan 2026-04-07 01:50:23 +00:00
Sovran_Systems b3f1c35995 Merge pull request #92 from naturallaw777/copilot/add-sparrow-bisq-integration
[WIP] Add NixOS module for Sparrow Wallet and Bisq 1 integration
2026-04-06 20:45:04 -05:00
copilot-swe-agent[bot]andnaturallaw777 27f27b1503 feat: add wallet-autoconnect module for Sparrow and Bisq 1
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/29aa6dce-667a-49a6-9740-68d501fed22c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 01:44:43 +00:00
copilot-swe-agent[bot] f108abd7ae Initial plan 2026-04-07 01:42:45 +00:00
Sovran_Systems 2be8fe65d8 Merge pull request #91 from naturallaw777/copilot/add-version-display-to-hub-dashboard
[WIP] Add version number display for active bitcoind implementation
2026-04-06 20:38:42 -05:00
copilot-swe-agent[bot]andnaturallaw777 a0c1628461 feat: display bitcoind version on Bitcoin node tile in Hub dashboard
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/5b4f8da9-beec-45f2-b116-b5c0dcf4506d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 01:38:17 +00:00
copilot-swe-agent[bot] 06615a3541 Initial plan 2026-04-07 01:33:08 +00:00
Sovran_Systems 328b2a3ee8 Merge pull request #90 from naturallaw777/copilot/add-xdg-autostart-entry-hub
[WIP] Add XDG autostart entry for Sovran Hub auto-launch
2026-04-06 20:29:07 -05:00
copilot-swe-agent[bot]andnaturallaw777 5123287ef7 Fix curl command in hub-autolaunch-script (remove unnecessary -w flag)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0b0d70c0-01d1-49d1-b9ca-8d4f8e5af64a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 01:28:55 +00:00
copilot-swe-agent[bot]andnaturallaw777 13e3b76c88 Add hub auto-launch: XDG autostart, API endpoints, and frontend toggle
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0b0d70c0-01d1-49d1-b9ca-8d4f8e5af64a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 01:26:11 +00:00
copilot-swe-agent[bot] 27502c6997 Initial plan 2026-04-07 01:22:39 +00:00
Sovran_Systems 6b467525cf Merge pull request #89 from naturallaw777/copilot/add-privacy-disclosure-to-upgrade-modal
Add privacy disclosure to Node → Server+Desktop upgrade modal
2026-04-06 19:59:39 -05:00
copilot-swe-agent[bot]andnaturallaw777 b1b0e85db7 Add privacy disclosure info box to Node→Server+Desktop upgrade modal
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/5dadd5f8-7c8d-4aa1-be01-3dba9fc5dc1d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-07 00:42:58 +00:00
copilot-swe-agent[bot] e84dd7cb91 Initial plan 2026-04-07 00:41:57 +00:00
naturallaw777 94d94fb7a2 fixed ssh at first boot 2026-04-06 18:40:17 -05:00
Sovran_Systems e67b4fecc4 Merge pull request #87 from naturallaw777/copilot/remove-terminal-domain-setup
Remove redundant terminal domain setup script (fixes password prompt on boot)
2026-04-06 18:30:02 -05:00
copilot-swe-agent[bot]andnaturallaw777 f7539dc9b6 Remove redundant terminal domain setup script and update stale references
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ed7fee4d-b50e-4387-8eb6-46840b9d930f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-06 23:27:52 +00:00
copilot-swe-agent[bot] 5632068ca8 Initial plan 2026-04-06 23:25:46 +00:00
Sovran_Systems 99df7a097b Merge pull request #86 from naturallaw777/copilot/remove-pdf-mentions-and-icons
Installer completion screen: remove PDF ref, icon, and fix reboot button color
2026-04-06 18:19:28 -05:00
copilot-swe-agent[bot]andnaturallaw777 cc17c3fb42 Remove PDF mention, icon, and fix reboot button color in push_complete
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3bb82d50-1a0b-4f1d-b186-1e4efde002d1

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-06 23:18:39 +00:00
copilot-swe-agent[bot] d51919ec69 Initial plan 2026-04-06 23:17:29 +00:00
Sovran_Systems ad80b9d8c3 Merge pull request #85 from naturallaw777/copilot/replace-custom-template-nix
Simplify custom.template.nix and add-custom-nix.sh — remove hub-managed steps
2026-04-05 12:57:51 -05:00
copilot-swe-agent[bot]andnaturallaw777 3c4495c066 Simplify custom.template.nix and add-custom-nix.sh for hub-managed configuration
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/f90e34be-674b-4047-8096-c0db7883de1a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 17:56:59 +00:00
copilot-swe-agent[bot] 4765a18224 Initial plan 2026-04-05 17:55:32 +00:00
Sovran_Systems 6bd11be8b5 Merge pull request #84 from naturallaw777/copilot/add-hardware-configuration-to-modules
Fix nixos-install: wire hardware-configuration.nix into flake and installer
2026-04-05 12:51:00 -05:00
Sovran_Systems f294828409 Merge pull request #83 from naturallaw777/copilot/add-internal-drive-check-role-cards
[WIP] Add internal drive check for role selection in push_welcome
2026-04-05 12:50:48 -05:00
copilot-swe-agent[bot]andnaturallaw777 953271eeee grey out node/server roles when no second internal drive detected
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/30ed5e6b-2d61-415c-ba07-aba31dbcd839

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 17:50:20 +00:00
copilot-swe-agent[bot]andnaturallaw777 70f3cef03a Fix NixOS install: add hardware-configuration.nix to flake modules and installer
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/5db1cd99-2067-4b5c-ba11-3e9aa8fde973

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 17:50:18 +00:00
copilot-swe-agent[bot] c9a5a4dec9 Initial plan 2026-04-05 17:48:34 +00:00
copilot-swe-agent[bot] 37cc35bb1b Initial plan 2026-04-05 17:48:12 +00:00
Sovran_Systems 25b758304d Merge pull request #81 from naturallaw777/copilot/add-sovran-hub-desktop-entry
Add sovran-hub.desktop entry and icon for GNOME dock
2026-04-05 11:58:33 -05:00
Sovran_Systems 2385466b1a Merge pull request #82 from naturallaw777/copilot/fix-nixos-install-absolute-paths
fix(installer): copy role-state.nix and custom.nix to host /etc/nixos before nixos-install
2026-04-05 11:57:33 -05:00
copilot-swe-agent[bot]andnaturallaw777 37ad4fd2ad fix: copy role-state.nix and custom.nix to host /etc/nixos before nixos-install
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/38396f35-c812-43e5-9bf0-f7bd611cbba7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 16:54:29 +00:00
copilot-swe-agent[bot] 35569e6ec0 Initial plan 2026-04-05 16:53:30 +00:00
copilot-swe-agent[bot]andnaturallaw777 536eb0deb1 Add sovran-hub.desktop entry and icon to GNOME dock
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/42720669-f980-4f13-989e-0728ea9307de

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 16:52:40 +00:00
copilot-swe-agent[bot] a390c4711a Initial plan 2026-04-05 16:51:21 +00:00
Sovran_Systems 86605bda05 Merge pull request #80 from naturallaw777/copilot/fix-nixos-install-error
Add --impure to nixos-install to allow absolute path references in flake
2026-04-05 11:24:47 -05:00
copilot-swe-agent[bot]andnaturallaw777 d6cdfcf31a Add --impure flag to nixos-install command
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7723d784-dc2c-41da-b523-451a63f335eb

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 16:23:40 +00:00
copilot-swe-agent[bot] a592b270af Initial plan 2026-04-05 16:22:43 +00:00
Sovran_Systems 4be9be34e0 Merge pull request #79 from naturallaw777/copilot/replace-disko-with-sgdisk-mkfs-mount
[WIP] Replace disko with direct sgdisk and mkfs commands in installer
2026-04-05 10:41:41 -05:00
copilot-swe-agent[bot]andnaturallaw777 cb7b097ce0 Drop disko: use direct sgdisk+mkfs+mount in installer, remove disko package and disko.nix
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3dbc739b-c3da-432d-b070-16217e58c76b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 15:40:42 +00:00
copilot-swe-agent[bot] 53a0010e47 Initial plan 2026-04-05 15:38:51 +00:00
Sovran_Systems 6d36023222 Merge pull request #77 from naturallaw777/copilot/fix-disable-ssh-option
[WIP] Fix bug to disable SSH after tech support session
2026-04-05 10:20:15 -05:00
copilot-swe-agent[bot]andnaturallaw777 7c1dbeac27 Fix disable SSH option and remove Feature Manager language
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3fc488d2-ef33-4d4f-aeb5-f2532c658aad

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 15:20:01 +00:00
Sovran_Systems 9386b073ba Merge pull request #78 from naturallaw777/copilot/fix-disko-command-wipe-handling
[WIP] Fix disko command handling in installer
2026-04-05 10:19:20 -05:00
copilot-swe-agent[bot]andnaturallaw777 cf09845431 Fix disko command: use single format,mount call since disks are pre-wiped
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/80b0e65c-24c9-4448-9fdb-870891ecc30e

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 15:18:54 +00:00
copilot-swe-agent[bot] d65193d7d3 Initial plan 2026-04-05 15:17:54 +00:00
copilot-swe-agent[bot] 0e0e91d1f8 Initial plan 2026-04-05 15:17:50 +00:00
Sovran_Systems a59165c31e Merge pull request #76 from naturallaw777/copilot/add-sshd-feature-module
[WIP] Add SSH feature module with default off setting
2026-04-05 10:11:11 -05:00
copilot-swe-agent[bot]andnaturallaw777 df2768c6fc feat: move sshd into its own Nix feature module, gate Tech Support behind it
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d45dc36f-0b3b-48bb-950f-700afe45dd06

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 15:09:02 +00:00
copilot-swe-agent[bot] 109c92a33a Initial plan 2026-04-05 15:03:10 +00:00
Sovran_Systems ca11cbbc79 Merge pull request #75 from naturallaw777/copilot/revert-commit-7c047a1
[WIP] Revert changes breaking local LAN access to Hub, RTL, and Mempool
2026-04-05 09:44:19 -05:00
copilot-swe-agent[bot]andnaturallaw777 6584b63c36 Revert commit 7c047a1: restore LAN access to Hub, RTL, and Mempool
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c92f1a7f-7c42-44f1-a86d-089383bafc94

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 14:43:49 +00:00
Sovran_Systems 88cac6c1e9 Merge pull request #74 from naturallaw777/copilot/fix-disko-command-error
Fix disko ESP mount failure on dual-NVMe by splitting destroy/format into sequential calls
2026-04-05 09:42:51 -05:00
copilot-swe-agent[bot] ef39040919 Initial plan 2026-04-05 14:42:36 +00:00
copilot-swe-agent[bot]andnaturallaw777 ca275c45de Fix disko mount failure by splitting destroy and format,mount into separate calls
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/0f9fe8d2-554e-4048-9dba-5a3c3c663410

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 14:41:16 +00:00
copilot-swe-agent[bot] fee6035de0 Initial plan 2026-04-05 14:40:22 +00:00
Sovran_Systems c42962f6da Merge pull request #73 from naturallaw777/copilot/remove-unnecessary-port-exposure
Security: restrict RTL, Hub, and Mempool to LAN-only access
2026-04-05 09:33:52 -05:00
copilot-swe-agent[bot]andnaturallaw777 7c047a16b7 Security: restrict RTL, Mempool ports to LAN-only; remove global firewall rules
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/1110322d-bc41-4d5d-9a4c-e5f7a5d2ef57

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 14:29:09 +00:00
copilot-swe-agent[bot] a3b34ef74b Initial plan 2026-04-05 14:27:55 +00:00
naturallaw777 8dc3066f06 syntax error 2026-04-05 09:27:38 -05:00
naturallaw777 04fd3c523b closed unused ports 2026-04-05 09:16:03 -05:00
Sovran_Systems 6f88d0726b Merge pull request #72 from naturallaw777/copilot/fix-disko-nix-partition-syntax-again
Fix disko partition syntax and remove broken Plymouth Spinner
2026-04-05 09:03:35 -05:00
copilot-swe-agent[bot]andnaturallaw777 53ea704e57 Fix disko.nix partition syntax and remove broken Spinner from Plymouth theme
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/487cd80b-c747-44b8-9479-d3f7f7cc3328

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 14:02:40 +00:00
copilot-swe-agent[bot] 4253518ceb Initial plan 2026-04-05 14:01:31 +00:00
Sovran_Systems 1c4382e655 Merge pull request #70 from naturallaw777/copilot/fix-iso-installer-blockers
[WIP] Fix confirmed blockers in ISO installer
2026-04-05 08:36:30 -05:00
copilot-swe-agent[bot]andnaturallaw777 5b1454adf6 Fix three installer blockers: disko --yes-wipe-all-disks, stdin=DEVNULL, nixos-install --no-root-password
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/477c45ee-0958-4ba8-9612-a3be1bff9c6d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 13:36:15 +00:00
copilot-swe-agent[bot] 6d3dbf497e Initial plan 2026-04-05 13:35:12 +00:00
Sovran_Systems 0526945114 Update README.md 2026-04-05 01:36:51 -05:00
Sovran_Systems 400a59f06a Merge pull request #69 from naturallaw777/copilot/fix-iso-installer-issues
[WIP] Fix issues in the ISO installer
2026-04-05 01:34:05 -05:00
copilot-swe-agent[bot]andnaturallaw777 48826590de Fix ISO installer: remove ports dialog, fix Plymouth paths/logo, add welcome page, fix pixelated icon, fix disko args
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/6b00bbbd-8ed5-4ef2-b2fc-bfbe6361e77c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 06:33:44 +00:00
Sovran_Systems 61cf06b4c7 Update README.md with new content 2026-04-05 01:32:15 -05:00
copilot-swe-agent[bot] f4a644dc05 Initial plan 2026-04-05 06:28:05 +00:00
Sovran_Systems fc847a17cd Update README.md 2026-04-05 01:09:30 -05:00
Sovran_Systems 6bb4aaf3ba Update README.md 2026-04-05 01:02:13 -05:00
Sovran_Systems beca9756ea Create README.md file for Sovran_SystemsOS 2026-04-05 00:48:49 -05:00
Sovran_Systems 54ed1db6cd Merge pull request #68 from naturallaw777/copilot/add-bitcoin-ibd-sync-indicator
feat: Bitcoin IBD sync progress bar in active Bitcoin tile
2026-04-05 00:45:49 -05:00
copilot-swe-agent[bot]andnaturallaw777 abaae7f360 feat: Bitcoin IBD sync progress indicator in Bitcoin tile
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/2c6f8fb7-5361-469b-b12b-ef846ffb669f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 05:33:05 +00:00
copilot-swe-agent[bot] 8ca1ea8e78 Initial plan 2026-04-05 05:26:49 +00:00
Sovran_Systems a0426b2fee Merge pull request #67 from naturallaw777/copilot/fix-disko-mode-and-disk-threshold
[WIP] Fix disko mode and 2 TB threshold issues
2026-04-05 00:21:44 -05:00
copilot-swe-agent[bot]andnaturallaw777 4fd8bd7534 Fix disko mode, 2 TB threshold, add interactive disk selection, fix data_path scoping
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a0f15fe6-f9a7-4f43-9f9d-5892b0f3aba4

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 05:21:18 +00:00
copilot-swe-agent[bot] b6046e63c5 Initial plan 2026-04-05 05:17:23 +00:00
Sovran_Systems 84d76f0436 Merge pull request #66 from naturallaw777/copilot/update-installer-for-desktop-only
Desktop Only: enforce 128 GB boot disk minimum, skip data disk logic
2026-04-05 00:08:54 -05:00
copilot-swe-agent[bot]andnaturallaw777 9664c59523 feat: enforce 128 GB minimum, skip data disk for Desktop Only role
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/2be6c138-feda-4c5d-9bd8-0e5f2f6416bc

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 05:07:55 +00:00
copilot-swe-agent[bot] 265f34b8aa Initial plan 2026-04-05 05:06:55 +00:00
Sovran_Systems 0d6e7e6381 Merge pull request #65 from naturallaw777/copilot/add-btcpay-web-feature
feat(node): add BTCPay Server web exposure as a toggleable feature
2026-04-04 23:59:15 -05:00
naturallaw777 4144198e4b changed to static icon 2026-04-04 23:58:53 -05:00
copilot-swe-agent[bot]andnaturallaw777 e5d3b9236c feat: add btcpay-web feature toggle for node role
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/3881717f-97fc-4b8a-8f01-794a0699e7b3

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 04:55:25 +00:00
copilot-swe-agent[bot] 53c2371c45 Initial plan 2026-04-05 04:53:41 +00:00
Sovran_Systems 48fb59b862 Merge pull request #64 from naturallaw777/copilot/make-backup-script-role-aware
[WIP] Update backup script to be role-aware
2026-04-04 23:43:35 -05:00
copilot-swe-agent[bot]andnaturallaw777 64744d1d93 Make backup script role-aware and add manual-backup docs
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a9c69b4d-1c8d-4ade-b444-33043e52fc63

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 04:43:04 +00:00
copilot-swe-agent[bot] a1d83e731a Initial plan 2026-04-05 04:39:42 +00:00
naturallaw777 f9ecdaec96 updated update icon 2026-04-04 23:34:23 -05:00
Sovran_Systems e8a7b2c7ab Merge pull request #63 from naturallaw777/copilot/replace-update-system-button-icon
Replace Update System sidebar button emoji with custom SVG icon
2026-04-04 23:26:14 -05:00
copilot-swe-agent[bot]andnaturallaw777 3e855af8d5 Replace Update System emoji with custom update.svg icon
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ff2815b6-a5d1-4f84-bffe-5c24d40760cd

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 04:25:28 +00:00
copilot-swe-agent[bot] 9672d30de4 Initial plan 2026-04-05 04:23:36 +00:00
Sovran_Systems 78d3559e7b Merge pull request #62 from naturallaw777/copilot/move-update-system-button
[WIP] Move Update System button to left sidebar
2026-04-04 23:10:25 -05:00
copilot-swe-agent[bot]andnaturallaw777 b8956ebf72 Move Update System button from header to sidebar
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/fb939db8-ba2c-4979-9b18-bebe2618d0b5

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 04:10:04 +00:00
copilot-swe-agent[bot] 4bda2f1aae Initial plan 2026-04-05 04:07:09 +00:00
Sovran_Systems 9897c2a991 Merge pull request #61 from naturallaw777/copilot/add-rdp-to-node-role
Add RDP to Bitcoin-only Node role
2026-04-04 23:04:42 -05:00
copilot-swe-agent[bot]andnaturallaw777 af31c60be8 Add RDP to Bitcoin-only Node role
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/17c88629-43c4-438a-9640-7abe3609c82d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 04:03:30 +00:00
copilot-swe-agent[bot] 2b89969a96 Initial plan 2026-04-05 04:02:35 +00:00
Sovran_Systems 88d0f8ffb8 Merge pull request #60 from naturallaw777/copilot/role-aware-service-filtering
[WIP] Refactor monitoredServices for role-aware service filtering
2026-04-04 22:55:55 -05:00
copilot-swe-agent[bot]andnaturallaw777 58966646c2 feat: role-aware hub — service filtering, onboarding, upgrade path
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/af4088da-8845-4f7f-914f-259fd33884ed

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 03:55:20 +00:00
copilot-swe-agent[bot] c28de5def9 Initial plan 2026-04-05 03:48:27 +00:00
Sovran_Systems 34d652acbb Merge pull request #59 from naturallaw777/copilot/add-manual-backup-feature
[WIP] Add manual backup feature to Tech Support section
2026-04-04 22:42:24 -05:00
copilot-swe-agent[bot]andnaturallaw777 cc72968583 Add Manual Backup improvements: lsblk drive filtering, UI instructions, CSS border fixes
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a43d270d-eb78-4ad3-b721-fe958883c305

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 03:41:53 +00:00
copilot-swe-agent[bot] 34db1439fa Initial plan 2026-04-05 03:36:18 +00:00
Sovran_Systems 980ea9a6a5 Merge pull request #58 from naturallaw777/copilot/add-manual-backup-button
[WIP] Add manual backup button in Hub sidebar for Sovran_SystemsOS
2026-04-04 22:24:23 -05:00
copilot-swe-agent[bot]andnaturallaw777 d864402de2 feat: Add Manual Backup button in Hub sidebar with drive detection and progress streaming
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/14dc5955-19b2-4e5b-965a-2795285a22fd

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 03:24:07 +00:00
copilot-swe-agent[bot] d59b878906 Initial plan 2026-04-05 03:18:02 +00:00
Sovran_Systems ac840b684f Merge pull request #57 from naturallaw777/copilot/remove-credentials-step-onboarding-wizard
Remove Credentials step from onboarding wizard (5 → 4 steps)
2026-04-04 22:06:37 -05:00
copilot-swe-agent[bot]andnaturallaw777 547ebdb000 Remove Credentials step from onboarding wizard (5 → 4 steps)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/9614a2f0-7aa6-486c-a8a3-f3a599cbbad5

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 03:05:21 +00:00
copilot-swe-agent[bot] 4675c0cb21 Initial plan 2026-04-05 03:01:55 +00:00
Sovran_Systems 149e35c1c4 Remove Credentials step from onboarding wizard (5 → 4 steps) 2026-04-04 21:58:42 -05:00
Sovran_Systems 1897ffddd9 Merge pull request #56 from naturallaw777/copilot/fix-onboarding-wizard-styling
[WIP] Fix onboarding wizard rendering issues by restoring missing CSS
2026-04-04 21:48:37 -05:00
copilot-swe-agent[bot]andnaturallaw777 87e40a631c Restore missing onboarding wizard CSS styles
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/efd0c45e-80b3-427c-af20-3f8bc07f8647

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 02:47:39 +00:00
copilot-swe-agent[bot] ca78bb4ed4 Initial plan 2026-04-05 02:45:46 +00:00
Sovran_Systems c9ce39b4b2 Merge pull request #55 from naturallaw777/copilot/remove-feature-manager-step
[WIP] Remove Feature Manager step from onboarding wizard
2026-04-04 21:40:58 -05:00
copilot-swe-agent[bot]andnaturallaw777 c7f48b2f4a Remove Feature Manager step from onboarding wizard (6 steps → 5 steps)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c5126015-7ea2-439c-a541-43ed2a7c2460

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 02:40:29 +00:00
copilot-swe-agent[bot] 9509dd539b Initial plan 2026-04-05 02:36:32 +00:00
Sovran_Systems 7a5e6082eb Merge pull request #54 from naturallaw777/copilot/fix-icon-name-mismatches
Fix icon name mismatches for System Passwords and Element-Call tiles; add haven.svg
2026-04-04 21:31:39 -05:00
copilot-swe-agent[bot]andnaturallaw777 67b533146a Fix icon name mismatches and add haven.svg for Haven Relay tile
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/f7fed319-711f-4ced-b732-6d832289bf4d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-05 02:30:55 +00:00
copilot-swe-agent[bot] 589879729d Initial plan 2026-04-05 02:29:21 +00:00
naturallaw777 5d7c5eb4a6 updated missing icons 2026-04-04 21:23:04 -05:00
naturallaw777 ae21a8ca39 updated logos 2026-04-04 21:08:29 -05:00
naturallaw777 369b63097e bigger logo 2026-04-04 18:58:17 -05:00
naturallaw777 25a84b8758 bigger logo 2026-04-04 18:55:16 -05:00
naturallaw777 2f30112c66 bigger logo 2026-04-04 18:48:46 -05:00
naturallaw777 9483f7c27a bigger logo 2026-04-04 18:43:12 -05:00
naturallaw777 33d55c4324 bigger logo 2026-04-04 18:39:14 -05:00
Sovran_Systems bd2299233d Merge pull request #52 from naturallaw777/copilot/refactor-split-css-js-files
[WIP] Refactor and split large CSS and JS files into smaller modules
2026-04-04 18:36:13 -05:00
copilot-swe-agent[bot]andnaturallaw777 815b195600 Split style.css and app.js into modular CSS/JS files under css/ and js/ directories
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/50712b31-5843-45c4-a8f1-3952656b636c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 23:35:27 +00:00
copilot-swe-agent[bot] 2493777a42 Initial plan 2026-04-04 23:19:54 +00:00
naturallaw777 f9a20ac39b bigger logo 2026-04-04 18:09:13 -05:00
Sovran_Systems a02f539729 Merge pull request #51 from naturallaw777/copilot/fix-bitcoin-node-modal-status
Fix Bitcoin node modals showing incorrect enabled state due to shared systemd unit
2026-04-04 17:59:42 -05:00
copilot-swe-agent[bot]andnaturallaw777 6b0da2f7cd Fix: Pass icon to disambiguate Bitcoin node modals sharing bitcoind.service unit
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ca5a66cc-4b7d-4d26-9a65-3d0c9de4a279

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 22:57:12 +00:00
copilot-swe-agent[bot] e40b0bd188 Initial plan 2026-04-04 22:53:05 +00:00
Sovran_Systems f10cb78022 Add missing service detail modal CSS styles (svc-detail, addon, domain, port, support, feature) 2026-04-04 17:27:35 -05:00
Sovran_Systems a875546133 Restore full style.css accidentally truncated in f86df9c; apply logo height fix (64px→36px) 2026-04-04 17:16:11 -05:00
Sovran_Systems 1d871133f7 Merge pull request #50 from naturallaw777/copilot/fix-dashboard-tile-service-status
Fix tile/modal status inconsistency and add BIP110/Bitcoin Core mutual-exclusivity messaging
2026-04-04 13:38:16 -05:00
copilot-swe-agent[bot]andnaturallaw777 1692ba0e9d Fix tile/modal status inconsistency and add BIP110/Bitcoin Core mutual-exclusivity messaging
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7238843b-8bbf-4f02-b932-defb5b6ace35

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 18:37:18 +00:00
copilot-swe-agent[bot] 1fd4e101e6 Initial plan 2026-04-04 18:28:29 +00:00
Sovran_Systems f86df9c173 fix: reduce header logo height from 64px to 36px to fit header bar 2026-04-04 13:12:58 -05:00
Sovran_Systems 89b55ce266 Merge pull request #49 from naturallaw777/copilot/update-logo-size-and-clarify-features
Larger logo, Bitcoin node mutual exclusivity UX, and uniform domain subdomain guidance
2026-04-04 11:55:01 -05:00
copilot-swe-agent[bot]andnaturallaw777 f5bff0b139 Make logo bigger, clarify Bitcoin node mutual exclusivity, and improve domain setup instructions
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/45129e42-f838-47b6-a33d-61c50a2ba927

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 16:48:15 +00:00
copilot-swe-agent[bot] b67e34127a Initial plan 2026-04-04 16:43:32 +00:00
Sovran_Systems 04b175df51 Merge pull request #48 from naturallaw777/copilot/research-caddy-and-zeus-tiles
Fix Caddy domain, Zeus emoji, Feature Manager in tiles, header centering, domain dialog parity
2026-04-04 11:29:44 -05:00
copilot-swe-agent[bot]andnaturallaw777 3a87297b41 Polish: clean up Unicode escapes and fix DDNS label wording
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/faca798f-6820-4db6-adc9-d5a5c9ac1ba1

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 16:28:23 +00:00
copilot-swe-agent[bot]andnaturallaw777 dd9ff2f4b2 Fix 5 issues: Caddy domain, Zeus emoji, Feature Manager in tiles, header centering, domain dialog content
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/faca798f-6820-4db6-adc9-d5a5c9ac1ba1

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 16:25:36 +00:00
Sovran_Systems d7cb97aa73 Merge pull request #47 from naturallaw777/copilot/add-composite-health-status
[WIP] Add composite health status to service detail API
2026-04-04 10:53:34 -05:00
copilot-swe-agent[bot]andnaturallaw777 7361047b48 Add composite health status, smart port language, remove banner, center layout, bigger logo
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/fbd178f9-a25d-4065-b3c1-79eecd3caade

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 15:52:44 +00:00
copilot-swe-agent[bot] cf176ea2db Initial plan 2026-04-04 15:46:48 +00:00
Sovran_Systems 2f467dfee2 Merge pull request #46 from naturallaw777/copilot/redesign-dashboard-service-tiles
[WIP] Redesign dashboard to simplify service tiles and add detail modal
2026-04-04 10:28:56 -05:00
copilot-swe-agent[bot]andnaturallaw777 03dd3eefb5 Redesign dashboard: simplify tiles to icon/name/status, add service detail modal, new /api/service-detail endpoint, SERVICE_DESCRIPTIONS dict, and updated CSS styles
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4f00183a-525f-4c71-91f8-c96c95ca1025

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 15:28:07 +00:00
copilot-swe-agent[bot] 13af3fb071 Initial plan 2026-04-04 15:20:54 +00:00
Sovran_Systems 470e47fefa Merge pull request #45 from naturallaw777/copilot/add-domain-health-status-to-dashboard
Add live domain health badges to hub tiles and Feature Manager
2026-04-04 09:50:50 -05:00
copilot-swe-agent[bot]andnaturallaw777 8002b180b1 Add domain health status to hub tiles and Feature Manager
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/52147672-b757-4524-971a-9e0dab981354

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 14:49:30 +00:00
copilot-swe-agent[bot] a3c75462c9 Initial plan 2026-04-04 14:42:09 +00:00
Sovran_Systems 1998fc0652 Delete .gitignore.txt 2026-04-04 09:31:46 -05:00
Sovran_Systems 6ee3d00802 Update .gitignore 2026-04-04 09:31:19 -05:00
Sovran_Systems cf46424f50 Delete path/to directory 2026-04-04 09:30:27 -05:00
Sovran_Systems f49a542ddf Update service data model to include requiresDomain and domain status fields. 2026-04-04 09:27:06 -05:00
Sovran_Systems b6be88d01f Merge pull request #44 from naturallaw777/copilot/update-tech-support-ssh-login-paths
Simplify tech support protected paths: replace per-app dirs with /home
2026-04-04 08:17:14 -05:00
copilot-swe-agent[bot]andnaturallaw777 2a105edf04 Update tech support protected paths: remove root/.lnd, sparrow, bisq; add /home
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c1303e8b-ff51-4951-b64c-2162d9e9a805

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 13:16:22 +00:00
copilot-swe-agent[bot] 159238f4f8 Initial plan 2026-04-04 13:15:17 +00:00
Sovran_Systems 868cdd9132 Merge pull request #43 from naturallaw777/copilot/feature-tech-support-tile-again
Feature: Tech Support tile with wallet privacy control
2026-04-03 20:40:09 -05:00
copilot-swe-agent[bot]andnaturallaw777 85396e804d Add NixOS tech-support module and security documentation
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7e7a94ca-202b-4eb5-aa3a-a36a1365574b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 01:31:56 +00:00
copilot-swe-agent[bot] 3407612ea9 Initial plan 2026-04-04 01:25:16 +00:00
Sovran_Systems d8b6785659 Merge pull request #42 from naturallaw777/copilot/feature-tech-support-tile
[WIP] Add tech support tile with user wallet privacy control
2026-04-03 20:04:46 -05:00
copilot-swe-agent[bot]andnaturallaw777 dd3a20ed00 feat: wallet privacy control and audit logging for tech support sessions
- Add dedicated `sovran-support` restricted user (non-root) for SSH sessions
- Apply POSIX ACLs via setfacl to block support user from wallet directories
  (LND, Sparrow, Bisq, nix-bitcoin-secrets) by default
- Graceful fallback to root authorized_keys if user creation fails (with UI warning)
- Add time-limited wallet unlock consent: POST /api/support/wallet-unlock
- Add wallet re-lock: POST /api/support/wallet-lock
- Add audit log: GET /api/support/audit-log (append-only, all events logged)
- Expand /api/support/status with wallet_protected, wallet_unlocked,
  wallet_unlocked_until, protected_paths, acl_applied fields
- Update frontend to show wallet protection status box with protected path list
- Show wallet unlock/re-lock controls with duration selector (30min/1h/2h)
- Show audit log viewer in support modal (toggleable)
- Add wallet unlock expiry auto-refresh timer in JS
- Add CSS styles for wallet protection box, unlock/lock buttons, audit log

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/70330ce3-1ed7-46b1-ac66-4cdc50de6017

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 01:02:58 +00:00
copilot-swe-agent[bot] 87529b0d3f Initial plan 2026-04-04 00:52:59 +00:00
Sovran_Systems 8d62ff0b1f Merge pull request #40 from naturallaw777/copilot/add-avahi-service-for-sovran-hub
Use Avahi hostName override for sovransystemsos.local mDNS without changing system hostname
2026-04-03 19:41:36 -05:00
copilot-swe-agent[bot]andnaturallaw777 ed1548ea81 Add Avahi mDNS hostName override and Caddy .local block for sovransystemsos.local LAN access
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/ca3945d7-a2cb-4121-bd89-a5e3fe31fc47

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 00:40:24 +00:00
copilot-swe-agent[bot] beada8f174 Initial plan 2026-04-04 00:39:05 +00:00
naturallaw777 3ec34cb12a bump 2026-04-03 19:31:57 -05:00
Sovran_Systems e8784cdedc Merge pull request #39 from naturallaw777/copilot/revert-hostname-mdns-caddy-changes
Revert hostName/mDNS/Caddy .local changes; restore flake-rebuild compatibility
2026-04-03 19:25:24 -05:00
copilot-swe-agent[bot]andnaturallaw777 0a323d7b3c Revert hostName/mDNS/Caddy .local block changes from PR #34
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/8d17fed2-7329-442e-bfa5-a96a38fb31e4

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-04 00:24:10 +00:00
copilot-swe-agent[bot] 74853431e1 Initial plan 2026-04-04 00:22:41 +00:00
Sovran_Systems df919975af Merge pull request #37 from naturallaw777/copilot/fix-onboarding-port-forwarding-colors
Fix low-contrast onboarding port forwarding info boxes in dark theme
2026-04-03 16:38:54 -05:00
copilot-swe-agent[bot]andnaturallaw777 72a756bfbf Fix dark theme contrast for onboarding port forwarding totals and warning boxes
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a017a6f3-2b07-4fa8-8815-84ae87f403bf

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 21:36:07 +00:00
copilot-swe-agent[bot] f0d22e698b Initial plan 2026-04-03 21:33:12 +00:00
Sovran_Systems 66b4d43fee Merge pull request #35 from naturallaw777/copilot/improve-port-forwarding-ui
Improve port forwarding panel readability: remove scroll cap, increase table font size
2026-04-03 16:12:09 -05:00
Sovran_Systems 5ecee06e58 Merge pull request #34 from naturallaw777/copilot/make-sovran-hub-accessible
feat: LAN discovery via mDNS — serve Hub at http://sovransystemsos.local
2026-04-03 16:11:34 -05:00
copilot-swe-agent[bot]andnaturallaw777 9d5e30ea83 Improve port forwarding panel UI: larger table font, no scroll cap on Step 3
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7fc0a8b1-1f5b-489c-8e6a-8cf9ed628ccf

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 21:08:58 +00:00
copilot-swe-agent[bot]andnaturallaw777 08452e06cc feat: enable mDNS (Avahi) and local reverse proxy for sovransystemsos.local
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4159c571-2bfb-48fc-a6bc-e0765ef88ef6

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 21:08:21 +00:00
copilot-swe-agent[bot] ab5494f4ad Initial plan 2026-04-03 21:06:42 +00:00
copilot-swe-agent[bot] 2e9bb9e920 Initial plan 2026-04-03 21:06:27 +00:00
Sovran_Systems 9684bc3569 Merge pull request #31 from naturallaw777/copilot/update-onboarding-step-2
[WIP] Update onboarding wizard Step 2 for clarity
2026-04-03 15:54:16 -05:00
copilot-swe-agent[bot]andnaturallaw777 15e6cfb866 Update onboarding Step 2: clarify Njal.la sequence and display external IP
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4e4b917b-6246-4db3-9e2d-536cce11a19a

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 20:54:01 +00:00
copilot-swe-agent[bot] 21fc552f40 Initial plan 2026-04-03 20:51:06 +00:00
Sovran_Systems cfb6c3409f Update onboarding Step 2 description to clarify Njal.la account/domain/Dynamic record flow 2026-04-03 15:44:41 -05:00
Sovran_Systems a1247010ca Update onboarding Step 2 instructions to clarify Njal.la setup order 2026-04-03 15:43:04 -05:00
Sovran_Systems b0d1ca7a80 Merge pull request #30 from naturallaw777/copilot/simplify-port-forwarding-step
[WIP] Simplify port forwarding step to show required ports clearly
2026-04-03 15:32:59 -05:00
copilot-swe-agent[bot]andnaturallaw777 c7974c7aa9 simplify onboarding Step 3 port forwarding to clean static list
- Replace complex per-service/health-check UI with a clear, hardcoded
  table of required ports (80, 443, 22, 8448) and an optional Element
  Calling section (7881 TCP, 7882-7894 UDP, 5349 TCP, 3478 UDP,
  30000-40000 TCP/UDP).
- Add totals line: 4 openings without Element Calling, 9 with.
- Drop /api/ports/health fetch and all dynamic breakdowns (affected
  services loop, closed-port warnings, "View All Required Ports" table).
- Keep internal-IP display box, SSL-cert warning, and "How to set up
  port forwarding" collapsible section.
- Add prominent note that each port only needs to be forwarded once.
- Update Step 3 header description in onboarding.html to match.

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/523e0770-f144-4f47-932b-c0d40782a35b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 20:32:05 +00:00
copilot-swe-agent[bot] 8cf8fcdf82 Initial plan 2026-04-03 20:29:52 +00:00
Sovran_Systems 49c20d8e40 Merge pull request #29 from naturallaw777/copilot/add-sovran-systems-logo
Add Sovran Systems SVG logo to hub header and onboarding welcome page
2026-04-03 15:17:21 -05:00
copilot-swe-agent[bot]andnaturallaw777 777558182d Add Sovran Systems SVG logo to hub header and onboarding welcome page
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/fd3f0f95-4795-4d0b-8d16-fc00bd9d15b6

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 20:16:31 +00:00
copilot-swe-agent[bot] 091f3eb33d Initial plan 2026-04-03 20:13:23 +00:00
Sovran_Systems 41e6eab343 Merge pull request #28 from naturallaw777/copilot/fix-onboarding-wizard-center
Fix onboarding wizard: centering, njal.la domain flow, port forwarding guidance
2026-04-03 14:59:36 -05:00
copilot-swe-agent[bot]andnaturallaw777 125e6bef76 Fix onboarding wizard: centering, njal.la domain instructions, port forwarding guidance
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a264d893-5e77-4b7b-98d5-23796530fe97

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 19:49:15 +00:00
copilot-swe-agent[bot] 0479b37982 Initial plan 2026-04-03 19:45:32 +00:00
Sovran_Systems 6c8bf8474e Merge pull request #27 from naturallaw777/copilot/add-first-boot-onboarding-wizard
[WIP] Add first-boot onboarding wizard for Sovran Hub
2026-04-03 14:16:20 -05:00
copilot-swe-agent[bot]andnaturallaw777 04d282f790 Add first-boot onboarding wizard (backend + frontend)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d070c508-d5df-43c7-a0a6-a7be4c65fed7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 19:13:26 +00:00
copilot-swe-agent[bot] 3488a888de Initial plan 2026-04-03 19:06:27 +00:00
Sovran_Systems 3c3f6bfdb4 Merge pull request #26 from naturallaw777/copilot/make-port-health-banner-subtle
Soften port health status banner: no flash, neutral text, calmer copy
2026-04-03 13:50:23 -05:00
copilot-swe-agent[bot]andnaturallaw777 11a2bc57a7 Make port health status banner more subtle for critical/warning states
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/153a5e43-7267-4f3c-aa97-ce6c80d78f82

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 18:49:49 +00:00
copilot-swe-agent[bot] 91cdda8961 Initial plan 2026-04-03 18:47:00 +00:00
Sovran_Systems d77a6a96f1 Merge pull request #25 from naturallaw777/copilot/add-global-system-status-banner
Add global port health status banner to Sovran_SystemsOS Hub
2026-04-03 13:32:43 -05:00
copilot-swe-agent[bot]andnaturallaw777 0d3e181458 feat: add global system status banner for port health
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/c41a2529-e172-4c84-90c0-1b5477ea4f9d

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 18:25:24 +00:00
copilot-swe-agent[bot] 4201ba2c6c Initial plan 2026-04-03 18:19:41 +00:00
Sovran_Systems fd918ad002 Merge pull request #24 from naturallaw777/copilot/refactor-sidebar-layout
[WIP] Refactor dashboard layout to include sidebar
2026-04-03 13:02:39 -05:00
copilot-swe-agent[bot]andnaturallaw777 02ae34dbd0 fix: collect all support services before rendering sidebar (code review fix)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4304350a-bc4f-4698-82b5-8ee28f0ad960

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 18:02:06 +00:00
copilot-swe-agent[bot]andnaturallaw777 c6868b63bc refactor: sidebar layout for Tech Support and Feature Manager
- Add <aside class="sidebar"> with #sidebar-support and #sidebar-features to index.html
- Restyle .main-content as flex layout (sidebar left, tiles right)
- Body is now display:flex column with overflow:hidden for independent scroll panels
- Sidebar (270px fixed) with overflow-y:auto scrolls independently
- Tiles area (flex:1) scrolls independently
- New sidebar support button (.sidebar-support-btn) replaces support tile in main grid
- Feature Manager now renders into #sidebar-features instead of $tilesArea
- Compact sidebar overrides for .feature-card padding/font-size
- Remove 'support' and 'feature-manager' from CATEGORY_ORDER
- Responsive: sidebar becomes full-width above tiles at <=768px

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4304350a-bc4f-4698-82b5-8ee28f0ad960

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 18:00:14 +00:00
copilot-swe-agent[bot] 5a4383b6ec Initial plan 2026-04-03 17:53:29 +00:00
Sovran_Systems ac6b568bdc Merge pull request #23 from naturallaw777/copilot/fix-icon-display-issue
[WIP] Fix icon display issue due to mount order conflict
2026-04-03 12:47:46 -05:00
copilot-swe-agent[bot]andnaturallaw777 6400deddbf Fix hub icons: swap mount order and add system/support/zeus SVGs
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e3246ce1-14ce-4dad-98e9-74738a24ae30

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 17:47:12 +00:00
copilot-swe-agent[bot] 3a59974277 Initial plan 2026-04-03 17:45:02 +00:00
Sovran_Systems fd7dfb7eda Merge pull request #22 from naturallaw777/copilot/add-dynamic-port-status-detection
Add local-only dynamic port status detection and clearer port forwarding UX
2026-04-03 12:34:41 -05:00
copilot-swe-agent[bot]andnaturallaw777 7be3f59613 Fix unnecessary escaped single quotes in app.js string literals
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/cd52f6a2-250b-49e3-8558-aa2ae7512d1b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 17:31:36 +00:00
copilot-swe-agent[bot]andnaturallaw777 df5ad3afe2 Add dynamic port status detection and improved port forwarding instructions
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/cd52f6a2-250b-49e3-8558-aa2ae7512d1b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 17:29:02 +00:00
copilot-swe-agent[bot] 0b122d8669 Initial plan 2026-04-03 17:23:13 +00:00
Sovran_Systems b0d7db3102 Merge pull request #21 from naturallaw777/copilot/add-port-requirements-notification
[WIP] Add network port requirements notification for installation
2026-04-03 12:04:00 -05:00
copilot-swe-agent[bot]andnaturallaw777 b2fb7035e0 Add network port requirements UI, install notification, and tile port info
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/54981eb1-b1c5-4e1a-b587-730f41c59e01

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 17:03:42 +00:00
copilot-swe-agent[bot] ede46facf1 Initial plan 2026-04-03 16:51:31 +00:00
naturallaw777 d4f81339ef added awk command 2026-04-03 11:36:03 -05:00
Sovran_Systems 12be806f89 Merge pull request #19 from naturallaw777/copilot/fix-matrix-synapse-create-users
[WIP] Fix matrix-synapse-create-users to always write individual Hub credential files
2026-04-03 11:32:41 -05:00
copilot-swe-agent[bot]andnaturallaw777 0f4f53b9e5 fix: matrix-synapse-create-users always writes individual Hub credential files
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/4259c835-2875-4a48-86c9-1efccbeb6887

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 16:32:25 +00:00
copilot-swe-agent[bot] 13b34ca5b9 Initial plan 2026-04-03 16:28:25 +00:00
Sovran_Systems ed82bd9fe1 Merge pull request #17 from naturallaw777/copilot/fix-matrix-modal-credentials-structure
Fix Matrix-Synapse credentials modal: replace multiline blob with individual credential rows
2026-04-03 11:15:05 -05:00
copilot-swe-agent[bot]andnaturallaw777 b1386ba701 Fix Matrix credentials modal: write individual credential files and update hub config
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/f4c4df17-1ef8-4b72-be8a-82472a5f4476

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 16:12:00 +00:00
copilot-swe-agent[bot] 9dd08dc2ae Initial plan 2026-04-03 16:09:01 +00:00
Sovran_Systems 6548773a76 Merge pull request #16 from naturallaw777/copilot/fix-css-theme-bug
[WIP] Fix CSS theme bug in support and feature manager styles
2026-04-03 10:59:35 -05:00
copilot-swe-agent[bot]andnaturallaw777 fc2c7e7928 Fix CSS media query, add Matrix user management UI and API endpoints
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/84f10dbb-7db4-4f3f-b9b4-0f20455ac3e0

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 15:58:33 +00:00
copilot-swe-agent[bot] e90fbccde0 Initial plan 2026-04-03 15:53:39 +00:00
Sovran_Systems 55dec88909 Merge pull request #15 from naturallaw777/copilot/fix-user-existence-check
[WIP] Fix user registration error on existing machines
2026-04-03 10:44:23 -05:00
copilot-swe-agent[bot]andnaturallaw777 570a767636 fix(synapse): tolerate existing users in matrix-synapse-create-users script
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/f76f46da-0836-4295-8e26-c656acc38e3f

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 15:44:00 +00:00
copilot-swe-agent[bot] 90ddd5812e Initial plan 2026-04-03 15:42:53 +00:00
Sovran_Systems 145083cfcc Merge pull request #14 from naturallaw777/copilot/fix-credentials-copy-button
Fix credential copy buttons on non-HTTPS (HTTP) contexts
2026-04-03 10:29:09 -05:00
copilot-swe-agent[bot]andnaturallaw777 8f6d294995 Fix copy buttons failing on non-HTTPS browsers with clipboard fallback
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/5f3c4b7f-716c-46ef-9a2a-b97b7c1f9501

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 15:27:40 +00:00
copilot-swe-agent[bot] 1ea3723849 Initial plan 2026-04-03 15:26:58 +00:00
Sovran_Systems 0086a47938 Merge pull request #13 from naturallaw777/copilot/add-rtl-mempool-lan-proxies
[WIP] Add RTL and Mempool LAN reverse proxies to Caddy
2026-04-03 10:21:56 -05:00
copilot-swe-agent[bot]andnaturallaw777 e6cdb3b840 Add RTL and Mempool LAN reverse proxies, open firewall ports
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/d29c1b82-a70e-4092-88c7-b521a1b3cac3

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 15:21:01 +00:00
copilot-swe-agent[bot] dfe45bdbb2 Initial plan 2026-04-03 15:19:46 +00:00
Sovran_Systems e42bea8edf Merge pull request #12 from naturallaw777/copilot/fix-service-tiles-runtime-state
Fix Feature Manager: runtime state not reflected in service tiles, missing feature_manager config key, empty domain false positive
2026-04-03 09:55:39 -05:00
copilot-swe-agent[bot]andnaturallaw777 9cc237fb5b Fix all 4 Feature Manager bugs in server.py
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/77921fb1-4d4b-4d10-b982-b3768b858b86

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 14:52:14 +00:00
copilot-swe-agent[bot] ab60d2b504 Initial plan 2026-04-03 14:50:04 +00:00
Sovran_Systems 478a8b0189 Merge pull request #11 from naturallaw777/copilot/eliminate-hub-overrides
Eliminate hub-overrides.nix: write feature toggles directly into custom.nix
2026-04-03 09:30:43 -05:00
copilot-swe-agent[bot]andnaturallaw777 3c6106d06a Eliminate hub-overrides.nix: write feature toggles into custom.nix instead
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/db82f216-af3e-4d7f-a972-86c03f23e069

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 14:28:24 +00:00
copilot-swe-agent[bot] 8d05f43594 Initial plan 2026-04-03 14:24:14 +00:00
naturallaw777 f5180767b1 updated wiring for hub feature enable 2026-04-03 09:07:07 -05:00
naturallaw777 f3d75b9ba5 updated wiring for hub feature enable 2026-04-03 08:37:21 -05:00
naturallaw777 304df327e3 UX update for feature manager 2026-04-03 07:31:17 -05:00
naturallaw777 801b46b95f deeper fix for RDP regeneration 2026-04-03 07:16:01 -05:00
naturallaw777 bc7a9d96da deeper fix for RDP regeneration 2026-04-03 07:10:40 -05:00
naturallaw777 1f273d9229 fix for RDP regeneration 2026-04-03 07:08:09 -05:00
Sovran_Systems 60638cd1e3 Merge pull request #10 from naturallaw777/copilot/fix-rebuild-modal-race-condition
Fix stale rebuild modal race condition in Features Manager
2026-04-02 20:41:28 -05:00
copilot-swe-agent[bot]andnaturallaw777 c139496af9 fix: clear stale rebuild log before new rebuild and delay first poll
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/eee95839-bfd2-4733-9799-a034178bcdd6

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 01:40:43 +00:00
copilot-swe-agent[bot] e0447c551a Initial plan 2026-04-03 01:38:04 +00:00
Sovran_Systems 81ad77567a Merge pull request #9 from naturallaw777/copilot/overlay-runtime-feature-states
[WIP] Update /api/services endpoint to overlay feature states
2026-04-02 20:29:45 -05:00
copilot-swe-agent[bot]andnaturallaw777 cba66e86df Fix service tiles showing stale enabled state by overlaying runtime hub-overrides
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/e840f6c9-69a3-4ced-b6ef-128a0775321c

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 01:29:21 +00:00
copilot-swe-agent[bot] 12eb68abdf Initial plan 2026-04-03 01:26:13 +00:00
naturallaw777 0670f1248a fix for feature manager 2026-04-02 20:15:57 -05:00
naturallaw777 69c01d605f fix for feature manager 2026-04-02 20:03:13 -05:00
naturallaw777 1090aa056b fix for feature manager 2026-04-02 20:00:27 -05:00
naturallaw777 2378a278f2 reverted to old file 2026-04-02 19:49:51 -05:00
Sovran_Systems 280e7a4132 Merge pull request #8 from naturallaw777/copilot/fix-feature-manager-bugs
[WIP] Fix Feature Manager bugs in Sovran Hub
2026-04-02 18:59:55 -05:00
copilot-swe-agent[bot] 6a0a4e0489 Initial plan 2026-04-02 23:50:44 +00:00
naturallaw777 a54beaffad updated Feature Manager 2026-04-02 18:42:00 -05:00
Sovran_Systems 064ede9a75 Merge pull request #7 from naturallaw777/copilot/add-feature-manager-ui
Add Feature Manager to Sovran Hub dashboard
2026-04-02 18:30:03 -05:00
copilot-swe-agent[bot]andnaturallaw777 e43552373c fix: validate domain_name to prevent path injection; fix toggle revert logic
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/9088415a-efc3-4dd1-9c22-877a543af47b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-02 23:27:14 +00:00
copilot-swe-agent[bot]andnaturallaw777 b9c8c20347 feat: add Feature Manager to Sovran Hub dashboard
- flake.nix: import /etc/nixos/hub-overrides.nix alongside custom.nix
- sovran-hub.nix: add hub-overrides-init service (seeds file if missing),
  sovran-hub-rebuild service (nixos-rebuild switch only), and
  feature_manager=true in generated config.json
- server.py: add FEATURE_REGISTRY with 6 features (rdp, haven,
  element-calling, mempool, bip110, bitcoin-core); add hub-overrides.nix
  read/write helpers; add /api/features, /api/features/toggle,
  /api/rebuild/status, /api/domains/set, /api/domains/set-email,
  /api/domains/status endpoints; update /api/config to expose feature_manager
- index.html: add domain setup modal, SSL email modal, feature confirm
  modal, and rebuild modal HTML
- app.js: add Feature Manager rendering with sub-category layout,
  feature toggle cards with sliding toggles, domain setup flow,
  SSL email collection, conflict confirmation, rebuild polling
- style.css: add Feature Manager styles (feature cards, toggle switch,
  domain badge, conflict warning, domain input fields)"

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/9088415a-efc3-4dd1-9c22-877a543af47b

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-02 23:24:17 +00:00
copilot-swe-agent[bot] 971b0797df Initial plan 2026-04-02 23:15:35 +00:00
naturallaw777 19c7e01b2a updated public key 2026-04-02 17:39:51 -05:00
naturallaw777 f8c15bdaa4 added public key 2026-04-02 17:25:21 -05:00
naturallaw777 71a25c12ab fixed service layout 2026-04-02 17:20:14 -05:00
naturallaw777 e436b2f7a6 added service feature 2026-04-02 17:08:20 -05:00
naturallaw777 c4307f358c fixed RDP layout 2026-04-02 16:48:01 -05:00
naturallaw777 b63ad15cc1 fixed RDP layout 2026-04-02 16:45:27 -05:00
naturallaw777 51c458c33a added RDP 2026-04-02 16:39:01 -05:00
naturallaw777 8546ff073a removed start stop toggles 2026-04-02 16:35:18 -05:00
naturallaw777 4d23197aa3 added qr code 2026-04-02 16:23:45 -05:00
naturallaw777 2fefbf47e4 added qr code 2026-04-02 16:17:39 -05:00
naturallaw777 d391905e92 added qr code 2026-04-02 16:10:44 -05:00
naturallaw777 13f38f6254 added lndconnet 2026-04-02 16:03:55 -05:00
naturallaw777 195f616ca8 frontend onion links 2026-04-02 15:55:57 -05:00
naturallaw777 78c1d2f2bc frontend link fix 2026-04-02 15:50:31 -05:00
naturallaw777 31539510d5 frontend link fix 2026-04-02 15:46:23 -05:00
naturallaw777 ad0f04c0bf frontend link fix 2026-04-02 15:43:32 -05:00
naturallaw777 b6dfbc4a56 frontend uses weblinks 2026-04-02 15:37:07 -05:00
naturallaw777 ae1f39f0c8 updated BIP110 2026-04-02 15:24:24 -05:00
naturallaw777 987d62ce4d updated security 2026-04-02 15:14:15 -05:00
naturallaw777 bb2c66a4dc added passwd fix for user account 2026-04-02 15:09:04 -05:00
naturallaw777 f1e79d6408 added password tile 2026-04-02 14:55:32 -05:00
naturallaw777 a7d3af4ddd fixed cache 2026-04-02 14:50:07 -05:00
naturallaw777 9f179295d8 fixed app.js 2026-04-02 14:25:04 -05:00
naturallaw777 64c32a7f53 added info dialog for each tile 2026-04-02 14:20:06 -05:00
naturallaw777 d9a5416012 fixed stale info 2026-04-02 14:07:47 -05:00
naturallaw777 9a61994dde fixed reboot menu 2026-04-02 14:01:08 -05:00
naturallaw777 868e6e3315 fixed the color of buttons 2026-04-02 13:54:03 -05:00
naturallaw777 bb7db0693a fixed updater 2026-04-02 13:42:24 -05:00
naturallaw777 a66e8e736f updated logging 2026-04-02 13:34:29 -05:00
naturallaw777 3e1f672c00 updated logging 2026-04-02 13:27:25 -05:00
naturallaw777 eb11231e34 updated logging 2026-04-02 13:21:33 -05:00
naturallaw777 150666d7c3 updated logging 2026-04-02 13:15:19 -05:00
naturallaw777 38733daffc updated logging 2026-04-02 13:09:07 -05:00
naturallaw777 08492cef94 added new systemd update unit 2026-04-02 12:54:32 -05:00
naturallaw777 ad688a1d29 fixed ssh access 2026-04-02 12:37:43 -05:00
naturallaw777 fb940b055d fixed for update button in web 2026-04-02 12:31:04 -05:00
naturallaw777 ea28f9b303 changed port 2026-04-02 12:15:53 -05:00
Sovran_Systems d60a51e033 Merge pull request #6 from naturallaw777/copilot/remove-gtk-app-add-web-app
Replace GTK4/Libadwaita desktop app with FastAPI web app (Sovran_SystemsOS Hub)
2026-04-02 12:04:10 -05:00
copilot-swe-agent[bot]andnaturallaw777 92a9d2cfa1 Add .gitignore to exclude __pycache__, remove pyc files
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/5c173acb-776f-4cd2-bc89-bb7675e38677

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-02 17:02:28 +00:00
copilot-swe-agent[bot]andnaturallaw777 592faeecca Remove __pycache__ from tracking, add to .gitignore
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/5c173acb-776f-4cd2-bc89-bb7675e38677

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-02 17:02:00 +00:00
copilot-swe-agent[bot]andnaturallaw777 42900608f6 Replace GTK4 desktop app with FastAPI web app (Sovran_SystemsOS Hub)
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/5c173acb-776f-4cd2-bc89-bb7675e38677

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-02 17:01:42 +00:00
copilot-swe-agent[bot] af1ad09e2e Initial plan 2026-04-02 16:50:57 +00:00
95 changed files with 2862 additions and 13726 deletions

No files matched your search

-272
View File
@@ -1,272 +0,0 @@
# Changelog
All notable changes to Sovran_SystemsOS will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [Unreleased]
### Fixed
- Fix Bitcoin Knots → Bitcoin Core switch hanging in the Hub UI
- Sessions are now persisted to `/var/lib/secrets/hub-sessions.json` so the
browser login survives the Hub service restart that `nixos-rebuild switch`
performs during activation. Previously the in-memory session store was
wiped by that restart, the `/api/rebuild/status` poll started returning
401, and the rebuild modal spun forever showing "Applying changes…" while
the switch result was never displayed.
- Rebuild and update modals now bail out and reload the page after sustained
polling failures instead of hanging indefinitely.
- Rebuild/update scripts stream `nixos-rebuild` output into the live log
(it was buffered until completion, making long rebuilds look frozen).
---
## [1.1.0] - 2026-08-11
### Added
- Speed up Hub service status loading
- Update Documentation
- Njalla.nix: fail on ImportError; fix redundant except clause
- Correctly escape DDNS placeholder in Nix string
- Security hardening: fix all 8 blocking findings for PR #419
- Security hardening: fix DDNS injection, Nix injection, reboot auth, support key, sudo rules
- Harden btcpay and nbxplorer config handling
- Add provenance headers and fix flake checks for PR409
- Restore upstream nix-bitcoin fetchNodeModules for mempool and RTL packages
- Refactor Nix build configuration for mempool
- Update npm dependencies and patch handling in default.nix
- Update npmDepsHash for backend and frontend packages
- Mempool: use postPatch to copy lock file for npmDeps fetcher
- Mempool: use preBuild cd instead of postUnpack so npmDeps fetcher finds lock files
- Mempool: fix npmDeps fetcher — postUnpack instead of sourceRoot, postPatch instead of patches
- Mempool: switch backend and frontend to buildNpmPackage with placeholder npmDepsHash
- Update npmDepsHash with correct hash value
- RTL: switch to buildNpmPackage with placeholder npmDepsHash
- Add bitcoind.rpc.users.btcpayserver for NBXplorer
- Restore original flake.nix with nixosModules.Sovran_SystemsOS export
- Add vendored RTL package and fix rtl.nix to use it
- Vendor mempool packages and wire mempool module to vendored pkgs
- Refactor onion service configuration for LND
- Add missing bitcoind-rpc-public-whitelist.nix
- Remove joinmarket-ob-watcher from onion-services defaults
- Tailor: make bitcoin modules truly Sovran-only (lnd-only) and delete stubs.nix
- Vendor: replace nix-bitcoin flake input with minimal vendored modules (nixpkgs-only)
- Remove duplicate 1.0.6 release notes from CHANGELOG
### Changed
- Clean mempool module comment typo
- Move vendor/nix-bitcoin to modules/bitcoin, remove overlays
- Nix flake update - drop nix-bitcoin
### Fixed
- Correct RTL and Mempool Hub versions
- Fix DDNS URL validation: replace ${IP} temporarily for validator, keep placeholder for storage
- Fix all 8 security hardening blockers for PR #423
- Fix Nix interpolation in DDNS runner
- Fix IP validation in DDNS and document journalctl sudo rule
- Report configured BTCPay Server version
- Fix Matrix SIGPIPE and RTL v0.15.8 config schema regressions
- Add NBXplorer cookie auth and WorkingDirectory for BTCPay service
- Fix BTCPay startup regression: add home dirs to service users
- Address btcpay hardening review feedback
- Register bitcoin-HMAC-btcpayserver as managed secret owned by bitcoind user
- Fix RTL: use fetchNodeModules instead of npm ci in buildPhase
- Fix PostgreSQL ensureUsers: use ensureDBOwnership instead of ensureClauses
- Fix lnd macaroons: replace invalid 'enable' with 'user'
- Fix postgresql ensurePermissions -> ensureClauses for nixpkgs unstable
- Fix btcpayserver.nix to use pkgs.stable overlay
- Fix btcpayserver syntax error and pin to nixpkgs-stable (2.4.2)
- Fix mempool package: remove fetchNodeModules dependency
- Fix typo in onion-addresses service check
- Fix formatting of extraGroups in btcpayserver.nix
- Add missing semicolon after extraGroups in btcpayserver.nix
- Remove services.clightning.enable assignment that fails on f13ff45
- Remove clightning and clightning-rest from stubs to avoid duplicate on f13ff45
- Add stubs for nixpkgs-unstable 2026-08 where services.clightning removed
- Keep ISO artifacts out of the repo and auto-update README on release
[1.1.0]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.1.0
## [1.0.6] - 2026-08-07
### Added
- Iso: fix replaceStrings length mismatch in cleanVersion
- Promote virtual machine trial option in README
- Improve installer VM compatibility
- Trim redundant dev-vs-stable Gitea explanations in README
- Add Arean.Ai to AI tools used for development
- Prefix OS version badge with 'v' (v1.0.5)
- Match OS version badge styling to service modal version badges
- Place OS version badge inline right after the Hub title
- Center OS version badge under the Hub title
- Strengthen entropy section: add DYOR emphasis and hardware wallet verification note
- Add 'About Bitcoin wallet entropy' section to README
- README: balance intro between Bitcoin sovereignty and sovereign computing
### Changed
- Removed temp patch file
### Fixed
- Resolve tag range detection and improve release diagnostics
- Add token scope diagnostics and un-silence gh release errors
- Harden error handling and sanitize exception details across security endpoints (CWE-209)
- Sanitize exception handling in verify-integrity and security-reset (CWE-209)
- Sanitize api_security_reset errors to prevent exception information exposure (CWE-209)
- Use canonical prefix containment check for CodeQL path-injection
- Pass sanitized abs_path to os.chown to resolve CodeQL path injection at 4338
- Add CodeQL-recognized path sanitization for domain_name
- Remove domain substring check for CodeQL incomplete-url
- Use sentinel for njalla header check (CodeQL incomplete-url-substring)
- Prevent reflected XSS in lnurl-qr print endpoint
- Hash root password instead of storing in clear text (CWE-312)
- Separate web auth hash from system password file
- Fix Matrix Hub admin API credentials
- Fix CWE-78: replace subprocess call with Synapse Admin API in create-user endpoint
### Documentation
- Add SECURITY.md detailing security policy and best practices
- Versioned CDN downloads + add CDN upload script
- Correct active development workflow
- Make repo references mirror-neutral for Gitea readers
- Clarify GitHub is the dev mirror of the Gitea stable repo
[1.0.6]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.6
## [1.0.5] - 2026-08-04
### Added
- LND Update to v0.21.1-beta
- Nixpkgs Update
- Nixpkgs Update
- Update LND REST Zeus Connect instructions and design to match NWC exactly
- UI: Align LND REST and NWC Zeus connection instructions styling
- Make Zeus LND REST instructions coherent with NWC Zeus connect guide
- Clarify Zeus NWC wallet setup
- Make Manual Backup match the system role (Desktop Only scope)
- Reload Caddy immediately when a domain (or ACME email) is saved
- Unify Njal.la + router port-forwarding guidance across onboarding and feature enable modals
- Enhance Lightning Wallet Connect modal with professional benefits grid and refined messaging
### Fixed
- Resolve 'vdev' version badge and align it under the Hub title
- Align release script with Gitea staging-dev and stable branch workflow
- Auto-detect git remotes and clean up temp files in release-stable.sh
### Documentation
- Write real v1.0.4 changelog and auto-generate release notes in release script
[1.0.5]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.5
## [Unreleased]
### Fixed
- Report the version of the configured BTCPay Server package in the Hub instead of the different package version from unstable nixpkgs.
- Installer VM compatibility: legacy BIOS VM boots now install with GRUB, UEFI VM installs avoid depending on NVRAM boot-entry writes, VM users get clearer disk/resource guidance, and the internet check falls back to HTTPS when ICMP is blocked.
- **Manual Backup now matches the system role**: on the Desktop Only role the Hub's
"What gets backed up" list no longer shows Node / Server + Desktop items
(nix-bitcoin secrets, `/var/lib` system service data, and the database/blockchain
caveats), and the backup script skips those stages entirely — Desktop Only backups
now mirror only the NixOS configuration (`/etc/nixos`) and home directory (`/home`).
The free-space estimate, stage numbering, backup manifest, and completion message
are all role-aware; Node and Server + Desktop backups are unchanged.
---
## [1.0.4] - 2026-07-29
### Added
- **Lightning Wallet Connections (NWC)** — Hub-managed Nostr Wallet Connect powered by Alby Hub + LND:
- Create, view, and delete wallet connections directly from the Sovran Hub with a tabbed modal UI
- Downloadable/printable LNURL QR codes for connecting external wallets
- Channel liquidity guide and onboarding guidance for new LND nodes
- Unique-hostname enforcement with conflict validation and UI guidance
- Official NWC branding and logo
- **Version visibility across the Hub**:
- Sovran_SystemsOS version badge displayed under the Hub title
- Version numbers shown on all Hub service tiles and next to service modal titles
- Deployed PHP app versions surfaced in service titles
- Build-time version reference file (`VERSION`) so version lookups are instantaneous and consistent
- **Automated stable release workflow** (`scripts/release-stable.sh`) with versioned ISO naming from the `VERSION` file
- `CONTRIBUTING.md` and expanded project documentation
### Changed
- **Manual Backup overhauled**: replaced tar+DB+LND archive approach with a reliable ext4 + rsync workflow, including
mount checks, path safety, atomic completion markers, stale-marker cleanup, and behavioral test coverage
- **Port-forwarding UX simplified**: removed onboarding Step 4 and the misleading local "ready" status;
Njal.la DDNS now runs automatically when the feature is enabled
- README restructured for clarity, links, and accuracy; added router/ISP port-forwarding requirements
for Server + Desktop mode; acknowledged LiveKit and Alby Hub
- Updated nixpkgs and Bitcoin clients
- Repository cleanup: removed unused `.github`, `.tests`, `nix/`, and `docs/ai` directories
### Fixed
- NWC wallet certificate path wiring (now uses the nix-bitcoin LND cert path)
- Deterministic LND/Alby Hub port collision
- Alby Hub executable resolution and v1.23.0 patches regenerated against exact upstream source
- Manual Backup exit-code failures (bash/gawk in service PATH, tar tolerance, flock, browser-cache exclusions)
- rsync destination-directory failures in backups (auto `mkdir -p`, mount check, 19 behavioral tests)
- `sovran-hosts-update` converted to `writeShellApplication` with explicit runtime inputs
- Incorrect `lib.mkIf` usage in the NWC wallets module
- Duplicate systemd LND strings
- `git fetch` tag-clobber errors against Gitea (now uses `--force`)
### Security
- Hardened Lightning Wallet Connections (NWC): restricted `ReadOnlyPaths` for the unlock password,
fixed an Authorization-header bug, eliminated stack-trace exposure flagged by CodeQL,
and tightened credential access and amount validation
[1.0.4]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.4
## [1.0.3] - 2026-07-29
### Added
- Incremental stable updates
## [1.0.2] - 2026-07-29
### Added
- Incremental stable updates
## [1.0.1] - 2026-07-29
### Added
- Incremental stable updates
## [1.0.0] - 2026-07-29
### Added
- Initial stable release of Sovran_SystemsOS
- Full Bitcoin self-custody desktop environment
- Integrated Sparrow Wallet, Bisq, and Bisq 2
- Comprehensive Sovran Hub for service management
- NixOS-based operating system with privacy and sovereignty focus
- Support for Bitcoin Knots + BIP110, Electrs, LND, Ride The Lightning, BTCPay Server, and more
- Server + Desktop hybrid mode with Matrix, Nextcloud, VaultWarden, and other self-hosted services
- Automated installer with graphical GNOME desktop
- Tor integration and onion services for all major components
- Remote desktop (GNOME Remote Desktop) support
- Role-based system configuration (desktop, server, server+desktop)
- Detailed service credential management in the Sovran Hub
### Changed
- Moved from development/main branch to dedicated `stable` branch for production releases
### Security
- All services run with least-privilege principles where possible
- Secrets stored in `/var/lib/secrets/`
- Strong emphasis on user-controlled keys and self-sovereignty
---
## [Unreleased]
### Added
- (Nothing yet)
[1.0.3]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.3
[1.0.2]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.2
[1.0.1]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.1
[1.0.0]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.0
-112
View File
@@ -1,112 +0,0 @@
# Contributing to Sovran Systems OS
First off, thank you for considering contributing to Sovran_SystemsOS! 🎉
## Development and Release Repositories
The GitHub repository is the primary location for Sovran_SystemsOS development
and collaboration and serves as the project's connection to the GitHub
ecosystem. Most work is contributed through feature branches and pull requests
that target GitHub `main`. Development may also begin in a local branch or on
`staging-dev` at the project's self-hosted Gitea instance:
https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS.
GitHub `main` and Gitea `staging-dev` are kept synchronized as the active
development line, although one may briefly lead the other while changes are
being integrated. The canonical, release-ready code is maintained on Gitea's
[`stable` branch](https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/src/branch/stable),
in keeping with the self-sovereignty ethos of Sovran_SystemsOS.
The workflow is:
1. Most development and project collaboration — including issues, pull
requests, and reviews — happens through GitHub, with completed work
integrated into `main`.
2. Work may also originate locally or on Gitea `staging-dev`. Accepted changes
are synchronized between GitHub `main` and Gitea `staging-dev`.
3. When changes are complete and tested, they are promoted to Gitea `stable`,
which is the code used by released Sovran_SystemsOS builds.
Please note: GitHub `main` and Gitea `staging-dev` may contain new features and
code not yet in `stable`, and that code may not be fully tested.
Moreover, Sovran_SystemsOS has been improved with the help of AI. We have used Copilot and Arena.ai to work through significant coding challenges and troubleshooting hurdles. We will continue to use AI to help keep Sovran_SystemsOS stable and maintained.
## How Can I Contribute?
### 🐛 Reporting Bugs
- Open an [Issue](https://github.com/naturallaw777/Sovran_SystemsOS/issues)
- Use a clear and descriptive title
- Describe the steps to reproduce the bug
- Include your OS version, hardware specs, and any relevant logs
### 💡 Suggesting Features
- Open an [Issue](https://github.com/naturallaw777/Sovran_SystemsOS/issues) with the tag `enhancement`
- Explain the feature and why it would be useful
- Be as detailed as possible
### 🔧 Submitting Code Changes
#### 1. Fork the Repository
Click the "Fork" button at the top right of the GitHub repository.
#### 2. Clone Your Fork
```bash
git clone https://github.com/YOUR_USERNAME/Sovran_SystemsOS.git
cd Sovran_SystemsOS
```
#### 3. Create a Feature Branch
```bash
git checkout -b feature/your-feature-name
```
#### 4. Make Your Changes
- Write clean, well-commented code
- Follow the existing code style and structure
- Test your changes thoroughly
#### 5. Commit Your Changes
```bash
git add .
git commit -m "Brief description of your changes"
```
#### 6. Push to Your Fork
```bash
git push origin feature/your-feature-name
```
#### 7. Open a Pull Request
- Go to the original repository on GitHub
- Click "New Pull Request"
- Select your fork and branch
- Provide a clear title and description of your changes
- Reference any related Issues (e.g., "Closes #12")
## Pull Request Guidelines
- **One PR per feature/fix.** Do not bundle unrelated changes.
- **Keep PRs small and focused.** Smaller PRs are easier to review and merge.
- **Write meaningful commit messages.**
- **Do not push directly to `main`.** Always use a feature branch and open a PR.
- **Be patient.** PRs will be reviewed as soon as possible.
## Security-Sensitive Changes
- Report vulnerabilities privately as described in [`SECURITY.md`](SECURITY.md).
- Pin upstream revisions and hashes; do not use floating source references.
- Update [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) when importing code.
- Add regression tests and describe any threat-model change in the pull request.
## Code Style
- Follow the existing patterns in the codebase
- Comment your code where necessary
- Keep functions small and focused
## Code of Conduct
By participating in this project, you agree to be respectful and constructive.
We are building something meaningful — let's do it together with integrity.
+677 -492
View File
File diff suppressed because it is too large. Load diff
-101
View File
@@ -1,101 +0,0 @@
# Security Policy
## Supported versions
| Release | Supported |
|---|:---:|
| Latest `1.0.x` stable release | Yes |
| `main` / `staging-dev` | Development only |
| Older than `1.0.0` | No |
Install the newest stable point release to receive security fixes.
## Report a vulnerability
**Do not open a public issue or pull request.** Report privately through:
- [GitHub Private Vulnerability Reporting](https://github.com/naturallaw777/Sovran_SystemsOS/security/advisories/new)
- Email: [support@sovransystems.com](mailto:support@sovransystems.com)
Include the affected version, impact, reproduction steps, and a minimal proof of
concept. Never send wallet recovery words, private keys, or live credentials.
We aim to acknowledge reports within two business days. Please allow reasonable
time for a fix and coordinated disclosure.
## Security model
### Local-first operation
The Hub and core data run on operator-owned hardware. The Hub is intended for a
trusted local network and must not be port-forwarded to the internet. Public
services, DDNS, software updates, and optional third-party relays require
external networks and are outside a “fully offline” model.
The local Hub currently uses HTTP. Authentication does not encrypt local network
traffic, so use a trusted LAN and avoid public or guest Wi-Fi.
### In-repository Bitcoin stack
Bitcoin and Lightning modules are maintained under `modules/bitcoin/`. They were
adapted from nix-bitcoin, but Sovran builds do not import or fetch nix-bitcoin.
The `nix-bitcoin.*` option namespace and `/etc/nix-bitcoin-secrets` path remain
only for upgrade compatibility.
### Supply chain and integrity
`flake.lock` pins flake inputs, and fetched source archives use fixed hashes.
Builds still depend on pinned Nixpkgs, NixVim, btc-clients-nix, upstream source
archives, and any configured binary cache. Keeping the Bitcoin modules in this
repository reduces an external dependency; it does not remove supply-chain
risk.
The Hub integrity check verifies Nix store contents and compares the running
system with a build from local `/etc/nixos`. It does not authenticate the release
publisher or protect against an attacker who already controls root and can
change both the system and local configuration.
### Access and service isolation
- Firewall enabled by default
- Public SSH and remote desktop disabled by default
- Separate service users and systemd sandboxing where supported
- Administrative service ports bound to loopback where practical
- Tor enforced for supported Bitcoin traffic and onion services
- Public web services exposed only when enabled by the operator
Tor reduces network exposure for configured Bitcoin services. It is not a
guarantee against every IP leak, application bug, or traffic-analysis attack.
### Restricted support access
Support uses a per-session SSH key on the non-root `sovran-support` account.
Sessions expire after 24 hours and have a small allowlist of `sudo` commands.
Wallet paths receive deny ACLs unless the operator explicitly removes them.
Disabling support removes the key and reapplies the ACLs.
Support events are written to `/var/log/sovran-support-audit.log`. This is a
local audit log, not a cryptographically tamper-evident record.
## Out of scope
Sovran_SystemsOS cannot protect against:
- Compromised root or administrator credentials
- Stolen recovery words, private keys, or backups
- Malicious or compromised hardware, firmware, or build infrastructure
- Services the operator deliberately exposes or weakens
- Physical access without appropriate disk and firmware protections
## Operator basics
- Verify downloads and stop if the checksum does not match.
- Apply stable security updates promptly.
- Use unique passwords and keep SSH/RDP off when not needed.
- Prefer a well-reviewed hardware signer for meaningful Bitcoin balances.
- Keep tested, offline backups in separate secure locations.
- Never share recovery words or private keys with support.
- Disable support access when the session ends and review the audit log.
No software can provide absolute security. Review your configuration and threat
model before storing important funds or data.
-33
View File
@@ -1,33 +0,0 @@
# Third-Party Notices
## nix-bitcoin
Portions of `modules/bitcoin/` and selected package definitions were adapted
from [fort-nix/nix-bitcoin](https://github.com/fort-nix/nix-bitcoin), commit
[`360e30fee5ba32f9fecc89bc35628195d9d2dbbe`](https://github.com/fort-nix/nix-bitcoin/commit/360e30fee5ba32f9fecc89bc35628195d9d2dbbe).
The current Sovran build does not import or fetch nix-bitcoin.
MIT License
Copyright (c) 2019 nix-bitcoin developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Other packaged applications retain their own upstream licenses and copyright
notices.
-1
View File
@@ -1 +0,0 @@
1.1.0
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="120" fill="none"><g clip-path="url(#a)"><path fill="#472459" d="M127.2 65.67c-5.13 5.11-12.23 3.88-17.35-1.25l-41.4-41.6 19.4-19.47a11.35 11.35 0 0 1 18.4 3.37l21.58 56.22a2.5 2.5 0 0 1-.57 2.67l-13.63 13.57 13.58-13.51Z"/><path fill="url(#b)" d="m109.85 64.42-54.16-54.4a13.13 13.13 0 0 0-18.57-.05L3.87 43.07a13.13 13.13 0 0 0-.05 18.57l54.16 54.4a13.13 13.13 0 0 0 18.57.04l10.7-10.64 3.12-3.12-10-10.03a19.04 19.04 0 0 1-23.83-2.56l-6.8-6.85a2.46 2.46 0 0 1 0-3.5l3.35-3.32-8.4-8.46a3.67 3.67 0 0 1-.34-4.9 3.57 3.57 0 0 1 5.29-.23l8.51 8.56 6.68-6.63-8.41-8.46a3.67 3.67 0 0 1-.33-4.9 3.58 3.58 0 0 1 5.3-.24l8.5 8.57 3.36-3.34a2.47 2.47 0 0 1 3.5.01l6.8 6.84a19.03 19.03 0 0 1 2.41 23.86l10 10.03 5.69-5.67 8.16-8.12 17.4-17.31c-5.15 5.11-12.25 3.88-17.36-1.25Z"/></g><defs><linearGradient id="b" x1="63.6" x2="63.6" y1="6.15" y2="119.91" gradientUnits="userSpaceOnUse"><stop stop-color="#FFCA4A"/><stop offset="1" stop-color="#F7931A"/></linearGradient><clipPath id="a"><path fill="#fff" d="M0 0h128v119.91H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

+1 -21
View File
@@ -1,4 +1,4 @@
"""Load the Nix-generated config and build-time versions for Sovran_SystemsOS_Hub."""
"""Load the Nix-generated config for Sovran_SystemsOS_Hub."""
import json
import os
@@ -18,23 +18,3 @@ def load_config() -> dict:
return json.load(fh)
except (FileNotFoundError, json.JSONDecodeError):
return {"refresh_interval": 5, "command_method": "systemctl", "services": []}
def load_versions() -> dict:
"""Read the Nix-generated or fallback build-time package versions."""
path = os.environ.get("SOVRAN_HUB_VERSIONS")
if not path:
# Candidate 1: parent folder (same as config.json, typical for Nix layout)
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
candidate1 = os.path.join(parent_dir, "versions.json")
if os.path.exists(candidate1):
path = candidate1
else:
# Candidate 2: local package folder (dev folder structure)
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "versions.json")
try:
with open(path, "r") as fh:
return json.load(fh)
except (FileNotFoundError, json.JSONDecodeError):
return {}
-75
View File
@@ -1,75 +0,0 @@
"""
Structured audit logging for NWC wallet operations.
Writes append-only JSON lines to /var/log/sovran-nwc-audit.log.
Log file is owned by albyhub:albyhub with mode 0600.
"""
from __future__ import annotations
import json
import logging
import os
import threading
import time
from typing import Any
logger = logging.getLogger(__name__)
AUDIT_LOG_PATH = "/var/log/sovran-nwc-audit.log"
_AUDIT_LOCK = threading.Lock()
_initialized = False
def _ensure_log_file() -> None:
"""Ensure audit log file exists with correct permissions."""
global _initialized
if _initialized:
return
with _AUDIT_LOCK:
if _initialized:
return
try:
# Create directory if needed
os.makedirs(os.path.dirname(AUDIT_LOG_PATH), exist_ok=True)
# Create file if it doesn't exist
if not os.path.exists(AUDIT_LOG_PATH):
with open(AUDIT_LOG_PATH, "w") as f:
pass
# Set restrictive permissions
os.chmod(AUDIT_LOG_PATH, 0o600)
# Try to set ownership to albyhub user (best effort)
try:
import pwd
import grp
albyhub_uid = pwd.getpwnam("albyhub").pw_uid
albyhub_gid = grp.getgrnam("albyhub").gr_gid
os.chown(AUDIT_LOG_PATH, albyhub_uid, albyhub_gid)
except Exception:
pass # Best effort; may not have permissions
_initialized = True
except Exception as exc:
logger.warning("Failed to initialize audit log: %s", exc)
def audit_log(event: str, **fields: Any) -> None:
"""Write a structured audit log entry.
Args:
event: Event type identifier (e.g., "wallet_created", "invoice_issued")
**fields: Additional key-value fields to include in the log entry
"""
_ensure_log_file()
entry = {
"ts": time.time(),
"event": event,
**fields,
}
try:
with _AUDIT_LOCK:
with open(AUDIT_LOG_PATH, "a") as f:
f.write(json.dumps(entry, separators=(",", ":")) + "\n")
except Exception as exc:
logger.error("Failed to write audit log: %s", exc)
-881
View File
@@ -1,881 +0,0 @@
"""
Alby Hub manager — shared backend for Lightning Wallet Connections API and recovery CLI.
Interfaces with the local Alby Hub instance at
http://127.0.0.1:18080 by default (override with NWC_ALBY_HUB_API_BASE).
All sensitive values (passwords, bearer tokens, pairing URIs, macaroon
contents, Nostr private keys) are redacted from any exception messages
or log output.
"""
from __future__ import annotations
import json
import logging
import os
import re
import secrets
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any
from . import nwc_audit as _audit_mod
logger = logging.getLogger(__name__)
# ── Constants ──────────────────────────────────────────────────────
DEFAULT_API_BASE = os.environ.get(
"NWC_ALBY_HUB_API_BASE",
"http://127.0.0.1:18080",
)
DEFAULT_UNLOCK_PASSWORD_FILE = "/var/lib/albyhub/unlock-password"
DEFAULT_MACAROON_FILE = os.environ.get(
"NWC_LND_MACAROON_FILE", "/run/lnd/albyhub.macaroon"
)
DEFAULT_LND_ADDRESS = os.environ.get("NWC_LND_ADDRESS", "127.0.0.1:10009")
DEFAULT_LND_CERT_FILE = os.environ.get("NWC_LND_CERT_FILE", "/var/lib/lnd/tls.cert")
DEFAULT_LND_SOCKET = "/run/lnd/lnd.socket"
LNURL_DESCRIPTION_DEFAULT = "Pay via Lightning"
NWC_MIN_SENDABLE_MSAT = 1000
NWC_MAX_SENDABLE_MSAT = 1_000_000_000
# Metadata key used to mark managed isolated wallets
_MANAGED_APP_STORE_ID = "uncle-jim"
_MANAGED_META_KEY = "app_store_app_id"
RECEIVE_ONLY_SCOPES = [
"get_info",
"get_balance",
"make_invoice",
"lookup_invoice",
"list_transactions",
"notifications",
]
LIMITED_SEND_SCOPES = RECEIVE_ONLY_SCOPES + ["pay_invoice"]
# ── Exceptions ─────────────────────────────────────────────────────
class AlbyHubError(Exception):
"""Base error from the Alby Hub manager.
The message string is safe to surface to the user — it never
contains raw secret material.
"""
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
def __str__(self) -> str:
return f"[{self.code}] {self.args[0]}"
class AlbyHubHttpError(AlbyHubError):
def __init__(self, status_code: int, message: str) -> None:
super().__init__(f"http_{status_code}", message)
self.status_code = status_code
# ── Manager class ──────────────────────────────────────────────────
class AlbyHubManager:
"""Thread-safe manager for Alby Hub API operations."""
def __init__(
self,
api_base: str = DEFAULT_API_BASE,
unlock_password_file: str = DEFAULT_UNLOCK_PASSWORD_FILE,
macaroon_file: str = DEFAULT_MACAROON_FILE,
lnd_address: str = DEFAULT_LND_ADDRESS,
lnd_cert_file: str = DEFAULT_LND_CERT_FILE,
) -> None:
self.api_base = api_base.rstrip("/")
self.unlock_password_file = unlock_password_file
self.macaroon_file = macaroon_file
self.lnd_address = lnd_address
self.lnd_cert_file = lnd_cert_file
self._lock = threading.Lock()
self._token: str | None = None
# ── Low-level HTTP ─────────────────────────────────────────────
def _request(
self,
method: str,
path: str,
body: dict | None = None,
token: str | None = None,
timeout: int = 30,
) -> dict:
"""Make a raw HTTP request to the local Alby Hub API.
Returns the parsed JSON response body.
Raises AlbyHubHttpError on non-2xx responses.
Secrets in response bodies are never included in raised exceptions.
"""
url = f"{self.api_base}{path}"
data = json.dumps(body).encode("utf-8") if body is not None else None
headers: dict[str, str] = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if token:
headers["Authorization"] = "Bearer " + token
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read()
if not raw:
return {}
return json.loads(raw)
except urllib.error.HTTPError as exc:
code = exc.code
# Read and discard the body — we do NOT include it in the exception
try:
exc.read()
except Exception:
pass
raise AlbyHubHttpError(code, f"Hub API {method} {path} returned HTTP {code}") from None
except (urllib.error.URLError, TimeoutError, OSError):
raise AlbyHubError(
"hub_unreachable",
f"Hub API {method} {path} is unreachable",
) from None
def _authenticated_request(
self,
method: str,
path: str,
body: dict | None = None,
timeout: int = 30,
) -> dict:
"""Make an authenticated request; retry once with a fresh token on 401/403."""
token = self.ensure_ready()
try:
return self._request(method, path, body=body, token=token, timeout=timeout)
except AlbyHubHttpError as exc:
if exc.status_code in (401, 403):
with self._lock:
self._token = None
token = self.ensure_ready()
return self._request(method, path, body=body, token=token, timeout=timeout)
raise
def _paginate(self, path_template: str, page_size: int = 100) -> list[dict]:
"""Paginate a list API completely, collecting all items.
``path_template`` must contain ``{limit}`` and ``{offset}`` placeholders.
"""
token = self.ensure_ready()
offset = 0
results: list[dict] = []
while True:
path = path_template.format(limit=page_size, offset=offset)
page = self._request("GET", path, token=token)
# Alby Hub returns apps at the top level or under "apps"/"transactions"
total_count: int | None = None
if isinstance(page, list):
items = page
elif isinstance(page, dict):
items = page.get("apps") or page.get("transactions") or []
if page.get("totalCount") is not None:
total_count = int(page.get("totalCount"))
else:
items = []
if not isinstance(items, list):
break
results.extend(items)
if total_count is not None:
if len(results) >= total_count:
break
elif len(items) < page_size:
break
offset += page_size
return results
# ── Audit log helper ───────────────────────────────────────────
def _audit(self, event: str, **fields: Any) -> None:
"""Emit structured audit log entry."""
_audit_mod.audit_log(event, **fields)
# ── Startup / Auth ─────────────────────────────────────────────
def _read_unlock_password(self) -> str:
try:
with open(self.unlock_password_file, "r") as fh:
return fh.read().strip()
except OSError as exc:
raise AlbyHubError(
"unlock_password_unavailable",
"Cannot read Alby Hub unlock password",
) from exc
def _wait_for_file(self, path: str, timeout: int = 120) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if os.path.exists(path):
return
time.sleep(2)
raise AlbyHubError(
"dependency_unavailable",
f"Timed out waiting for required file: {path}",
)
def _wait_for_hub_api(self, timeout: int = 120) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
self._request("GET", "/api/info", timeout=5)
return
except AlbyHubError:
pass
time.sleep(3)
raise AlbyHubError("hub_unavailable", "Timed out waiting for Alby Hub API")
def _hub_setup(self, password: str) -> None:
"""Perform /api/setup idempotently."""
try:
info = self._request("GET", "/api/info", timeout=10)
if info.get("setupCompleted"):
return
except AlbyHubError:
pass
setup_body = {
"backendType": "LND",
"unlockPassword": password,
"lndAddress": self.lnd_address,
"lndCertFile": self.lnd_cert_file,
"lndMacaroonFile": self.macaroon_file,
}
try:
self._request("POST", "/api/setup", body=setup_body, timeout=30)
except AlbyHubHttpError as exc:
if exc.status_code == 409:
return # already setup
raise
def _obtain_token(self, password: str) -> str:
info = self._request("GET", "/api/info", timeout=10)
if info.get("running"):
resp = self._request(
"POST",
"/api/unlock",
body={
"unlockPassword": password,
"permission": "full",
},
timeout=30,
)
else:
resp = self._request(
"POST",
"/api/start",
body={"unlockPassword": password},
timeout=30,
)
token = (
resp.get("token")
or resp.get("accessToken")
or resp.get("access_token")
)
if not token or not isinstance(token, str):
raise AlbyHubError("auth_failed", "Alby Hub auth response missing token")
return token
def _wait_for_node_ready(self, token: str, timeout: int = 120) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
status = self._request(
"GET", "/api/node/status", token=token, timeout=10
)
if status.get("isReady") or status.get("running") or status.get("online"):
return
except AlbyHubError:
pass
time.sleep(3)
raise AlbyHubError("node_not_ready", "Timed out waiting for Alby Hub node to be ready")
def ensure_ready(self) -> str:
"""Ensure Alby Hub is set up, unlocked, and authenticated.
Returns a valid bearer token. Caches it and uses a lock to
prevent concurrent setup races.
"""
with self._lock:
if self._token:
return self._token
password = self._read_unlock_password()
self._wait_for_file(self.macaroon_file, timeout=120)
self._wait_for_hub_api(timeout=120)
self._hub_setup(password)
token = self._obtain_token(password)
self._wait_for_node_ready(token, timeout=120)
self._token = token
return token
# ── App isolation helpers ──────────────────────────────────────
@staticmethod
def _parse_metadata(raw: Any) -> dict:
if isinstance(raw, dict):
return raw
if isinstance(raw, str):
try:
result = json.loads(raw)
if isinstance(result, dict):
return result
except Exception:
pass
return {}
def _is_managed_app(self, app: dict) -> bool:
meta = self._parse_metadata(app.get("metadata"))
alias = str(meta.get("lnurl_alias", "")).strip().lower()
return (
meta.get(_MANAGED_META_KEY) == _MANAGED_APP_STORE_ID
and bool(alias)
)
def _app_to_wallet_meta(self, app: dict, domain: str | None) -> dict:
meta = self._parse_metadata(app.get("metadata"))
alias = meta.get("lnurl_alias", "")
address = f"{alias}@{domain}" if alias and domain else None
scopes = app.get("scopes") or []
access_preset = (
"send_receive_limited" if "pay_invoice" in scopes else "receive_only"
)
balance_msat = int(app.get("balanceMsat", 0) or 0)
balance_sats = balance_msat // 1000
dust_msat = balance_msat % 1000
spending_limit_sats: int | None = None
max_amount = app.get("maxAmountSat") or 0
if max_amount:
spending_limit_sats = int(max_amount)
# Count pending transactions from the budget or transactions list
pending_txs = int(app.get("pendingTransactionsCount", 0) or 0)
return {
"id": str(app.get("id", "")),
"pubkey": app.get("appPubkey") or app.get("nostrPubkey") or app.get("pubkey") or "",
"name": app.get("name", ""),
"alias": alias,
"lightning_address": address,
"access_preset": access_preset,
"spending_limit_sats": spending_limit_sats,
"balance_sats": balance_sats,
"dust_msat": dust_msat,
"pending_transactions": pending_txs,
"created_at": app.get("createdAt") or app.get("created_at"),
"min_sendable_msat": int(
meta.get("lnurl_min_sendable_msat", NWC_MIN_SENDABLE_MSAT)
),
"max_sendable_msat": int(
meta.get("lnurl_max_sendable_msat", NWC_MAX_SENDABLE_MSAT)
),
}
def _all_managed_apps(self) -> list[dict]:
apps = self._paginate("/api/apps?limit={limit}&offset={offset}&order_by=created_at")
return [a for a in apps if a.get("isolated") and self._is_managed_app(a)]
def _find_managed_app(self, identifier: str) -> dict | None:
needle = identifier.strip().lower()
for app in self._all_managed_apps():
if str(app.get("id", "")).lower() == needle:
return app
pubkey = (
app.get("appPubkey") or app.get("nostrPubkey") or app.get("pubkey") or ""
).lower()
if pubkey == needle:
return app
return None
# ── Public API ─────────────────────────────────────────────────
def list_wallets(self, domain: str | None = None) -> list[dict]:
"""Return all managed isolated app wallets (no secrets)."""
wallets = []
for app in self._all_managed_apps():
app_copy = dict(app)
app_copy["pendingTransactionsCount"] = len(self._get_app_pending_txs(int(app["id"])))
wallets.append(self._app_to_wallet_meta(app_copy, domain))
return wallets
def create_wallet(
self,
name: str,
alias: str,
access_preset: str,
spending_limit_sats: int | None,
domain: str | None = None,
) -> dict:
"""Create a new isolated Alby Hub app (wallet connection).
Returns a dict containing:
``wallet`` — safe metadata (no secrets)
``pairing_uri`` — real Alby Hub pairingUri (returned ONCE)
``result`` — creation status report
"""
# Validate uniqueness
managed = self._all_managed_apps()
for a in managed:
meta = self._parse_metadata(a.get("metadata"))
if meta.get("lnurl_alias", "").lower() == alias.lower():
raise AlbyHubError(
"alias_exists", "That Lightning Address alias is already in use."
)
if (a.get("name") or "").lower() == name.lower():
raise AlbyHubError(
"wallet_name_exists",
"That Wallet Connection name already exists.",
)
scopes = (
LIMITED_SEND_SCOPES
if access_preset == "send_receive_limited"
else RECEIVE_ONLY_SCOPES
)
max_amount = (
spending_limit_sats
if access_preset == "send_receive_limited" and spending_limit_sats
else 0
)
create_body: dict = {
"name": name,
"scopes": scopes,
"isolated": True,
"budgetRenewal": "never",
"maxAmountSat": max_amount,
"metadata": {
_MANAGED_META_KEY: _MANAGED_APP_STORE_ID,
"lnurl_alias": alias,
"lnurl_description": LNURL_DESCRIPTION_DEFAULT,
"lnurl_min_sendable_msat": NWC_MIN_SENDABLE_MSAT,
"lnurl_max_sendable_msat": NWC_MAX_SENDABLE_MSAT,
},
}
resp = self._authenticated_request("POST", "/api/apps", body=create_body)
pairing_uri: str = resp.get("pairingUri") or resp.get("pairing_uri") or ""
app_id = resp.get("id")
# Fetch full app details for accurate metadata
app_detail: dict | None = None
if app_id is not None:
try:
app_detail = self._authenticated_request(
"GET", f"/api/v2/apps/{app_id}"
)
except AlbyHubError:
pass
if app_detail is None:
# Fallback: search recent apps for the one we just created
updated = self._all_managed_apps()
for a in updated:
if str(a.get("id", "")) == str(app_id):
app_detail = a
break
wallet_meta = self._app_to_wallet_meta(app_detail or resp, domain)
# Initial internal transfer for limited wallets
funding_result: dict = {"attempted": False, "success": False}
if (
access_preset == "send_receive_limited"
and spending_limit_sats
and app_id is not None
):
funding_result["attempted"] = True
try:
self._authenticated_request(
"POST",
"/api/transfers",
body={
"toAppId": int(app_id),
"amountSat": spending_limit_sats,
"description": f"Initial funding for {name}",
},
)
funding_result["success"] = True
except AlbyHubError as exc:
funding_result["error"] = exc.code
funding_result["message"] = (
"The wallet was created successfully and the NWC connection secret is shown "
"above, but initial funding failed. Save the NWC secret now. "
"Do not recreate this wallet."
)
# Audit log: wallet created
self._audit(
"wallet_created",
wallet_id=str(app_id) if app_id else "unknown",
name=name,
alias=alias,
access_preset=access_preset,
spending_limit_sats=spending_limit_sats,
lightning_address=wallet_meta.get("lightning_address"),
funding_attempted=funding_result["attempted"],
funding_success=funding_result["success"],
)
return {
"wallet": wallet_meta,
"pairing_uri": pairing_uri, # returned once on create only
"result": {
"wallet_created": True,
"secret_created": bool(pairing_uri),
"lightning_address_registered": bool(alias and domain),
"funding": funding_result,
},
}
def _get_app_balance_msat(self, app: dict) -> int:
return int(app.get("balanceMsat", 0) or 0)
def _get_app_pending_txs(self, app_id: int) -> list[dict]:
txs = self._paginate(
f"/api/transactions?appId={app_id}&limit={{limit}}&offset={{offset}}"
)
return [
t for t in txs if str(t.get("state", "")).lower() == "pending"
]
def drain_wallet(self, identifier: str) -> dict:
"""Drain all whole-satoshi funds from an isolated app to the primary wallet.
Returns ``{"ok": True, "drained_sats": N, "dust_msat": M}``.
Raises AlbyHubError on rejection or failure.
"""
app = self._find_managed_app(identifier)
if app is None:
raise AlbyHubError("wallet_not_found", "Wallet connection not found.")
app_id = int(app["id"])
balance_msat = self._get_app_balance_msat(app)
if balance_msat < 0:
raise AlbyHubError("negative_balance", "Wallet has a negative balance.")
pending = self._get_app_pending_txs(app_id)
if pending:
raise AlbyHubError(
"pending_transactions",
"Wallet has pending transactions and cannot be drained.",
)
transferable_msat = (balance_msat // 1000) * 1000
expected_dust_msat = balance_msat - transferable_msat
if transferable_msat == 0:
return {"ok": True, "drained_sats": 0, "dust_msat": expected_dust_msat}
# Save original permissions
original_scopes = list(app.get("scopes") or [])
original_max = app.get("maxAmountSat") or 0
original_renewal = app.get("budgetRenewal") or "never"
# Temporarily grant pay_invoice scope with sufficient budget
app_pubkey = app.get("appPubkey") or app.get("nostrPubkey") or app.get("pubkey") or ""
if not app_pubkey:
raise AlbyHubError(
"app_pubkey_missing",
"Cannot drain app: app public key not available.",
)
patch_body = {
"scopes": sorted(set(original_scopes) | {"pay_invoice"}),
"maxAmountSat": 0,
"budgetRenewal": "never",
}
self._authenticated_request("PATCH", f"/api/apps/{app_pubkey}", body=patch_body)
drain_error: AlbyHubError | None = None
drained_sats = 0
try:
self._authenticated_request(
"POST",
"/api/transfers",
body={
"fromAppId": app_id,
"amountMsat": transferable_msat,
"description": f"Drain isolated subwallet {app.get('name', '')}",
},
)
drained_sats = transferable_msat // 1000
except AlbyHubError as exc:
drain_error = exc
finally:
# Restore original permissions whether drain succeeded or not
restore_body = {
"scopes": original_scopes,
"maxAmountSat": original_max,
"budgetRenewal": original_renewal,
}
try:
self._authenticated_request(
"PATCH", f"/api/apps/{app_pubkey}", body=restore_body
)
except AlbyHubError:
pass # best-effort restore; don't mask the original error
if drain_error is not None:
raise drain_error
# Verify remaining balance equals expected dust
refreshed = self._authenticated_request("GET", f"/api/v2/apps/{app_id}")
remaining_msat = self._get_app_balance_msat(refreshed)
if remaining_msat != expected_dust_msat:
raise AlbyHubError(
"drain_incomplete",
"Drain verification failed: final balance does not match expected dust.",
)
# Audit log: wallet drained
self._audit(
"wallet_drained",
wallet_id=str(app_id),
name=app.get("name", ""),
alias=app.get("alias", ""),
drained_sats=drained_sats,
dust_msat=expected_dust_msat,
)
return {
"ok": True,
"drained_sats": drained_sats,
"dust_msat": expected_dust_msat,
"remaining_msat": remaining_msat,
}
def delete_wallet(self, identifier: str) -> dict:
"""Safely drain and delete an isolated app.
Returns ``{"ok": True, "drained_sats": N}``.
"""
app = self._find_managed_app(identifier)
if app is None:
raise AlbyHubError("wallet_not_found", "Wallet connection not found.")
app_id = int(app["id"])
pending = self._get_app_pending_txs(app_id)
if pending:
raise AlbyHubError(
"pending_transactions",
"Wallet has pending transactions and cannot be deleted.",
)
drain_result = self.drain_wallet(identifier)
# Verify no transferable balance remains
refreshed = self._authenticated_request("GET", f"/api/v2/apps/{app_id}")
remaining_msat = self._get_app_balance_msat(refreshed)
if remaining_msat < 0:
raise AlbyHubError(
"negative_balance",
"Wallet has a negative final balance and cannot be deleted.",
)
if remaining_msat >= 1000:
raise AlbyHubError(
"drain_incomplete",
f"Drain verification failed: funds still remain.",
)
# Delete by app pubkey
pubkey = app.get("appPubkey") or app.get("nostrPubkey") or app.get("pubkey") or ""
if not pubkey:
raise AlbyHubError(
"app_pubkey_missing",
"Cannot delete app: nostr pubkey not available.",
)
self._authenticated_request(
"DELETE",
f"/api/apps/{urllib.parse.quote(pubkey, safe='')}",
)
# Audit log: wallet deleted
self._audit(
"wallet_deleted",
wallet_id=str(app_id),
name=app.get("name", ""),
alias=app.get("alias", ""),
drained_sats=drain_result.get("drained_sats", 0),
dust_msat=remaining_msat,
)
return {
"ok": True,
"drained_sats": drain_result.get("drained_sats", 0),
"dust_msat": remaining_msat,
}
def issue_invoice(
self, app_id: int, amount_msat: int, description: str = ""
) -> str:
"""Create an LND invoice attributed to a specific isolated app.
Returns a valid BOLT11 invoice string.
Raises AlbyHubError if the Hub returns an invalid or misattributed invoice.
"""
resp = self._authenticated_request(
"POST",
"/api/invoices",
body={
"amountMsat": amount_msat,
"description": description or LNURL_DESCRIPTION_DEFAULT,
"appId": app_id,
},
)
invoice: str = resp.get("invoice") or ""
returned_app_id = resp.get("appId")
if not invoice:
raise AlbyHubError("invoice_creation_failed", "Hub returned empty invoice.")
# Require a valid BOLT11 prefix (mainnet, testnet, signet, regtest)
if not re.match(r"^ln", invoice, re.IGNORECASE):
raise AlbyHubError(
"invalid_invoice", "Hub returned a non-BOLT11 invoice string."
)
if returned_app_id is None or int(returned_app_id) != app_id:
raise AlbyHubError(
"invoice_attribution_failed",
"Invoice attribution mismatch: returned appId does not match.",
)
# Audit log: invoice issued via API
self._audit(
"invoice_issued",
app_id=app_id,
amount_msat=amount_msat,
amount_sat=amount_msat // 1000,
invoice_prefix=invoice[:50] + "..." if len(invoice) > 50 else invoice,
)
return invoice
def find_app_by_alias(self, alias: str) -> dict | None:
"""Find a managed isolated app by its ``lnurl_alias`` metadata field."""
alias_lower = alias.strip().lower()
for a in self._all_managed_apps():
meta = self._parse_metadata(a.get("metadata"))
if meta.get("lnurl_alias", "").lower() == alias_lower:
return a
return None
def rotate_wallet_secret(self, identifier: str) -> dict:
"""Rotate the NWC pairing secret for a wallet connection.
Revokes the old Nostr key and generates a new pairing URI.
Returns the new pairing URI (shown ONCE).
"""
app = self._find_managed_app(identifier)
if app is None:
raise AlbyHubError("wallet_not_found", "Wallet connection not found.")
app_id = int(app["id"])
app_pubkey = app.get("appPubkey") or app.get("nostrPubkey") or app.get("pubkey") or ""
if not app_pubkey:
raise AlbyHubError(
"app_pubkey_missing",
"Cannot rotate secret: app public key not available.",
)
# Call Alby Hub's rotate secret endpoint (if available)
# Alby Hub may not have this endpoint yet; fall back to re-creating the app
# For now, we'll delete and re-create with same metadata
# This is a safe operation since we drain first
name = app.get("name", "")
alias = app.get("alias", "")
scopes = app.get("scopes") or []
max_amount = app.get("maxAmountSat") or 0
metadata = app.get("metadata") or {}
# Drain first
self.drain_wallet(identifier)
# Delete old app
self._authenticated_request(
"DELETE",
f"/api/apps/{urllib.parse.quote(app_pubkey, safe='')}",
)
# Create new app with same parameters
create_body: dict = {
"name": name,
"scopes": scopes,
"isolated": True,
"budgetRenewal": "never",
"maxAmountSat": max_amount,
"metadata": metadata,
}
resp = self._authenticated_request("POST", "/api/apps", body=create_body)
new_pairing_uri: str = resp.get("pairingUri") or resp.get("pairing_uri") or ""
new_app_id = resp.get("id")
# Audit log: secret rotated
self._audit(
"wallet_secret_rotated",
old_wallet_id=str(app_id),
new_wallet_id=str(new_app_id) if new_app_id else "unknown",
name=name,
alias=alias,
)
return {
"wallet_id": str(new_app_id) if new_app_id else "",
"pairing_uri": new_pairing_uri,
"message": "New NWC connection secret generated. Save it now — it will not be shown again.",
}
def health(self) -> dict:
"""Return a basic health summary."""
try:
token = self.ensure_ready()
status = self._request(
"GET", "/api/node/status", token=token, timeout=10
)
return {
"ok": True,
"hub_ready": bool(
status.get("isReady") or status.get("running")
),
}
except AlbyHubError as exc:
return {"ok": False, "error": exc.code, "message": str(exc)}
# ── Module-level singleton ──────────────────────────────────────────
_manager: AlbyHubManager | None = None
_manager_lock = threading.Lock()
def get_manager() -> AlbyHubManager:
"""Return the module-level singleton AlbyHubManager."""
global _manager
if _manager is None:
with _manager_lock:
if _manager is None:
_manager = AlbyHubManager()
return _manager
@@ -1,307 +0,0 @@
"""
Dedicated LNURL service for Lightning Wallet Connections.
Runs as ``nwc-lnurl.service`` on 127.0.0.1:8181 (loopback only).
Caddy proxies the public Lightning Address domain's LNURL routes to this port.
Routes:
GET /.well-known/lnurlp/{alias}
GET /lnurlp/{alias}/callback?amount=<msat>
All error responses are safe for public consumption — raw Alby Hub bodies
and internal credentials are never returned to callers.
"""
from __future__ import annotations
import json
import logging
import os
import re
import time
import urllib.parse
from collections import defaultdict
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import TYPE_CHECKING
from . import nwc_hub_manager as _mgr_mod
from . import nwc_audit as _audit_mod
if TYPE_CHECKING:
from .nwc_hub_manager import AlbyHubManager
logger = logging.getLogger(__name__)
# ── Configuration ─────────────────────────────────────────────────
LNURL_BIND_HOST = "127.0.0.1"
LNURL_PORT = int(os.environ.get("NWC_LNURL_PORT", "8181"))
DOMAIN_FILE = "/var/lib/domains/lightning"
NWC_ALIAS_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,31}$")
# Rate limiting configuration
RATE_LIMIT_WINDOW_SEC = 60
RATE_LIMIT_MAX_REQUESTS = 30
_rate_limit_buckets: dict[str, list[float]] = defaultdict(list)
# ── Helpers ───────────────────────────────────────────────────────
def _read_domain() -> str | None:
try:
with open(DOMAIN_FILE, "r") as fh:
raw = fh.read(256).strip().lower()
# Strict FQDN validation: must be a valid hostname with at least one dot
# Reject localhost, IP addresses, and single-label names
if not re.match(r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$", raw):
return None
# Explicitly reject local/reserved names
if raw in {"localhost", "localhost.localdomain", "local"}:
return None
return raw
except OSError:
pass
return None
def _check_rate_limit(client_ip: str) -> bool:
"""Check and update rate limit bucket for client IP. Returns True if allowed."""
now = time.monotonic()
bucket = _rate_limit_buckets[client_ip]
# Prune old entries
cutoff = now - RATE_LIMIT_WINDOW_SEC
while bucket and bucket[0] < cutoff:
bucket.pop(0)
if len(bucket) >= RATE_LIMIT_MAX_REQUESTS:
return False
bucket.append(now)
return True
def _lnurl_discovery(alias: str, manager: "AlbyHubManager", client_ip: str = "") -> tuple[dict, int]:
alias = alias.strip().lower()
if not NWC_ALIAS_RE.match(alias):
return {"status": "ERROR", "reason": "Unknown Lightning Address alias"}, 404
domain = _read_domain()
if not domain:
return {"status": "ERROR", "reason": "Lightning domain is not configured"}, 503
try:
app = manager.find_app_by_alias(alias)
except _mgr_mod.AlbyHubError:
return {"status": "ERROR", "reason": "Service temporarily unavailable"}, 503
if app is None:
return {"status": "ERROR", "reason": "Unknown Lightning Address alias"}, 404
meta = _mgr_mod.AlbyHubManager._parse_metadata(app.get("metadata"))
min_sendable = int(
meta.get("lnurl_min_sendable_msat", _mgr_mod.NWC_MIN_SENDABLE_MSAT)
)
max_sendable = int(
meta.get("lnurl_max_sendable_msat", _mgr_mod.NWC_MAX_SENDABLE_MSAT)
)
callback_alias = urllib.parse.quote(alias, safe="")
callback = f"https://{domain}/lnurlp/{callback_alias}/callback"
description = meta.get("lnurl_description") or f"Pay {alias}"
metadata = json.dumps([["text/plain", description]], separators=(",", ":"))
# Audit log: LNURL discovery
_audit_mod.audit_log(
"lnurl_discovery",
alias=alias,
domain=domain,
client_ip=client_ip,
min_sendable_msat=min_sendable,
max_sendable_msat=max_sendable,
)
return {
"tag": "payRequest",
"callback": callback,
"minSendable": min_sendable,
"maxSendable": max_sendable,
"metadata": metadata,
"commentAllowed": 0,
}, 200
def _lnurl_callback(
alias: str, amount_str: str | None, manager: "AlbyHubManager", client_ip: str = ""
) -> tuple[dict, int]:
payload, status_code = _lnurl_discovery(alias, manager, client_ip)
if status_code != 200:
return payload, status_code
if amount_str is None:
return {"status": "ERROR", "reason": "Missing amount parameter"}, 400
if not re.match(r"^\d+$", amount_str):
return {
"status": "ERROR",
"reason": "Amount must be an integer millisatoshi value",
}, 400
amount_msat = int(amount_str)
min_sendable = int(payload["minSendable"])
max_sendable = int(payload["maxSendable"])
if amount_msat < min_sendable:
return {
"status": "ERROR",
"reason": "Amount is below the minimum sendable value",
}, 400
if amount_msat > max_sendable:
return {
"status": "ERROR",
"reason": "Amount is above the maximum sendable value",
}, 400
if amount_msat % 1000 != 0:
return {
"status": "ERROR",
"reason": "Amount must be a whole-satoshi value",
}, 400
try:
app = manager.find_app_by_alias(alias)
except _mgr_mod.AlbyHubError:
return {"status": "ERROR", "reason": "Service temporarily unavailable"}, 503
if app is None:
return {"status": "ERROR", "reason": "Unknown Lightning Address alias"}, 404
meta = _mgr_mod.AlbyHubManager._parse_metadata(app.get("metadata"))
description = meta.get("lnurl_description") or f"Pay {alias}"
try:
app_id = int(app["id"])
invoice = manager.issue_invoice(app_id, amount_msat, description)
except _mgr_mod.AlbyHubError:
return {"status": "ERROR", "reason": "Invoice creation failed"}, 502
# Audit log: Invoice generated via LNURL
_audit_mod.audit_log(
"lnurl_invoice_created",
alias=alias,
amount_msat=amount_msat,
amount_sat=amount_msat // 1000,
client_ip=client_ip,
invoice_prefix=invoice[:50] + "..." if len(invoice) > 50 else invoice,
)
return {"pr": invoice, "routes": []}, 200
# ── HTTP server ───────────────────────────────────────────────────
def _make_handler(manager: "AlbyHubManager") -> type:
"""Return a handler class bound to the given manager."""
class LnurlHandler(BaseHTTPRequestHandler):
_manager = manager
def log_message(self, fmt: str, *args: object) -> None:
logger.debug(f"LNURL {self.address_string()} {fmt % args}")
def _send_json(self, status: int, body: dict) -> None:
raw = json.dumps(body, separators=(",", ":")).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
def _get_client_ip(self) -> str:
# Check X-Forwarded-For header (set by Caddy)
forwarded = self.headers.get("X-Forwarded-For")
if forwarded:
# Take the first IP in the chain
return forwarded.split(",")[0].strip()
# Fallback to direct connection IP
return self.client_address[0]
def _check_rate_limit(self) -> bool:
client_ip = self._get_client_ip()
if not _check_rate_limit(client_ip):
self._send_json(429, {
"status": "ERROR",
"reason": "Rate limit exceeded. Please slow down."
})
_audit_mod.audit_log(
"rate_limit_exceeded",
client_ip=client_ip,
path=self.path,
)
return False
return True
def do_GET(self) -> None: # noqa: N802
if not self._check_rate_limit():
return
parsed = urllib.parse.urlparse(self.path)
path = parsed.path
qs = urllib.parse.parse_qs(parsed.query)
client_ip = self._get_client_ip()
# /.well-known/lnurlp/{alias}
m = re.fullmatch(
r"/.well-known/lnurlp/([^/]+)", path
)
if m:
alias = urllib.parse.unquote(m.group(1))
payload, code = _lnurl_discovery(alias, self._manager, client_ip)
self._send_json(code, payload)
return
# /lnurlp/{alias}/callback
m = re.fullmatch(r"/lnurlp/([^/]+)/callback", path)
if m:
alias = urllib.parse.unquote(m.group(1))
amount_values = qs.get("amount")
if not amount_values:
amount_str = None
elif len(amount_values) != 1:
self._send_json(
400,
{
"status": "ERROR",
"reason": "A single amount parameter is required",
},
)
return
else:
amount_str = amount_values[0]
payload, code = _lnurl_callback(alias, amount_str, self._manager, client_ip)
self._send_json(code, payload)
return
self._send_json(404, {"status": "ERROR", "reason": "Not found"})
return LnurlHandler
def run(
host: str = LNURL_BIND_HOST,
port: int = LNURL_PORT,
manager: "AlbyHubManager | None" = None,
) -> None:
"""Start the blocking LNURL HTTP server."""
if manager is None:
manager = _mgr_mod.get_manager()
handler_class = _make_handler(manager)
server = HTTPServer((host, port), handler_class)
logger.info("nwc-lnurl service listening on %s:%d", host, port)
server.serve_forever()
def main() -> None:
logging.basicConfig(level=logging.INFO)
run()
if __name__ == "__main__":
main()
-131
View File
@@ -1,131 +0,0 @@
from __future__ import annotations
import argparse
import json
import sys
from . import nwc_hub_manager as _mgr_mod
from .server import _nwc_domain, _nwc_validate_alias, _nwc_test_address
def _print(data) -> None:
print(json.dumps(data, indent=2, sort_keys=True))
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="nwc-wallet")
sub = parser.add_subparsers(dest="cmd", required=True)
create = sub.add_parser("create")
create.add_argument("name")
create.add_argument("alias")
preset_group = create.add_mutually_exclusive_group()
preset_group.add_argument("--receive-only", action="store_true")
preset_group.add_argument("--limit-sats", type=int)
sub.add_parser("list")
drain = sub.add_parser("drain")
drain.add_argument("wallet")
delete = sub.add_parser("delete")
delete.add_argument("wallet")
addr = sub.add_parser("address")
addr_sub = addr.add_subparsers(dest="address_cmd", required=True)
addr_show = addr_sub.add_parser("show")
addr_show.add_argument("alias")
rotate = sub.add_parser("rotate")
rotate.add_argument("wallet")
sub.add_parser("health")
args = parser.parse_args(argv)
manager = _mgr_mod.get_manager()
domain = _nwc_domain()
if args.cmd == "list":
try:
wallets = manager.list_wallets(domain)
except _mgr_mod.AlbyHubError as exc:
print(f"Error: {exc.code} - {exc}", file=sys.stderr)
return 1
_print({"wallets": wallets})
return 0
if args.cmd == "health":
result = manager.health()
_print(result)
return 0 if result.get("ok") else 1
if args.cmd == "address" and args.address_cmd == "show":
alias = args.alias.strip().lower()
test = _nwc_test_address(alias)
_print(test)
return 0 if test.get("ok") else 1
if args.cmd == "drain":
try:
result = manager.drain_wallet(args.wallet)
except _mgr_mod.AlbyHubError as exc:
print(f"Error: {exc.code} - {exc}", file=sys.stderr)
return 1
_print(result)
return 0
if args.cmd == "delete":
try:
result = manager.delete_wallet(args.wallet)
except _mgr_mod.AlbyHubError as exc:
print(f"Error: {exc.code} - {exc}", file=sys.stderr)
return 1
_print(result)
return 0
if args.cmd == "rotate":
try:
result = manager.rotate_wallet_secret(args.wallet)
except _mgr_mod.AlbyHubError as exc:
print(f"Error: {exc.code} - {exc}", file=sys.stderr)
return 1
_print({
"wallet_id": result.get("wallet_id", ""),
"pairing_uri": result.get("pairing_uri", ""),
"message": result.get("message", "New NWC connection secret generated. Save it now — it will not be shown again."),
})
return 0
if args.cmd == "create":
alias = args.alias.strip().lower()
if not _nwc_validate_alias(alias):
print("Error: alias_invalid - Alias must be lowercase letters, digits, '_' or '-'.", file=sys.stderr)
return 1
access_preset = "send_receive_limited" if args.limit_sats is not None else "receive_only"
try:
result = manager.create_wallet(
args.name.strip(),
alias,
access_preset,
args.limit_sats if access_preset == "send_receive_limited" else None,
domain,
)
except _mgr_mod.AlbyHubError as exc:
print(f"Error: {exc.code} - {exc}", file=sys.stderr)
return 1
# Print the pairing URI once — this is the only time it is shown
_print(
{
"wallet": result["wallet"],
"pairing_uri": result.get("pairing_uri", ""),
"message": "Keep the NWC connection secret private. It cannot be displayed again.",
"result": result.get("result", {}),
}
)
return 0
return 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,29 +1,12 @@
#!/usr/bin/env bash
# ── Sovran Hub External Backup Script ────────────────────────────
# Backs up Sovran_SystemsOS data to an external USB hard drive using rsync.
# Backs up Sovran_SystemsOS data to an external USB hard drive.
# Designed for the Hub web UI (no GUI dependencies).
#
# On Server + Desktop and Node systems, your Sovran Pro already backs up
# your data automatically to its internal second drive (BTCEcoandBackup at
# /run/media/Second_Drive); this script stores a copy in a third location.
# Desktop Only systems have no internal second drive, so the external copy
# is the second location.
#
# What gets mirrored depends on the system role:
# - Node / Server + Desktop: /etc/nixos, /etc/nix-bitcoin-secrets,
# /home, and /var/lib (minus databases, blockchain data, logs, caches).
# - Desktop Only: /etc/nixos and /home. Desktop Only runs no server or
# Bitcoin services, so there are no nix-bitcoin secrets or system
# service data to back up.
#
# The external drive must be formatted as ext4. Files are stored as
# directly browsable files under Sovran_SystemsOS_Backup/current/.
# Later runs update the same mirror and only transfer changed or new
# files, making repeat backups fast.
#
# PostgreSQL and MariaDB/MySQL databases are NOT included. Bitcoin
# blockchain and Electrs index data are NOT included (they live on
# the internal second drive).
# Your Sovran Pro already backs up your data automatically to its
# internal second drive (BTCEcoandBackup at /run/media/Second_Drive).
# This script creates an additional copy on an external USB drive —
# storing your data in a third location for maximum protection.
#
# Usage:
# BACKUP_TARGET=/run/media/<user>/<drive> bash sovran-hub-backup.sh
@@ -34,28 +17,18 @@ set -euo pipefail
BACKUP_LOG="/var/log/sovran-hub-backup.log"
BACKUP_STATUS="/var/log/sovran-hub-backup.status"
MEDIA_ROOT="/run/media"
MIN_FREE_GB=10
HUB_CONFIG_JSON="/var/lib/sovran-hub/config.json"
ROLE_STATE_NIX="/etc/nixos/role-state.nix"
SECOND_DRIVE_MOUNT="/run/media/Second_Drive"
SAFETY_MARGIN_BYTES=$((1024 * 1024 * 1024))
# ── Internal drive labels/paths to NEVER use as backup targets ───
INTERNAL_LABELS=("BTCEcoandBackup" "sovran_systemsos")
INTERNAL_MOUNTS=("$SECOND_DRIVE_MOUNT" "/boot/efi" "/")
FAILED_ALREADY=0
BACKUP_COMPLETE=0
RSYNC_WARNINGS=()
# Stable rsync mirror sub-path under the target drive. Not timestamped
# so later runs update the same destination and only transfer new or changed files.
BACKUP_SUBPATH="Sovran_SystemsOS_Backup/current"
INTERNAL_MOUNTS=("/run/media/Second_Drive" "/boot/efi" "/")
# ── Logging helpers ──────────────────────────────────────────────
log() {
local msg
msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$BACKUP_LOG"
}
@@ -64,47 +37,16 @@ set_status() {
}
fail() {
FAILED_ALREADY=1
log "ERROR: $*"
set_status "FAILED"
exit 1
}
cleanup() {
local rc=$?
# Release the concurrency lock file descriptor if it was opened
if [[ -n "${LOCK_FD:-}" ]]; then
exec {LOCK_FD}>&- 2>/dev/null || true
fi
if [[ "$BACKUP_COMPLETE" -eq 1 && "$rc" -eq 0 ]]; then
return
fi
if [[ "$FAILED_ALREADY" -eq 0 ]]; then
log "ERROR: Backup terminated unexpectedly (exit code $rc)."
set_status "FAILED"
fi
# Mark the backup directory as incomplete so failed runs are identifiable
if [[ -n "${BACKUP_DIR:-}" && -d "${BACKUP_DIR:-}" && ! -f "${BACKUP_DIR:-}/BACKUP_COMPLETE" ]]; then
touch "${BACKUP_DIR}/INCOMPLETE" 2>/dev/null || true
fi
}
trap cleanup EXIT
trap 'exit 1' INT TERM
require_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || fail "Required command not found: $cmd"
}
# ── Check whether a mount point is an internal drive ────────────
is_internal() {
local mnt="$1"
# Reject known internal mount points and their subdirectories
for internal in "${INTERNAL_MOUNTS[@]}"; do
if [[ "$mnt" == "$internal" || "$mnt" == "${internal}/"* ]]; then
return 0
@@ -117,37 +59,39 @@ is_internal() {
find_external_drive() {
local target=""
# lsblk JSON output: NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE
if command -v lsblk &>/dev/null; then
while IFS=$'\t' read -r dev_type hotplug removable label mountpoint; do
# Must be a partition or disk, and be removable/hotplug
[[ "$dev_type" == "part" || "$dev_type" == "disk" ]] || continue
[[ "$hotplug" == "1" || "$removable" == "1" ]] || continue
[[ -n "$mountpoint" ]] || continue
while IFS=$'\t' read -r dev_type hotplug removable label mountpoint; do
[[ "$dev_type" == "part" || "$dev_type" == "disk" ]] || continue
[[ "$hotplug" == "1" || "$removable" == "1" ]] || continue
[[ -n "$mountpoint" ]] || continue
# Filter out internal labels
local skip=0
for lbl in "${INTERNAL_LABELS[@]}"; do
[[ "$label" == "$lbl" ]] && skip=1 && break
done
[[ "$skip" -eq 1 ]] && continue
local skip=0
for lbl in "${INTERNAL_LABELS[@]}"; do
[[ "$label" == "$lbl" ]] && skip=1 && break
done
[[ "$skip" -eq 1 ]] && continue
# Filter out internal mount points
is_internal "$mountpoint" && continue
is_internal "$mountpoint" && continue
if mountpoint -q "$mountpoint" 2>/dev/null; then
target="$mountpoint"
break
fi
done < <(lsblk -J -o NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE 2>/dev/null | \
python3 -c "
if mountpoint -q "$mountpoint" 2>/dev/null; then
target="$mountpoint"
break
fi
done < <(lsblk -J -o NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE 2>/dev/null | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
def flatten(devs):
for d in devs:
yield d
for c in d.get('children', []):
yield from flatten([c])
data = json.load(sys.stdin)
for d in flatten(data.get('blockdevices', [])):
print('\\t'.join([
print('\t'.join([
d.get('type') or '',
str(d.get('hotplug') or '0'),
str(d.get('rm') or '0'),
@@ -155,10 +99,24 @@ for d in flatten(data.get('blockdevices', [])):
d.get('mountpoint') or '',
]))
" 2>/dev/null || true)
fi
# Fallback: walk /run/media/ if lsblk produced nothing
if [[ -z "$target" && -d "$MEDIA_ROOT" ]]; then
while IFS= read -r -d '' mnt; do
is_internal "$mnt" && continue
# Check label via lsblk on the device backing this mount
local dev
dev=$(findmnt -n -o SOURCE "$mnt" 2>/dev/null || true)
if [[ -n "$dev" ]]; then
local lbl
lbl=$(lsblk -n -o LABEL "$dev" 2>/dev/null || true)
local skip=0
for internal_lbl in "${INTERNAL_LABELS[@]}"; do
[[ "$lbl" == "$internal_lbl" ]] && skip=1 && break
done
[[ "$skip" -eq 1 ]] && continue
fi
if mountpoint -q "$mnt" 2>/dev/null; then
target="$mnt"
break
@@ -170,10 +128,16 @@ for d in flatten(data.get('blockdevices', [])):
}
# ── Detect the configured system role ───────────────────────────
#
# Priority:
# 1. Hub config JSON (/var/lib/sovran-hub/config.json) — "role" key
# 2. role-state.nix (/etc/nixos/role-state.nix) — grep for true flag
# 3. Default: server_plus_desktop
detect_role() {
local role="server_plus_desktop"
# 1. Try the Hub config JSON
if [[ -f "$HUB_CONFIG_JSON" ]] && command -v python3 &>/dev/null; then
local r
r=$(python3 -c \
@@ -185,6 +149,7 @@ detect_role() {
fi
fi
# 2. Fall back to parsing role-state.nix
if [[ -f "$ROLE_STATE_NIX" ]]; then
if grep -q 'roles\.desktop = lib\.mkDefault true' "$ROLE_STATE_NIX" 2>/dev/null; then
role="desktop"
@@ -196,119 +161,6 @@ detect_role() {
echo "$role"
}
validate_target_mount() {
local target="$1"
[[ "$target" == "${MEDIA_ROOT}/"* ]] || fail "Target '$target' must be mounted under $MEDIA_ROOT."
[[ -d "$target" ]] || fail "Target path '$target' does not exist."
mountpoint -q "$target" || fail "Target path '$target' is not a mount point."
local fstype=""
fstype=$(findmnt -n -o FSTYPE -T "$target" 2>/dev/null || true)
[[ -n "$fstype" ]] || fail "Could not determine filesystem type for '$target'."
if [[ "$fstype" != "ext4" ]]; then
fail "Target '$target' must be formatted as ext4 (detected filesystem: $fstype). Manual Backup requires an ext4-formatted external drive for Linux metadata preservation. exFAT, FAT32, and NTFS are not supported."
fi
local write_test
write_test="$target/.sovran-write-test-$$"
if ! ( : > "$write_test" && echo "ok" >> "$write_test" && rm -f "$write_test" ); then
fail "Target '$target' is not writable."
fi
log "Verified backup target filesystem: $fstype"
}
estimate_path_bytes() {
local path="$1"
shift || true
[[ -e "$path" ]] || {
echo 0
return
}
local size
size=$(du -s -B1 -x "$@" "$path" 2>/dev/null | awk '{print $1}' || true)
[[ -n "$size" ]] || size=0
echo "$size"
}
# ── Sync one source tree to its backup destination ───────────────
# Usage: sync_tree <label> <allow_vanished> <source> <destination> [rsync options...]
#
# allow_vanished: "yes" means rsync exit 24 (vanished files) is nonfatal.
# Used for /home only — files may disappear while the desktop is active.
# All other nonzero exit codes are always fatal.
#
# Before every rsync call this helper:
# 1. Re-verifies $TARGET is still a mount point (fails if drive disconnected).
# 2. Verifies the destination path remains beneath $BACKUP_DIR and $BACKUP_DIR
# remains beneath $TARGET (safe-path check).
# 3. Creates the full destination directory hierarchy with mkdir -p so that
# rsync never fails trying to create a directory whose parent is absent.
sync_tree() {
local label="$1"
local allow_vanished="$2"
local source="$3"
local destination="$4"
shift 4
# Remaining "$@" are rsync options (--exclude, etc.)
# ── Re-verify the external drive is still mounted ────────────────
mountpoint -q "$TARGET" 2>/dev/null || \
fail "Stage $label: external drive '$TARGET' is no longer mounted. Refusing to write."
# ── Verify path safety ────────────────────────────────────────────
# BACKUP_DIR must remain beneath TARGET.
case "$BACKUP_DIR" in
"$TARGET"/*) ;;
*) fail "Stage $label: BACKUP_DIR '$BACKUP_DIR' is outside TARGET '$TARGET'." ;;
esac
# Destination must remain beneath BACKUP_DIR.
case "$destination" in
"$BACKUP_DIR"/*|"$BACKUP_DIR") ;;
*) fail "Stage $label: destination '$destination' is outside BACKUP_DIR '$BACKUP_DIR'. Refusing to write." ;;
esac
# ── Create complete destination directory hierarchy ───────────────
# This is the fix for the production failure:
# rsync: [Receiver] mkdir ".../current/etc/nixos" failed: No such file or directory
# mkdir -p creates all intermediate parents (e.g. current/etc/) before rsync runs.
mkdir -p -- "$destination" || \
fail "Stage $label: failed to create destination directory '$destination' (source: '$source')."
local rsync_err_tmp
rsync_err_tmp="$(mktemp /tmp/sovran-rsync-err.XXXXXX)"
local rc=0
rsync \
--archive \
--acls \
--xattrs \
--hard-links \
--numeric-ids \
--one-file-system \
--partial \
"$@" "$source" "$destination" 2>"$rsync_err_tmp" || rc=$?
if [[ -s "$rsync_err_tmp" ]]; then
while IFS= read -r rline; do
log "rsync: $rline"
done < "$rsync_err_tmp"
fi
rm -f "$rsync_err_tmp"
if [[ "$rc" -eq 0 ]]; then
return 0
elif [[ "$allow_vanished" == "yes" && "$rc" -eq 24 ]]; then
log "NOTE: $label — some files vanished during sync (normal on an active desktop). Your important data is backed up."
RSYNC_WARNINGS+=("$label: some files vanished during sync (rsync exit 24 — normal on active desktop)")
return 0
else
fail "rsync failed for $label (exit code $rc). See the rsync errors above."
fi
}
# ── Initialise log file ──────────────────────────────────────────
: > "$BACKUP_LOG"
@@ -317,58 +169,22 @@ set_status "RUNNING"
log "=== Sovran_SystemsOS External Hub Backup ==="
log "Starting backup process…"
# ── Acquire exclusive run lock ────────────────────────────────────
# Prevents two simultaneous backup runs (e.g. from double-click or
# stale RUNNING status after a Hub restart).
LOCK_FILE="/var/lock/sovran-hub-backup.lock"
# Note: exec {LOCK_FD}>>file requires bash 4.1+ (NixOS provides bash 5.x).
exec {LOCK_FD}>>"$LOCK_FILE" 2>/dev/null || \
fail "Cannot open lock file: $LOCK_FILE. Ensure /var/lock is writable."
flock --nonblock "$LOCK_FD" 2>/dev/null || \
fail "Another backup is already running. Wait for it to complete or check $BACKUP_STATUS."
require_cmd rsync
require_cmd findmnt
require_cmd lsblk
require_cmd mountpoint
require_cmd df
require_cmd du
require_cmd awk
require_cmd find
require_cmd hostname
require_cmd date
require_cmd python3
require_cmd flock
# ── Detect system role ───────────────────────────────────────────
ROLE="$(detect_role)"
case "$ROLE" in
desktop) ROLE_LABEL="Desktop Only" ;;
node) ROLE_LABEL="Node (Bitcoin-only)" ;;
server_plus_desktop) ROLE_LABEL="Server + Desktop" ;;
*) ROLE_LABEL="$ROLE" ;;
desktop) ROLE_LABEL="Desktop Only" ;;
node) ROLE_LABEL="Node (Bitcoin-only)" ;;
server_plus_desktop) ROLE_LABEL="Server + Desktop" ;;
*) ROLE_LABEL="$ROLE" ;;
esac
log "Detected role: $ROLE_LABEL"
# Backup scope depends on the role. Desktop Only systems run no server or
# Bitcoin services, so only the NixOS configuration and home directory are
# mirrored (2 stages). Node and Server + Desktop systems also mirror the
# nix-bitcoin secrets and /var/lib system service data (4 stages).
if [[ "$ROLE" == "desktop" ]]; then
TOTAL_STAGES=2
HOME_STAGE_NUM=2
log "Desktop Only role: backing up the NixOS configuration (/etc/nixos) and home directory (/home) only."
else
TOTAL_STAGES=4
HOME_STAGE_NUM=3
fi
# ── Detect target drive ──────────────────────────────────────────
if [[ -n "${BACKUP_TARGET:-}" ]]; then
TARGET="$BACKUP_TARGET"
# Safety: never allow internal drives even if explicitly passed
if is_internal "$TARGET"; then
fail "Target '$TARGET' is an internal system drive and cannot be used for external backup."
fi
@@ -377,259 +193,129 @@ else
log "Auto-detecting external USB drives…"
TARGET="$(find_external_drive)"
if [[ -z "$TARGET" ]]; then
fail "No external USB drive detected. Please plug in an ext4-formatted USB drive and try again."
fail "No external USB drive detected. " \
"Please plug in an exFAT-formatted USB drive (≥500 GB) and try again."
fi
log "Detected external drive: $TARGET"
fi
validate_target_mount "$TARGET"
# ── Verify mount point ───────────────────────────────────────────
# ── Set up stable backup destination ────────────────────────────
# Subsequent runs update the same mirror, transferring only new or changed files.
[[ -d "$TARGET" ]] || fail "Target path '$TARGET' does not exist."
mountpoint -q "$TARGET" || fail "Target path '$TARGET' is not a mount point."
BACKUP_DIR="${TARGET}/${BACKUP_SUBPATH}"
mkdir -p -- "$BACKUP_DIR"
# ── Check free disk space (require ≥ 10 GB) ──────────────────────
# Remove any stale BACKUP_COMPLETE left by a previous successful run.
# The new run will re-earn it only after all stages succeed.
rm -f "$BACKUP_DIR/BACKUP_COMPLETE"
FREE_KB=$(df -k --output=avail "$TARGET" | tail -1)
FREE_GB=$(( FREE_KB / 1024 / 1024 ))
log "Free space on drive: ${FREE_GB} GB"
(( FREE_GB >= MIN_FREE_GB )) || \
fail "Not enough free space on drive (${FREE_GB} GB available, ${MIN_FREE_GB} GB required)."
# Write an INCOMPLETE marker immediately; replaced by BACKUP_COMPLETE only
# after all rsync stages and manifest write succeed. Failed or interrupted
# runs keep this marker so they are clearly identifiable.
touch "$BACKUP_DIR/INCOMPLETE"
# ── Create timestamped backup directory ─────────────────────────
TIMESTAMP="$(date '+%Y%m%d_%H%M%S')"
BACKUP_DIR="${TARGET}/Sovran_SystemsOS_Backup/${TIMESTAMP}"
mkdir -p "$BACKUP_DIR"
log "Backup destination: $BACKUP_DIR"
# ── Estimate required free space ─────────────────────────────────
# PostgreSQL/MariaDB raw directories and Bitcoin/Electrs data are excluded
# from the estimate to avoid inflating the required size.
ETC_NIXOS_BYTES=$(estimate_path_bytes /etc/nixos)
HOME_BYTES=$(estimate_path_bytes /home --exclude='*/.cache' --exclude='*/.local/share/Trash' --exclude='*/Trash')
# nix-bitcoin secrets and /var/lib system service data exist only on the
# Node and Server + Desktop roles — they are skipped entirely on Desktop Only.
SECRETS_BYTES=0
VAR_LIB_BYTES=0
if [[ "$ROLE" != "desktop" ]]; then
SECRETS_BYTES=$(estimate_path_bytes /etc/nix-bitcoin-secrets)
VAR_LIB_BYTES=$(estimate_path_bytes /var/lib \
--exclude='postgresql' \
--exclude='mysql' \
--exclude='mariadb' \
--exclude='bitcoind' \
--exclude='electrs' \
--exclude='*/log' \
--exclude='*/logs' \
--exclude='*/cache' \
--exclude='*/tmp')
fi
ESTIMATED_BYTES=$(( ETC_NIXOS_BYTES + HOME_BYTES + SECRETS_BYTES + VAR_LIB_BYTES ))
# Require 20% growth headroom plus a fixed 1 GiB safety margin.
# Later incremental runs need far less space, but a conservative first-run
# check protects against running out of space mid-backup.
REQUIRED_BYTES=$(( ESTIMATED_BYTES + (ESTIMATED_BYTES / 5) + SAFETY_MARGIN_BYTES ))
FREE_BYTES=$(df -B1 --output=avail "$TARGET" | tail -1 | tr -d ' ')
FREE_GB=$(( FREE_BYTES / 1024 / 1024 / 1024 ))
REQUIRED_GB=$(( REQUIRED_BYTES / 1024 / 1024 / 1024 ))
log "Estimated backup size: $(( ESTIMATED_BYTES / 1024 / 1024 / 1024 )) GB"
log "Required free space (with safety margin): ${REQUIRED_GB} GB"
log "Free space on drive: ${FREE_GB} GB"
(( FREE_BYTES >= REQUIRED_BYTES )) || \
fail "Not enough free space on drive (${FREE_GB} GB available, ${REQUIRED_GB} GB required)."
# ── Stage 1: NixOS configuration ────────────────────────────────
# ── Stage 1/4: NixOS configuration ──────────────────────────────
log ""
log "── Stage 1/${TOTAL_STAGES}: NixOS configuration (/etc/nixos) ──────────────"
log "── Stage 1/4: NixOS configuration (/etc/nixos) ──────────────"
if [[ -d /etc/nixos ]]; then
sync_tree "/etc/nixos" no /etc/nixos/ "$BACKUP_DIR/etc/nixos/"
rsync -a --info=progress2 /etc/nixos/ "$BACKUP_DIR/nixos/" 2>&1 | tee -a "$BACKUP_LOG" || \
fail "Stage 1 failed while copying /etc/nixos"
log "Stage 1 complete."
else
log "WARNING: /etc/nixos not found — skipping."
fi
# ── Stage 2: Secrets ────────────────────────────────────────────
# Only applies to the Node and Server + Desktop roles. Desktop Only systems
# run no nix-bitcoin services, so there are no secrets to back up and this
# stage does not exist for them.
if [[ "$ROLE" != "desktop" ]]; then
log ""
log "── Stage 2/${TOTAL_STAGES}: Secrets (/etc/nix-bitcoin-secrets) ───────────"
if [[ -e /etc/nix-bitcoin-secrets ]]; then
sync_tree "/etc/nix-bitcoin-secrets" no /etc/nix-bitcoin-secrets/ "$BACKUP_DIR/etc/nix-bitcoin-secrets/"
else
log "(not found: /etc/nix-bitcoin-secrets — skipping)"
fi
log "Stage 2 complete."
fi
# ── Home directory ──────────────────────────────────────────────
# Stage 2/2 on Desktop Only, stage 3/4 on Node and Server + Desktop.
# Rsync exit code 24 (vanished source files) is treated as nonfatal here
# because the desktop may be active and files can disappear between the
# directory scan and the copy. All other nonzero exit codes remain fatal.
# ── Stage 2/4: Secrets ──────────────────────────────────────────
log ""
log "── Stage ${HOME_STAGE_NUM}/${TOTAL_STAGES}: Home directory (/home) ───────────────────────"
log "── Stage 2/4: Secrets ───────────────────────────────────────"
mkdir -p "$BACKUP_DIR/secrets"
if [[ "$ROLE" == "desktop" ]]; then
log "Skipping /etc/nix-bitcoin-secrets — not applicable for Desktop Only role."
else
if [[ -e /etc/nix-bitcoin-secrets ]]; then
rsync -a --info=progress2 /etc/nix-bitcoin-secrets "$BACKUP_DIR/secrets/" 2>&1 | tee -a "$BACKUP_LOG" || \
log "WARNING: Could not copy /etc/nix-bitcoin-secrets — continuing."
else
log " (not found: /etc/nix-bitcoin-secrets — skipping)"
fi
fi
log "Stage 2 complete."
# ── Stage 3/4: Home directory ────────────────────────────────────
log ""
log "── Stage 3/4: Home directory (/home) ───────────────────────"
if [[ -d /home ]]; then
sync_tree "/home" yes /home/ "$BACKUP_DIR/home/" \
rsync -a --info=progress2 \
--exclude='.cache/' \
--exclude='.local/share/Trash/' \
--exclude='Trash/' \
--exclude='.mozilla/firefox/*/cache2/' \
--exclude='.mozilla/firefox/*/startupCache/' \
--exclude='.mozilla/firefox/*/thumbnails/' \
--exclude='.config/google-chrome/*/Cache/' \
--exclude='.config/google-chrome/*/Code Cache/' \
--exclude='.config/chromium/*/Cache/' \
--exclude='.config/chromium/*/Code Cache/' \
--exclude='.config/BraveSoftware/Brave-Browser/*/Cache/' \
--exclude='.config/BraveSoftware/Brave-Browser/*/Code Cache/' \
--exclude='.local/share/baloo/' \
--exclude='.thumbnails/' \
--exclude='.xsession-errors' \
--exclude='.xsession-errors.old'
log "Stage ${HOME_STAGE_NUM} complete."
--exclude='*/Trash/' \
/home/ "$BACKUP_DIR/home/" 2>&1 | tee -a "$BACKUP_LOG" || \
fail "Stage 3 failed while copying /home"
log "Stage 3 complete."
else
log "WARNING: /home not found — skipping."
fi
# ── Stage 4: System data ────────────────────────────────────────
# Only applies to the Node and Server + Desktop roles — Desktop Only systems
# run no server services, so /var/lib holds no service data worth mirroring.
# PostgreSQL/MariaDB raw database directories are excluded. Application
# databases must be backed up separately with native database tools.
# Bitcoin/Electrs data are excluded; they live on the internal second drive.
# ── Stage 4/4: System data ───────────────────────────────────────
if [[ "$ROLE" != "desktop" ]]; then
log ""
log "── Stage 4/${TOTAL_STAGES}: System data (/var/lib) ───────────────────────"
log ""
log "── Stage 4/4: System data (/var/lib) ────────────────────────"
if [[ "$ROLE" == "desktop" ]]; then
if [[ -d /var/lib ]]; then
sync_tree "/var/lib" no /var/lib/ "$BACKUP_DIR/var/lib/" \
--exclude='postgresql/' \
--exclude='mysql/' \
--exclude='mariadb/' \
--exclude='bitcoind/' \
--exclude='electrs/' \
--exclude='*/log/' \
rsync -a --info=progress2 \
--filter='- /lnd/***' \
--exclude='logs/' \
--exclude='log/' \
--exclude='*/logs/' \
--exclude='*/cache/' \
--exclude='*/tmp/'
log "Stage 4 complete."
--exclude='*/log/' \
/var/lib/ "$BACKUP_DIR/var-lib/" 2>&1 | tee -a "$BACKUP_LOG" || \
fail "Stage 4 failed while copying /var/lib for Desktop Only role"
log "Stage 4 complete (Desktop Only role excludes /var/lib/lnd)."
else
log "WARNING: /var/lib not found — skipping."
fi
elif [[ -d /var/lib ]]; then
rsync -a --info=progress2 \
--exclude='logs/' \
--exclude='log/' \
--exclude='*/logs/' \
--exclude='*/log/' \
/var/lib/ "$BACKUP_DIR/var-lib/" 2>&1 | tee -a "$BACKUP_LOG" || \
fail "Stage 4 failed while copying /var/lib"
log "Stage 4 complete."
else
log "WARNING: /var/lib not found — skipping."
fi
# ── Generate manifest ────────────────────────────────────────────
log ""
log "Generating BACKUP_MANIFEST.txt …"
MANIFEST_FILE="$BACKUP_DIR/BACKUP_MANIFEST.txt"
{
echo "Sovran_SystemsOS Backup Manifest"
echo "Updated: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo "Generated: $(date)"
echo "Hostname: $(hostname)"
echo "Role: $ROLE_LABEL"
echo "Target: $TARGET"
echo ""
echo "Backup type: Live rsync mirror (directly browsable files)"
echo "Location: ${BACKUP_DIR}"
echo ""
echo "Source paths mirrored:"
echo "- /etc/nixos → current/etc/nixos/"
if [[ "$ROLE" != "desktop" ]]; then
echo "- /etc/nix-bitcoin-secrets (when present) → current/etc/nix-bitcoin-secrets/"
fi
echo "- /home → current/home/"
if [[ "$ROLE" != "desktop" ]]; then
echo "- /var/lib → current/var/lib/"
fi
echo ""
echo "Exclusions:"
if [[ "$ROLE" != "desktop" ]]; then
echo "- /var/lib/postgresql (PostgreSQL raw database files — not included)"
echo "- /var/lib/mysql, /var/lib/mariadb (MariaDB raw database files — not included)"
echo "- /var/lib/bitcoind (Bitcoin blockchain — excluded; lives on internal second drive)"
echo "- /var/lib/electrs (Electrs index — excluded; lives on internal second drive)"
echo "- /run/media/Second_Drive (internal second drive — never traversed)"
echo "- /var/lib/*/log, /var/lib/*/logs, /var/lib/*/cache, /var/lib/*/tmp"
fi
echo "- Browser disk caches, thumbnail caches, trash directories, X session error logs"
echo ""
echo "Important limitations:"
if [[ "$ROLE" != "desktop" ]]; then
echo "- PostgreSQL and MariaDB/MySQL application databases are NOT included in this"
echo " backup. If you use Nextcloud, Matrix/Synapse, or other database-backed"
echo " applications, their data must be backed up separately using native tools."
echo "- Bitcoin blockchain data and Electrs indexes are NOT included; they are"
echo " reconstructable or stored on the internal second drive."
fi
echo "- This is a live file-level mirror, not a transactional database backup."
echo " Files being written during the backup may be in an inconsistent state."
echo ""
echo "Restore guidance:"
echo "- Files are directly browsable on the backup drive under: ${BACKUP_DIR}"
echo "- To restore a directory:"
echo " sudo rsync -aAXH --numeric-ids current/etc/nixos/ /etc/nixos/"
echo " sudo rsync -aAXH --numeric-ids current/home/ /home/"
if [[ "$ROLE" != "desktop" ]]; then
echo " sudo rsync -aAXH --numeric-ids current/var/lib/ /var/lib/"
fi
echo "- To copy individual files:"
echo " sudo cp -a current/home/username/ /home/username/"
echo "- When restoring /etc/nixos to replacement hardware, regenerate"
echo " hardware-configuration.nix for the new hardware before rebuilding."
echo ""
echo "Nonfatal warnings:"
if [[ "${#RSYNC_WARNINGS[@]}" -eq 0 ]]; then
echo "- none"
else
for warning in "${RSYNC_WARNINGS[@]}"; do
echo "- $warning"
done
fi
if [[ "$ROLE" != "desktop" ]]; then
echo ""
echo "Note: Bitcoin blockchain and Electrs index data are intentionally excluded"
echo "from manual external backup because they already live on the internal second drive"
echo "(/run/media/Second_Drive) and are reconstructable/internal-backup data."
fi
} > "$MANIFEST_FILE"
log "Manifest written to $MANIFEST_FILE"
echo "Contents:"
find "$BACKUP_DIR" -mindepth 1 -maxdepth 2 | sort
} > "$BACKUP_DIR/BACKUP_MANIFEST.txt"
log "Manifest written to $BACKUP_DIR/BACKUP_MANIFEST.txt"
# ── Done ─────────────────────────────────────────────────────────
log ""
if [[ "${#RSYNC_WARNINGS[@]}" -gt 0 ]]; then
log "Backup completed with nonfatal warnings:"
for warning in "${RSYNC_WARNINGS[@]}"; do
log " WARNING: $warning"
done
log "Your important data is backed up. The warnings above indicate files that"
log "vanished during backup, which is normal on an active desktop."
log ""
fi
if [[ "$ROLE" == "desktop" ]]; then
log "All Finished! Your data is now backed up to a second, external location."
else
log "All Finished! Your data is now backed up to a third location."
fi
log "Files are directly browsable on the drive under: ${BACKUP_DIR}"
log "All Finished! Your data is now backed up to a third location."
log "Please eject the drive safely before removing it from your Sovran Pro."
# Remove incomplete marker and write completion marker only after all work succeeds.
# A later successful run will update the same mirror and replace any INCOMPLETE state.
rm -f "$BACKUP_DIR/INCOMPLETE"
date -u '+%Y-%m-%dT%H:%M:%SZ' > "$BACKUP_DIR/BACKUP_COMPLETE"
BACKUP_COMPLETE=1
set_status "SUCCESS"
@@ -1,313 +0,0 @@
"""Sovran Hub — pure security validation helpers.
This module contains the dependency-light security helper functions used by
the Hub server. Keeping them here allows tests to import and exercise the
exact production implementations rather than maintaining separate copies.
All functions in this module depend only on the Python standard library.
"""
from __future__ import annotations
import base64
import ipaddress
import json
import os
import re
import tempfile
import time
import urllib.parse
# ── Nix string escaping ────────────────────────────────────────────────────────
def _nix_escape(value: str) -> str:
"""Escape *value* for use inside a Nix double-quoted string literal.
Handles backslashes, double-quotes, newlines, carriage returns, tabs, and
Nix-specific anti-quotation sequences (``${...}``). The returned value is
safe to embed as ``"<returned_value>"`` in generated Nix source.
"""
value = value.replace("\\", "\\\\")
value = value.replace('"', '\\"')
value = value.replace("\n", "\\n")
value = value.replace("\r", "\\r")
value = value.replace("\t", "\\t")
value = value.replace("${", "\\${")
return value
# ── Nostr npub validation (NIP-19 / Bech32) ───────────────────────────────────
# Fast pre-filter: "npub1" followed by exactly 58 lower-case bech32 characters.
NPUB_RE = re.compile(r"^npub1[023456789acdefghjklmnpqrstuvwxyz]{58}$")
_BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
_BECH32_GENERATOR = (0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3)
def _bech32_polymod(values: list[int]) -> int:
chk = 1
for value in values:
top = chk >> 25
chk = ((chk & 0x1FFFFFF) << 5) ^ value
for i in range(5):
if (top >> i) & 1:
chk ^= _BECH32_GENERATOR[i]
return chk
def _bech32_hrp_expand(hrp: str) -> list[int]:
return [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]
def _bech32_create_checksum(hrp: str, data: list[int]) -> list[int]:
values = _bech32_hrp_expand(hrp) + data
polymod = _bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
return [(polymod >> (5 * (5 - i))) & 31 for i in range(6)]
def _bech32_convertbits_decode(data: list[int]) -> list[int] | None:
"""Convert a 5-bit integer sequence to 8-bit bytes, stripping padding."""
acc = 0
bits = 0
ret: list[int] = []
for value in data:
acc = (acc << 5) | value
bits += 5
while bits >= 8:
bits -= 8
ret.append((acc >> bits) & 0xFF)
if bits >= 5 or ((acc << (8 - bits)) & 0xFF):
return None # invalid padding
return ret
def _bech32_decode(bech: str) -> tuple[str, bytes] | None:
"""Decode a bech32 string. Returns ``(hrp, payload_bytes)`` or ``None``.
Verifies:
- Lowercase-only (mixed case rejected per BIP-173).
- Only valid bech32 charset characters.
- Valid checksum.
- Exactly one separator (``1``).
- Minimum data part length (≥ 8 chars = 6 checksum + ≥ 2 data).
"""
if bech != bech.lower():
return None # mixed case
sep = bech.rfind("1")
if sep < 1 or sep + 7 > len(bech):
return None
hrp = bech[:sep]
data_part = bech[sep + 1:]
if any(c not in _BECH32_CHARSET for c in data_part):
return None
decoded = [_BECH32_CHARSET.index(c) for c in data_part]
if _bech32_polymod(_bech32_hrp_expand(hrp) + decoded) != 1:
return None # bad checksum
converted = _bech32_convertbits_decode(decoded[:-6])
if converted is None:
return None
return hrp, bytes(converted)
def _validate_npub(value: str) -> bool:
"""Return ``True`` iff *value* is a valid NIP-19 Nostr npub.
Checks:
- Lowercase ``npub`` HRP.
- Valid bech32 charset (no uppercase, no invalid chars).
- Valid bech32 checksum.
- Exactly 32 decoded payload bytes (256-bit public key).
- Retains the original regex as a fast pre-filter.
"""
if not NPUB_RE.fullmatch(value):
return False
result = _bech32_decode(value)
if result is None:
return False
hrp, payload = result
return hrp == "npub" and len(payload) == 32
# ── DDNS URL validation ────────────────────────────────────────────────────────
_DDNS_URL_MAX_LEN = 2048
_DDNS_CONTROL_RE = re.compile(r"[\x00-\x1f\x7f]")
# Allowlist: only the official Njal.la provider hostnames are accepted for
# DDNS update URLs. Any other host would allow SSRF against the Hub's
# internal network.
_DDNS_ALLOWED_HOSTNAMES: frozenset[str] = frozenset(["njal.la", "www.njal.la"])
def _validate_ddns_url(url: str) -> str:
"""Validate *url* as a safe DDNS update URL and return it normalised.
Rules:
- Must be a valid URL parseable by urllib.parse.
- Scheme must be ``https`` (case-insensitive).
- No userinfo (credentials must not be embedded in the URL).
- No fragment.
- No control characters.
- Must not exceed ``_DDNS_URL_MAX_LEN`` bytes.
- Hostname must be the exact Njal.la provider hostname (njal.la or www.njal.la).
- Port must be absent or the default HTTPS port 443.
- No percent-encoded null bytes.
Raises ``ValueError`` with a safe (non-secret) message on failure.
"""
if not url:
raise ValueError("DDNS URL must not be empty")
if len(url) > _DDNS_URL_MAX_LEN:
raise ValueError("DDNS URL exceeds maximum length")
if _DDNS_CONTROL_RE.search(url):
raise ValueError("DDNS URL contains control characters")
try:
parsed = urllib.parse.urlparse(url)
except Exception:
raise ValueError("DDNS URL could not be parsed")
if parsed.scheme.lower() != "https":
raise ValueError("DDNS URL must use the https scheme")
if parsed.username or parsed.password:
raise ValueError("DDNS URL must not contain credentials")
if parsed.fragment:
raise ValueError("DDNS URL must not contain a fragment")
if parsed.port is not None and parsed.port != 443:
raise ValueError("DDNS URL must use the default HTTPS port")
hostname = parsed.hostname or ""
if not hostname:
raise ValueError("DDNS URL must contain a hostname")
# Reject raw IP addresses
try:
ipaddress.ip_address(hostname)
raise ValueError("DDNS URL hostname must not be a raw IP address")
except ValueError as exc:
if "raw IP" in str(exc):
raise
# Allowlist: only Njal.la
if hostname.lower() not in _DDNS_ALLOWED_HOSTNAMES:
raise ValueError(
f"DDNS URL hostname is not an allowed Njal.la host "
f"(got {hostname!r})"
)
if "%00" in url.lower():
raise ValueError("DDNS URL must not contain encoded null bytes")
# Reject any remaining $ expressions — after ${IP} substitution there
# must be none. Callers that store ${IP} placeholder URLs must substitute
# before calling this function.
if "$" in url:
raise ValueError("DDNS URL must not contain $ expressions")
# Require the exact /update/ path used by Njal.la
if parsed.path != "/update/":
raise ValueError("DDNS URL path must be exactly /update/")
return url
# ── SSH public-key validation ─────────────────────────────────────────────────
_SSH_PUBKEY_ALGORITHMS = frozenset([
"ssh-ed25519",
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
"sk-ssh-ed25519@openssh.com",
])
def _validate_ssh_pubkey(key: str) -> str:
"""Validate *key* as a single OpenSSH public key and return it normalised.
Accepts only single-line keys with a supported algorithm, valid base64
payload, and an optional comment. Rejects options, multiple lines,
control characters, and unsupported algorithms.
Raises ``ValueError`` with a safe message on failure.
"""
key = key.strip()
if not key:
raise ValueError("SSH public key must not be empty")
if _DDNS_CONTROL_RE.search(key):
raise ValueError("SSH public key contains control characters")
if "\n" in key or "\r" in key:
raise ValueError("SSH public key must be a single line")
parts = key.split()
if len(parts) < 2:
raise ValueError("SSH public key is malformed")
algo, b64 = parts[0], parts[1]
if algo not in _SSH_PUBKEY_ALGORITHMS:
raise ValueError(f"Unsupported SSH key algorithm: {algo!r}")
try:
decoded = base64.b64decode(b64, validate=True)
except Exception:
raise ValueError("SSH public key payload is not valid base64")
if len(decoded) < 20:
raise ValueError("SSH public key payload is too short")
return key
# ── Persistent Hub session store ─────────────────────────────────────────────
def load_session_store(path: str) -> dict[str, float]:
"""Load persisted Hub sessions from *path*.
Returns a mapping of session token → expiry timestamp (epoch seconds).
Expired entries are discarded. A missing, unreadable or malformed file
yields an empty mapping — losing sessions is a UX inconvenience (the user
must log in again), never a fatal error.
Persistence exists so that authenticated sessions survive a restart of
the Hub service itself. ``nixos-rebuild switch`` restarts
``sovran-hub-web.service`` during activation (its unit definition changes
with every feature toggle), and without persistence the in-progress
rebuild/update status polling loses authentication and the UI hangs.
"""
try:
with open(path, "r") as f:
data = json.load(f)
except (OSError, ValueError):
return {}
if not isinstance(data, dict):
return {}
now = time.time()
sessions: dict[str, float] = {}
for token, expiry in data.items():
if not isinstance(token, str) or not token:
continue
if isinstance(expiry, bool) or not isinstance(expiry, (int, float)):
continue
if expiry > now:
sessions[token] = float(expiry)
return sessions
def save_session_store(path: str, sessions: dict[str, float]) -> bool:
"""Atomically persist *sessions* (token → expiry) to *path* with mode 0600.
Writes to a temp file in the same directory and renames it into place so
the store is never left partially written. Returns True on success,
False otherwise (persistence is best-effort).
"""
directory = os.path.dirname(path) or "."
fd = None
tmp_path = None
try:
os.makedirs(directory, exist_ok=True)
fd, tmp_path = tempfile.mkstemp(dir=directory, prefix=".hub_sessions_tmp")
with os.fdopen(fd, "w") as f:
fd = None # os.fdopen takes ownership of the descriptor
json.dump(sessions, f)
os.chmod(tmp_path, 0o600)
os.replace(tmp_path, path)
return True
except OSError:
if fd is not None:
try:
os.close(fd)
except OSError:
pass
if tmp_path is not None:
try:
os.unlink(tmp_path)
except OSError:
pass
return False
File diff suppressed because it is too large. Load diff
@@ -171,67 +171,3 @@ domain-field-actions {
.port-req-status {
font-weight: 600;
}
/* ── Port-forwarding instructions ─────────────────────────────────── */
.port-req-steps {
margin: 4px 0 12px;
padding-left: 20px;
font-size: 0.84rem;
color: var(--text-secondary);
line-height: 1.65;
}
.port-req-steps li {
margin-bottom: 5px;
}
.port-proto-badge {
display: inline-block;
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
padding: 2px 7px;
border-radius: 5px;
white-space: nowrap;
}
.port-proto-badge--tcp {
color: #6dbf8b;
background: rgba(109, 191, 139, 0.12);
border: 1px solid rgba(109, 191, 139, 0.4);
}
.port-proto-badge--udp {
color: #6aa9e0;
background: rgba(106, 169, 224, 0.12);
border: 1px solid rgba(106, 169, 224, 0.4);
}
.port-proto-badge--both {
color: #e5a50a;
background: rgba(229, 165, 10, 0.12);
border: 1px solid rgba(229, 165, 10, 0.45);
}
.port-proto-note {
display: block;
margin-top: 3px;
font-size: 0.68rem;
color: var(--text-dim);
text-transform: none;
letter-spacing: 0;
}
/* ── Wallet Connections unique-hostname warning ───────────────────── */
.domain-nwc-warning {
margin-bottom: 14px;
padding: 10px 14px;
background: rgba(255, 180, 0, 0.10);
border: 1px solid var(--warning-color, #f59e0b);
border-radius: 8px;
font-size: 0.88rem;
line-height: 1.6;
}
@@ -29,15 +29,6 @@
color: var(--text-primary);
}
.title-group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 6px 10px;
}
.header-buttons {
display: flex;
align-items: center;
@@ -54,23 +45,6 @@
letter-spacing: 0.03em;
}
/* ── OS Version Badge — identical styling to the version badge ────
── shown next to titles in the service modal windows ──────────── */
.os-version-badge {
background-color: rgba(255, 255, 255, 0.06);
color: var(--text-secondary);
font-size: 0.72rem;
font-weight: 600;
padding: 2px 10px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
letter-spacing: 0.02em;
display: inline-flex;
align-items: center;
white-space: nowrap;
flex-shrink: 0;
}
/* ── IP bar ─────────────────────────────────────────────────────── */
.ip-bar {
@@ -1,56 +1,5 @@
/* ── NWC Benefits Grid ─────────────────────────────────────────── */
.nwc-benefits-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 14px;
}
.nwc-benefit-item {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 10px 14px;
display: flex;
gap: 12px;
align-items: flex-start;
transition: background-color 0.2s;
}
.nwc-benefit-item:hover {
background: rgba(255, 255, 255, 0.04);
}
.nwc-benefit-icon {
font-size: 1.25rem;
margin-top: 2px;
opacity: 0.9;
}
.nwc-benefit-content strong {
display: block;
font-size: 0.82rem;
color: var(--text-primary);
margin-bottom: 2px;
}
.nwc-benefit-content p {
margin: 0;
font-size: 0.75rem;
line-height: 1.45;
color: var(--text-secondary);
}
@media (max-width: 800px) {
.nwc-benefits-grid {
grid-template-columns: 1fr;
}
}
/* ── Update modal ────────────────────────────────────────────────── */
.modal-overlay {
display: none;
position: fixed;
@@ -251,19 +200,6 @@ button.btn-reboot:hover:not(:disabled) {
gap: 10px;
}
.creds-title-version-badge {
background-color: rgba(255, 255, 255, 0.06);
color: var(--text-secondary);
font-size: 0.72rem;
font-weight: 600;
padding: 2px 10px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
letter-spacing: 0.02em;
display: inline-flex;
align-items: center;
}
.creds-title-icon {
width: 28px;
height: 28px;
@@ -287,10 +223,6 @@ button.btn-reboot:hover:not(:disabled) {
color: var(--text-primary);
}
.creds-dialog--wide {
max-width: 980px;
}
.creds-body {
padding: 24px 28px;
overflow-y: auto;
@@ -546,593 +478,3 @@ button.btn-reboot:hover:not(:disabled) {
color: var(--text-secondary);
font-style: italic;
}
.nwc-secret-warning {
margin: 0 0 14px;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #f59e0b;
background: rgba(245, 158, 11, 0.12);
color: #fbbf24;
font-size: 0.86rem;
}
/* ── Lightning Wallet Connections: tabbed shell ─────────────────── */
.nwc-tabs-bar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin: -6px 0 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.nwc-tabs-btns {
display: flex;
gap: 4px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 4px;
}
.nwc-tab-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 0.86rem;
font-weight: 600;
padding: 8px 16px;
border-radius: 7px;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.15s ease, color 0.15s ease;
}
.nwc-tab-btn:hover:not(.active) {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.04);
}
.nwc-tab-btn.active {
background: var(--accent-color);
color: #0A1A10;
}
.nwc-header-chips {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.nwc-header-chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-color);
border-radius: 999px;
padding: 4px 10px;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nwc-header-chip--domain {
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-size: 0.72rem;
}
.nwc-header-chip--warn {
border-color: rgba(245, 158, 11, 0.5);
color: #fbbf24;
}
.nwc-tab-panel {
display: none;
}
.nwc-tab-panel.active {
display: block;
animation: nwc-tab-fade 0.16s ease-out;
}
@keyframes nwc-tab-fade {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Setup tab: first section shouldn't double up on top padding */
#nwc-tab-setup .svc-detail-section:first-child {
padding-top: 0;
margin-top: 0;
border-top: none;
}
.nwc-tab-intro {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 16px;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.nwc-tab-intro-title {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
.nwc-tab-intro-desc {
margin: 0;
font-size: 0.85rem;
line-height: 1.5;
color: var(--text-secondary);
max-width: 62ch;
}
/* ── Wallet toolbar ─────────────────────────────────────────────── */
.nwc-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.nwc-toolbar-count {
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-dim);
}
.nwc-toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.nwc-toolbar-actions .matrix-action-btn,
.nwc-toolbar-actions .matrix-form-back {
padding: 8px 14px;
font-size: 0.82rem;
}
/* ── Domain-required callout ────────────────────────────────────── */
.nwc-domain-required {
border: 1px solid rgba(245, 158, 11, 0.4);
background: rgba(245, 158, 11, 0.08);
border-radius: 10px;
padding: 14px 16px;
margin-bottom: 16px;
font-size: 0.86rem;
color: var(--text-primary);
}
.nwc-domain-required-body {
margin-top: 6px;
color: var(--text-secondary);
line-height: 1.5;
}
.nwc-domain-required-btn {
margin-top: 12px;
}
/* ── Empty / disabled states ────────────────────────────────────── */
.nwc-empty-state,
.nwc-disabled-state {
text-align: center;
padding: 36px 24px;
border: 1px dashed var(--border-color);
border-radius: 12px;
background: rgba(255, 255, 255, 0.015);
}
.nwc-empty-icon,
.nwc-disabled-icon {
font-size: 2rem;
margin-bottom: 10px;
}
.nwc-empty-title,
.nwc-disabled-title {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 6px;
}
.nwc-empty-desc,
.nwc-disabled-desc {
margin: 0 auto 14px;
max-width: 46ch;
font-size: 0.86rem;
line-height: 1.55;
color: var(--text-secondary);
}
/* ── Wallet cards ───────────────────────────────────────────────── */
.nwc-wallet-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 12px;
margin-top: 12px;
}
.nwc-wallet-card {
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 14px 16px;
background: rgba(255, 255, 255, 0.02);
display: flex;
flex-direction: column;
transition: border-color 0.15s ease;
}
.nwc-wallet-card:hover {
border-color: rgba(255, 255, 255, 0.16);
}
.nwc-wallet-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.nwc-wallet-card-title {
font-weight: 700;
font-size: 0.95rem;
color: var(--text-primary);
word-break: break-word;
}
.nwc-wallet-chips {
display: flex;
gap: 6px;
flex-wrap: wrap;
justify-content: flex-end;
flex-shrink: 0;
}
.nwc-wallet-chip {
display: inline-flex;
align-items: center;
font-size: 0.72rem;
font-weight: 700;
border-radius: 999px;
padding: 4px 10px;
white-space: nowrap;
}
.nwc-wallet-chip--balance {
background: rgba(109, 191, 139, 0.14);
color: var(--green);
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
}
.nwc-wallet-chip--pending {
background: rgba(245, 158, 11, 0.14);
color: #fbbf24;
}
.nwc-wallet-address {
margin-bottom: 4px;
}
.nwc-wallet-address .creds-value {
font-size: 0.82rem;
padding: 9px 12px;
}
.nwc-wallet-address .creds-copy-btn {
margin-top: 5px;
padding: 7px 12px;
}
.nwc-wallet-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.nwc-wallet-actions-spacer {
flex: 1;
min-width: 0;
}
.nwc-wallet-actions .matrix-form-back,
.nwc-wallet-actions .btn-close-modal,
.nwc-wallet-actions .matrix-action-btn {
padding: 7px 12px;
font-size: 0.8rem;
}
.nwc-wallet-danger-btn {
color: #f08a8a;
}
.nwc-wallet-danger-btn:hover:not(:disabled) {
background-color: rgba(220, 80, 80, 0.14);
color: #ff9d9d;
}
/* ── NWC LNURL share / download view ───────────────────────────── */
.nwc-share-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 14px;
}
.nwc-share-actions .matrix-form-back,
.nwc-share-actions .matrix-form-submit {
padding: 8px 12px;
font-size: 0.82rem;
}
.nwc-lnurl-value {
font-size: 0.72rem;
word-break: break-all;
}
.nwc-share-formats-hint {
text-align: center;
}
/* ── NWC Liquidity Guide (collapsible) ──────────────────────────── */
.nwc-liquidity-details {
margin-top: 20px;
border: 1px solid var(--border-color);
border-radius: 12px;
background: rgba(255, 255, 255, 0.015);
}
.nwc-liquidity-summary {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
cursor: pointer;
list-style: none;
color: var(--text-primary);
font-size: 0.9rem;
}
.nwc-liquidity-summary::-webkit-details-marker {
display: none;
}
.nwc-liquidity-summary::after {
content: "▸";
margin-left: auto;
color: var(--text-dim);
transition: transform 0.15s ease;
}
.nwc-liquidity-details[open] .nwc-liquidity-summary::after {
transform: rotate(90deg);
}
.nwc-liquidity-summary:hover {
background: rgba(255, 255, 255, 0.02);
border-radius: 12px;
}
.nwc-liquidity-icon {
font-size: 22px;
}
.nwc-liquidity-subtitle {
display: block;
margin-top: 2px;
font-size: 0.75rem;
font-weight: 400;
color: var(--text-secondary);
}
.nwc-liquidity-guide-card {
padding: 0 16px 16px;
color: var(--text-primary);
}
.nwc-liquidity-body {
font-size: 0.82rem;
line-height: 1.55;
margin: 0 0 12px;
color: var(--text-secondary);
}
.nwc-liquidity-steps {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 14px;
}
.nwc-liquidity-step {
display: flex;
gap: 10px;
align-items: flex-start;
font-size: 0.85rem;
}
.nwc-liquidity-step-desc {
margin: 2px 0 0;
color: var(--text-secondary);
font-size: 0.78rem;
line-height: 1.5;
}
.step-num {
background: var(--accent-color);
color: #0A1A10;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.75rem;
flex-shrink: 0;
margin-top: 1px;
}
.nwc-liquidity-action {
background: rgba(109, 191, 139, 0.08);
border-left: 3px solid var(--green);
padding: 10px 12px;
border-radius: 0 8px 8px 0;
font-size: 0.82rem;
}
.nwc-liquidity-analogy {
margin: 0 0 10px;
font-size: 0.8rem;
line-height: 1.45;
}
.nwc-rtl-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--border-color);
color: var(--text-primary);
border: none;
padding: 8px 14px;
border-radius: 8px;
font-size: 0.8rem;
cursor: pointer;
}
.nwc-rtl-btn:hover {
background: #1c2a24;
}
/* ── NWC Connect Guide (shown after wallet creation) ────────────── */
.nwc-connect-guide {
margin-top: 16px;
padding: 16px;
border: 1px solid var(--border-color);
border-radius: 12px;
background: rgba(109, 191, 139, 0.04);
}
.nwc-connect-guide-title {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.nwc-connect-guide-intro {
margin: 0 0 14px;
font-size: 0.85rem;
line-height: 1.55;
color: var(--text-secondary);
}
.nwc-connect-steps {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 14px;
}
.nwc-connect-step {
display: flex;
gap: 10px;
align-items: flex-start;
font-size: 0.86rem;
line-height: 1.5;
}
.nwc-step-num {
background: var(--accent-color);
color: #0A1A10;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.78rem;
flex-shrink: 0;
margin-top: 2px;
}
.nwc-connect-note {
background: rgba(245, 158, 11, 0.08);
border-left: 3px solid #f59e0b;
padding: 10px 12px;
border-radius: 0 8px 8px 0;
font-size: 0.8rem;
line-height: 1.5;
color: var(--text-secondary);
}
.nwc-connect-note strong {
color: #fbbf24;
}
/* ── Narrow screens ─────────────────────────────────────────────── */
@media (max-width: 720px) {
.nwc-tabs-bar {
align-items: stretch;
flex-direction: column;
}
.nwc-tabs-btns {
justify-content: stretch;
}
.nwc-tab-btn {
flex: 1;
padding: 8px 10px;
}
.nwc-wallet-list {
grid-template-columns: 1fr;
}
.nwc-wallet-card-head {
flex-direction: column;
}
.nwc-wallet-chips {
justify-content: flex-start;
}
.nwc-wallet-actions-spacer {
display: none;
}
}
@@ -1,28 +1,5 @@
/* ── Service tile card (status-only) ─────────────────────────────── */
.dashboard-loading {
min-height: 180px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
color: var(--text-secondary);
font-size: 0.9rem;
}
.dashboard-loading-spinner {
width: 16px;
height: 16px;
border: 2px solid var(--border-color);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: dashboard-loading-spin 0.8s linear infinite;
}
@keyframes dashboard-loading-spin {
to { transform: rotate(360deg); }
}
.service-tile {
width: 160px;
min-height: 130px;
@@ -5,10 +5,6 @@
const POLL_INTERVAL_SERVICES = 5000;
const POLL_INTERVAL_UPDATES = 1800000;
const UPDATE_POLL_INTERVAL = 2000;
// Max consecutive failed rebuild/update status polls before the page gives up
// waiting and reloads to re-sync (2s interval → ~2 minutes of failures).
// A brief Hub restart during activation only causes a handful of failures.
const STATUS_POLL_MAX_FAILURES = 60;
const REBOOT_CHECK_INTERVAL = 5000;
const REBOOT_FETCH_TIMEOUT = 12000;
const REBOOT_REQUEST_TIMEOUT = 4000;
@@ -1,146 +0,0 @@
/* Sovran_SystemsOS Hub — Shared domain-prerequisite instructions.
SINGLE SOURCE OF TRUTH for the "you need a Njal.la domain + router port
forwarding" guidance that must read identically everywhere it appears:
• First-boot onboarding wizard (Server + Desktop role)
— onboarding.js step 3
• Feature-enable domain modal (Node role, and any role)
— features.js openDomainSetupModal()
(Lightning Wallet Connections / NWC, BTCPay Server, Haven, …)
• Domain reconfigure / troubleshooting modal
— features.js openDomainReconfigureModal()
Keep these three surfaces word-for-word consistent: always edit this
file, never fork the wording inline. Plain classic script (no modules) —
both templates load it with a plain <script> tag. */
"use strict";
/* Escape helper local to this file so it is self-contained on both pages. */
function dpEsc(str) {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
/* ── "What you'll need" intro ──────────────────────────────────────
opts.serviceName — e.g. "Lightning Wallet Connections", or null for
the multi-service (onboarding) wording.
opts.hostExample — e.g. "lightning" → lightning.yourdomain.com */
function renderDomainNeedsHtml(opts) {
var serviceName = opts.serviceName || null;
var hostExample = dpEsc(opts.hostExample || "myservice");
var html = "";
if (serviceName) {
html += "<p>To enable <strong>" + dpEsc(serviceName) + "</strong>, you'll need two things first:</p>";
html += '<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">';
html += "<li><strong>A domain of your own from <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">Njal.la</a></strong> "
+ "— privacy-friendly, no personal details required, accepts Bitcoin. "
+ dpEsc(serviceName) + " gets its own hostname: a subdomain (e.g. <code>" + hostExample + ".yourdomain.com</code>) "
+ "or a separate domain — your choice. Subdomains are free, and one domain can have many.</li>";
html += "<li><strong>Access to your router</strong> — you'll forward ports <strong>80</strong> and <strong>443</strong> (TCP) "
+ "to this computer once. All domain-based services share these two ports; "
+ "they're required for HTTPS and SSL certificates.</li>";
html += "</ol>";
} else {
html += "<p><strong>Each service below needs two things set up:</strong></p>";
html += '<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">';
html += "<li><strong>A domain of your own from <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">Njal.la</a></strong> "
+ "— privacy-friendly, no personal details required, accepts Bitcoin. "
+ "Each service gets its own hostname: its own subdomain (e.g. <code>" + hostExample + ".yourdomain.com</code>) "
+ "or a separate domain — your choice. Subdomains are free, and one domain can have many, "
+ "so a single domain can serve every service.</li>";
html += "<li><strong>Access to your router</strong> — you'll forward ports <strong>80</strong> and <strong>443</strong> (TCP) "
+ "to this computer once. All domain-based services share these two ports; "
+ "they're required for HTTPS and SSL certificates.</li>";
html += "</ol>";
}
return html;
}
/* ── "How to set it up at Njal.la" steps ───────────────────────────
opts.hostExample — host part used in the examples (default "call").
opts.pasteHint — where the curl command goes:
"below" (single service) or "next to its service below"
(onboarding, many services). */
function renderNjallaStepsHtml(opts) {
var hostExample = dpEsc((opts && opts.hostExample) || "call");
var pasteHint = (opts && opts.pasteHint) || "below";
var html = "";
html += "<p style=\"margin-top:12px;\"><strong>How to set it up at Njal.la:</strong></p>";
html += '<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">';
html += "<li>Create an account at <a href=\"https://njal.la\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:var(--accent-color);\">https://njal.la</a> and buy a domain.</li>";
html += "<li>Add a <strong>Dynamic</strong> record for the hostname:"
+ '<ul style="margin:4px 0 0 16px;padding:0;line-height:1.7;">'
+ "<li>In the Njal.la <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>"
+ "(For &quot;" + hostExample + ".yourdomain.com&quot; you&apos;d type just: <code>" + hostExample + "</code>.)<br>"
+ "&#9888; Do NOT type the full domain here — Njal.la adds it automatically.</li>"
+ "<li>Dedicating a whole separate domain to this service? Leave the Name field blank or use <code>@</code>.</li>"
+ "</ul>"
+ "</li>";
html += "<li>A Dynamic record has <strong>NO IP field</strong> — you don&apos;t enter an IP anywhere. "
+ "It auto-fills once Sovran_SystemsOS runs the update command (on save, and again after every reboot).</li>";
html += "<li>Njal.la gives you a curl command, e.g.:<br>"
+ '<code style="font-size:0.8em;">curl &quot;https://njal.la/update/?h=' + hostExample + '.yourdomain.com&amp;k=abc123&amp;auto&quot;</code><br>'
+ "Copy it and paste it " + pasteHint + ".</li>";
html += "</ol>";
return html;
}
/* ── "One router task" port-forwarding box ─────────────────────────
opts.internalIp — LAN IP string, or empty/null for generic wording.
opts.plural — true for multi-service (onboarding) wording.
opts.includeSsh — also mention port 22 for SSH (onboarding).
opts.extraNote — extra sentence appended at the end (optional). */
function renderRouterPortsHtml(opts) {
opts = opts || {};
var ipPart = opts.internalIp
? " to this computer&rsquo;s internal IP <strong>" + dpEsc(opts.internalIp) + "</strong>"
: " to this computer&rsquo;s internal IP";
var serviceWord = opts.plural ? "services" : "service";
var html = "";
html += "🔌 <strong>One router task:</strong> in your router&rsquo;s <strong>port forwarding</strong> settings, "
+ "forward port <strong>80 (TCP)</strong> and port <strong>443 (TCP)</strong>"
+ ipPart + ". Use the <strong>same number for the internal and external port</strong>. "
+ "This only needs to be done once — all domain services share these ports — "
+ "but HTTPS and SSL certificates won&rsquo;t work without them, so your " + serviceWord + " "
+ "can&rsquo;t be reached from outside your home network. "
+ "You&rsquo;ll need normal access to your router&rsquo;s settings with working port forwarding — "
+ "if your ISP blocks it (e.g. CGNAT), domain-based services can&rsquo;t be reached from the internet. ";
if (opts.includeSsh) {
html += "Add port <strong>22 (TCP)</strong> as well if you want remote SSH access. ";
}
if (opts.extraNote) {
html += dpEsc(opts.extraNote);
}
return html;
}
/* ── Async helper: fill a router-box container with the internal IP ──
Renders generic text immediately, then upgrades to the concrete LAN IP
if /api/network provides one. Best-effort — never blocks the UI. */
function renderRouterPortsBox(elId, opts) {
var el = document.getElementById(elId);
if (!el) return;
el.innerHTML = renderRouterPortsHtml(opts || {});
fetch("/api/network")
.then(function(r) { return r.json(); })
.then(function(data) {
var target = document.getElementById(elId);
if (!target) return;
var ip = data && data.internal_ip;
if (ip && ip !== "unavailable") {
var next = {};
for (var k in (opts || {})) next[k] = opts[k];
next.internalIp = String(ip).trim();
target.innerHTML = renderRouterPortsHtml(next);
}
})
.catch(function() { /* generic wording already shown — fine */ });
}
+15 -24
View File
@@ -187,39 +187,30 @@ function showSecurityBanner() {
// ── Init ──────────────────────────────────────────────────────────
async function init() {
// These lightweight requests are independent. Running them together avoids
// making the dashboard wait through three serial network round-trips before
// it can even start loading the service tiles.
var onboardingStatus = null;
var bannerData = null;
var cfg;
// Check onboarding status first — redirect to wizard if not complete
try {
var startupResults = await Promise.all([
apiFetch("/api/onboarding/status").catch(function() { return null; }),
apiFetch("/api/security/banner-status").catch(function() { return null; }),
apiFetch("/api/config"),
]);
onboardingStatus = startupResults[0];
bannerData = startupResults[1];
cfg = startupResults[2];
var onboardingStatus = await apiFetch("/api/onboarding/status");
if (!onboardingStatus.complete) {
window.location.href = "/onboarding";
return;
}
} catch (_) {
// If config cannot be loaded, continue with the normal service fallback.
cfg = null;
}
if (onboardingStatus && !onboardingStatus.complete) {
window.location.href = "/onboarding";
return;
// If we can't reach the endpoint, continue to normal dashboard
}
// Show first-login security banner only for machines that went through onboarding
// (legacy machines without the onboarding flag will never see this)
if (bannerData && bannerData.show) {
showSecurityBanner();
try {
var bannerData = await apiFetch("/api/security/banner-status");
if (bannerData && bannerData.show) {
showSecurityBanner();
}
} catch (_) {
// Non-fatal — silently ignore
}
try {
if (!cfg) throw new Error("Hub config unavailable");
var cfg = await apiFetch("/api/config");
_currentRole = cfg.role || "server_plus_desktop";
if (cfg.category_order) {
for (var i = 0; i < cfg.category_order.length; i++) {
+96 -61
View File
@@ -59,8 +59,6 @@ function openDomainSetupModal(feat, onSaved) {
if (!$domainSetupModal) return;
if ($domainSetupTitle) $domainSetupTitle.textContent = "🌐 Domain Setup — " + feat.name;
var isWalletConnections = (feat.id === "nwc-wallets" || feat.domain_name === "lightning");
var npubField = "";
if (feat.id === "haven") {
var currentNpub = "";
@@ -75,32 +73,48 @@ function openDomainSetupModal(feat, onSaved) {
npubField = '<div class="domain-field-group"><label class="domain-field-label" for="domain-npub-input">Nostr Public Key (npub1...):</label><input class="domain-field-input" type="text" id="domain-npub-input" placeholder="npub1..." value="' + escHtml(currentNpub) + '" /></div>';
}
var nwcWarning = isWalletConnections
? '<div class="domain-nwc-warning">' +
'<strong>⚠ Lightning Wallet Connections requires its own unique hostname.</strong> ' +
'Use a new subdomain such as <code>lightning.yourdomain.com</code>, or a separate domain. ' +
'Do not reuse a domain already assigned to Matrix, Nextcloud, WordPress, BTCPay Server, Vaultwarden, Haven, or another Caddy site.' +
'</div>'
: '';
var domainPlaceholder = isWalletConnections ? "lightning.yourdomain.com" : "myservice.example.com";
var domainLabelExample = isWalletConnections ? "lightning.yourdomain.com" : "call.yourdomain.com";
// Shared instructions (single source of truth: static/js/domain-prereqs.js) —
// identical wording to the Server + Desktop onboarding wizard and every other
// domain-based feature, regardless of role (Node / Desktop / Server+Desktop).
var hostExample = isWalletConnections ? "lightning" : "call";
var introHtml;
if (_currentRole === "node") {
introHtml =
'<p>To enable <strong>' + escHtml(feat.name) + '</strong>, it needs its own domain from Njal.la.</p>' +
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
'<li>Create an account at <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">njal.la</a>.</li>' +
'<li>Set up a domain for it — either a free subdomain or a separate domain. Pick one option:</li>' +
'</ol>';
} else {
introHtml =
'<p>To enable <strong>' + escHtml(feat.name) + '</strong>, it needs its own domain from Njal.la. ' +
'In your Njal.la account, set up a domain for it — either a free subdomain or a separate domain. Pick one option:</p>';
}
$domainSetupBody.innerHTML =
'<div class="domain-setup-intro">' +
nwcWarning +
renderDomainNeedsHtml({ serviceName: feat.name, hostExample: hostExample }) +
renderNjallaStepsHtml({ hostExample: hostExample, pasteHint: "below" }) +
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
'<p style="margin-top:10px;">Below, enter the full domain for this service — a subdomain (e.g. ' + domainLabelExample + ') or a separate domain — and paste its curl command.</p>' +
introHtml +
'<details style="margin-top:10px;">' +
'<summary style="cursor:pointer;font-weight:600;">Option A — Free subdomain (recommended)</summary>' +
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
'<li>In Njal.la, open a domain you own and click &quot;Add record&quot;.</li>' +
'<li>Set record type to <strong>Dynamic</strong>.</li>' +
'<li>In the <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>' +
'(Example only, your choice — for &quot;call.yourdomain.com&quot; you&apos;d type just: &nbsp;<code>call</code>)<br>' +
'&#9888; Do NOT type the full domain here — Njal.la adds it automatically.</li>' +
'<li>A Dynamic record has NO IP field — the IP auto-fills after the rebuild/reboot.</li>' +
'<li>Copy the curl command Njal.la gives you, e.g.:<br>' +
'<code style="font-size:0.8em;">curl &quot;https://njal.la/update/?h=call.yourdomain.com&amp;k=abc123&amp;auto&quot;</code></li>' +
'</ol>' +
'</details>' +
'<details style="margin-top:6px;">' +
'<summary style="cursor:pointer;font-weight:600;">Option B — Separate / new domain</summary>' +
'<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">' +
'<li>In Njal.la, buy the domain you want.</li>' +
'<li>Add a Dynamic record as in Option A. If this domain is dedicated to the service, leave the Name field blank or use <code>@</code>.</li>' +
'<li>Copy the curl command Njal.la gives you.</li>' +
'</ol>' +
'</details>' +
'<p style="margin-top:10px;">Below, enter the full domain for this service — a subdomain (e.g. call.yourdomain.com) or a separate domain (e.g. call.com) — and paste its curl command.</p>' +
'</div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" /></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl &quot;https://njal.la/update/?h=' + domainPlaceholder + '&amp;k=abc123&amp;auto&quot;" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. call.yourdomain.com):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="myservice.example.com" /></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl &quot;https://njal.la/update/?h=myservice.example.com&amp;k=abc123&amp;auto&quot;" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
npubField +
'<div class="domain-field-actions"><button class="btn btn-close-modal" id="domain-setup-cancel-btn">Cancel</button><button class="btn btn-primary" id="domain-setup-save-btn">Save &amp; Enable</button></div>';
@@ -136,23 +150,17 @@ function openDomainSetupModal(feat, onSaved) {
} catch (err) {
saveBtn.disabled = false;
saveBtn.textContent = "Save & Enable";
var msg = (err && err.message) ? err.message : "Failed to save domain. Please try again.";
alert(msg);
alert("Failed to save domain. Please try again.");
}
});
$domainSetupModal.classList.add("open");
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
renderRouterPortsBox("domain-router-box");
}
function openDomainReconfigureModal(feat, existingDomain, onSaved) {
if (!$domainSetupModal) return;
if ($domainSetupTitle) $domainSetupTitle.textContent = "🔄 Reconfigure Domain — " + feat.name;
var isWalletConnections = (feat.id === "nwc-wallets" || feat.domain_name === "lightning");
var npubField = "";
if (feat.id === "haven") {
var currentNpub = "";
@@ -167,23 +175,11 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
npubField = '<div class="domain-field-group"><label class="domain-field-label" for="domain-npub-input">Nostr Public Key (npub1...):</label><input class="domain-field-input" type="text" id="domain-npub-input" placeholder="npub1..." value="' + escHtml(currentNpub) + '" /></div>';
}
var nwcWarning = isWalletConnections
? '<div class="domain-nwc-warning">' +
'<strong>⚠ Lightning Wallet Connections requires its own unique hostname.</strong> ' +
'Use a new subdomain such as <code>lightning.yourdomain.com</code>, or a separate domain. ' +
'Do not reuse a domain already assigned to Matrix, Nextcloud, WordPress, BTCPay Server, Vaultwarden, Haven, or another Caddy site.' +
'</div>'
: '';
var domainPlaceholder = isWalletConnections ? "lightning.yourdomain.com" : "myservice.example.com";
var domainLabelExample = isWalletConnections ? "lightning.yourdomain.com" : "call.yourdomain.com";
var externalIp = _cachedExternalIp || "your external IP";
var currentDomain = existingDomain || "";
$domainSetupBody.innerHTML =
'<div class="domain-setup-intro">' +
nwcWarning +
'<p>Your domain <strong>' + escHtml(currentDomain || "this domain") + '</strong> is configured but isn\'t resolving correctly.</p>' +
'<p><strong>Troubleshooting steps:</strong></p>' +
'<ol>' +
@@ -193,12 +189,10 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
'<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' +
'<li>If the IP is wrong or the record is missing, update it</li>' +
'<li>If you changed the DDNS curl command, paste the updated one below</li>' +
'<li>Confirm ports <strong>80</strong> and <strong>443</strong> (TCP) are still forwarded on your router to this computer — see the reminder below:</li>' +
'</ol>' +
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
'</div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" value="' + escHtml(currentDomain) + '" /></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl &quot;https://njal.la/update/?h=' + domainPlaceholder + '&amp;k=abc123&amp;auto&quot;" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. call.yourdomain.com):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="myservice.example.com" value="' + escHtml(currentDomain) + '" /></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl &quot;https://njal.la/update/?h=myservice.example.com&amp;k=abc123&amp;auto&quot;" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
npubField +
'<div class="domain-field-actions"><button class="btn btn-close-modal" id="domain-setup-cancel-btn">Cancel</button><button class="btn btn-primary" id="domain-setup-save-btn">Save &amp; Update</button></div>';
@@ -234,15 +228,11 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
} catch (err) {
saveBtn.disabled = false;
saveBtn.textContent = "Save & Update";
var msg = (err && err.message) ? err.message : "Failed to save domain. Please try again.";
alert(msg);
alert("Failed to save domain. Please try again.");
}
});
$domainSetupModal.classList.add("open");
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
renderRouterPortsBox("domain-router-box");
}
function closeDomainSetupModal() {
@@ -259,12 +249,28 @@ function openPortRequirementsModal(featureName, ports, onContinue) {
: '';
function renderPortRequirements(internalIp) {
var rows = ports.map(function(p) {
return '<tr><td class="port-req-port">' + escHtml(p.port) + '</td>' +
'<td class="port-req-proto">' + escHtml(p.protocol) + '</td>' +
'<td class="port-req-desc">' + escHtml(p.description) + '</td></tr>';
}).join("");
var ipLine = internalIp
? '<p class="port-req-intro">Forward each port below <strong>to this machine\'s internal IP: <code class="port-req-internal-ip">' + escHtml(internalIp) + '</code></strong></p>'
: "<p class=\"port-req-intro\">Forward each port below to this machine's internal LAN IP in your router's port forwarding settings.</p>";
$portReqBody.innerHTML =
renderPortForwardGuideHtml(ports, {
internalIp: internalIp,
serviceName: featureName,
}) +
'<p class="port-req-hint">💡 This list is always available again on the <strong>' + escHtml(featureName) + '</strong> tile.</p>' +
'<p class="port-req-intro"><strong>Port Forwarding Required</strong></p>' +
'<p class="port-req-intro">For <strong>' + escHtml(featureName) + "</strong> to work with clients outside your local network, " +
"you must configure <strong>port forwarding</strong> in your router's admin panel.</p>" +
ipLine +
'<table class="port-req-table">' +
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Purpose</th></tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>' +
"<p class=\"port-req-hint\"><strong>How to verify:</strong> Router-side forwarding cannot be checked from inside your network. " +
"To confirm ports are forwarded correctly, test from a device on a different network (e.g. a phone on mobile data) " +
"or check your router's port forwarding page.</p>" +
'<p class="port-req-hint">ℹ Search "<em>how to set up port forwarding on [your router model]</em>" for step-by-step instructions.</p>' +
'<div class="domain-field-actions">' +
'<button class="btn btn-close-modal" id="port-req-dismiss-btn">Dismiss</button>' +
continueBtn +
@@ -397,10 +403,39 @@ function handleFeatureToggle(feat, newEnabled) {
return;
}
// Always show the full list of ports to forward. Port forwarding happens on
// the router, which this computer cannot inspect — a local check would only
// hide ports the user still has to open.
openPortRequirementsModal(feat.name, ports, proceedAfterPortCheck);
// Check which ports are actually closed before showing the modal
fetch("/api/ports/status", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ports: ports }),
})
.then(function(r) {
if (!r.ok) throw new Error("Port status request failed: " + r.status);
return r.json();
})
.then(function(data) {
var portStatuses = {};
(data.ports || []).forEach(function(p) {
portStatuses[p.port + "/" + p.protocol] = p.status;
});
var closedPorts = ports.filter(function(p) {
var key = p.port + "/" + p.protocol;
var status = portStatuses[key] || "unknown";
return status !== "listening" && status !== "firewall_open";
});
if (closedPorts.length === 0) {
proceedAfterPortCheck();
} else {
openPortRequirementsModal(feat.name, closedPorts, proceedAfterPortCheck);
}
})
.catch(function(err) {
console.warn("Failed to fetch port status for feature enable flow:", err);
// Safe fallback if status check fails
openPortRequirementsModal(feat.name, ports, proceedAfterPortCheck);
});
}
if (feat.id === "bitcoin-core") {
+1 -74
View File
@@ -40,66 +40,6 @@ function linkify(str) {
return escHtml(str).replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer" class="creds-link">$1</a>');
}
// ── Router port-forwarding guide ──────────────────────────────────
// Whether a port is truly reachable can only be judged from OUTSIDE the
// network, so we never show a local "ready" verdict here. We only tell the
// user exactly what to enter in their router.
// Render the protocol cell so TCP / UDP / both is unmistakable.
function portProtocolHtml(protocol) {
var p = String(protocol || "TCP").toUpperCase();
var isTcp = p.indexOf("TCP") !== -1;
var isUdp = p.indexOf("UDP") !== -1;
if (isTcp && isUdp) {
return '<span class="port-proto-badge port-proto-badge--both">TCP + UDP</span>' +
'<span class="port-proto-note">both required</span>';
}
if (isUdp) return '<span class="port-proto-badge port-proto-badge--udp">UDP</span>';
return '<span class="port-proto-badge port-proto-badge--tcp">TCP</span>';
}
// ports: [{ port, protocol, description }]
// opts: { internalIp, serviceName, tableClass, introClass, noteClass }
function renderPortForwardGuideHtml(ports, opts) {
opts = opts || {};
var tableClass = opts.tableClass || "port-req-table";
var introClass = opts.introClass || "port-req-intro";
var noteClass = opts.noteClass || "port-req-hint";
var ipHtml = opts.internalIp
? '<code class="port-req-internal-ip">' + escHtml(opts.internalIp) + '</code>'
: 'this computer&rsquo;s <strong>internal IP</strong> (shown as &ldquo;Internal IP&rdquo; at the top of the Hub dashboard)';
var rows = (ports || []).map(function(p) {
return '<tr>' +
'<td class="port-req-port">' + escHtml(p.port) + '</td>' +
'<td class="port-req-proto">' + portProtocolHtml(p.protocol) + '</td>' +
'<td class="port-req-desc">' + escHtml(p.description || "") + '</td>' +
'</tr>';
}).join("");
var forWhat = opts.serviceName
? 'For <strong>' + escHtml(opts.serviceName) + '</strong> to be reachable from outside your home network, open'
: 'Open';
return '<p class="' + introClass + '">' +
forWhat + ' the ports below in your router&rsquo;s <strong>port forwarding</strong> settings ' +
'and point them at ' + ipHtml + '.' +
'</p>' +
'<ul class="port-req-steps">' +
'<li>Set the <strong>internal (private) port</strong> and the <strong>external (public) port</strong> to the <strong>same number</strong>.</li>' +
'<li>Match the <strong>protocol</strong> exactly — a rule set to TCP will not pass UDP traffic. Where the table says <strong>TCP + UDP</strong>, create both rules (or pick &ldquo;Both&rdquo;/&ldquo;TCP/UDP&rdquo; if your router offers it).</li>' +
'<li>For a range such as <strong>30000-40000</strong>, use your router&rsquo;s port-range fields — start 30000, end 40000 — rather than one rule per port.</li>' +
'</ul>' +
'<table class="' + tableClass + '">' +
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Used for</th></tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>' +
'<p class="' + noteClass + '">' +
'📱 <strong>How to confirm it worked:</strong> forwarding happens on your router, so it can only be verified from outside your network. ' +
'Turn Wi-Fi off on your phone and open the service over mobile data — if it loads, your ports are open.' +
'</p>';
}
function formatDuration(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
@@ -115,20 +55,7 @@ async function apiFetch(path, options) {
const res = await fetch(path, options || {});
if (!res.ok) {
let detail = res.status + " " + res.statusText;
try {
const body = await res.json();
if (body && body.detail) {
if (typeof body.detail === "string") {
detail = body.detail;
} else if (body.detail && typeof body.detail.message === "string") {
detail = body.detail.message;
}
} else if (body && typeof body.message === "string") {
detail = body.message;
} else if (body && typeof body.error === "string") {
detail = body.error;
}
} catch (e) {}
try { const body = await res.json(); if (body && body.detail) detail = body.detail; } catch (e) {}
throw new Error(detail);
}
return res.json();
@@ -8,7 +8,6 @@ function openRebuildModal() {
_rebuildLogOffset = 0;
_rebuildServerDown = false;
_rebuildFinished = false;
_rebuildPollFailures = 0;
if ($rebuildLog) { $rebuildLog.textContent = ""; $rebuildLog.style.display = "none"; }
var action = _rebuildIsEnabling ? "Enabling" : "Disabling";
var label = _rebuildFeatureName || "feature";
@@ -46,7 +45,6 @@ async function pollRebuildStatus() {
if (_rebuildFinished) return;
try {
var data = await apiFetch("/api/rebuild/status?offset=" + _rebuildLogOffset);
_rebuildPollFailures = 0;
if (_rebuildServerDown) { _rebuildServerDown = false; }
if (data.log) appendRebuildLog(data.log);
_rebuildLogOffset = data.offset;
@@ -59,18 +57,6 @@ async function pollRebuildStatus() {
onRebuildDone(data.result === "success");
}
} catch (err) {
_rebuildPollFailures += 1;
// The Hub restarts itself during activation, which briefly drops this poll.
// If polling stays broken long past a normal restart, the page's session
// almost certainly no longer matches the server (e.g. the Hub restarted
// and sessions were not recovered). Reload to re-authenticate and show
// the real feature state instead of spinning forever.
if (_rebuildPollFailures >= STATUS_POLL_MAX_FAILURES) {
_rebuildFinished = true;
stopRebuildPoll();
window.location.reload();
return;
}
if (!_rebuildServerDown) { _rebuildServerDown = true; if ($rebuildStatus) $rebuildStatus.textContent = "Applying changes…"; }
}
}
@@ -2,21 +2,6 @@
// ── Service detail modal ──────────────────────────────────────────
function _getZeusConnectGuideHtml() {
return '<div class="nwc-connect-guide">' +
'<div class="nwc-connect-guide-title">📱 Connect to Zeus</div>' +
'<p class="nwc-connect-guide-intro">This connection URL is an <strong>LND REST</strong> connection — the direct way to use Zeus with your node for full admin access. It connects securely through Tor, allowing you to manage channels, balances, and your full node on the go.</p>' +
'<div class="nwc-connect-steps">' +
'<div class="nwc-connect-step"><div class="nwc-step-num">1</div><div><strong>Download Zeus</strong> from the App Store or Google Play.</div></div>' +
'<div class="nwc-connect-step"><div class="nwc-step-num">2</div><div>Open Zeus and open the <strong>Wallets</strong> screen.</div></div>' +
'<div class="nwc-connect-step"><div class="nwc-step-num">3</div><div>Tap the <strong>+ (Add Wallet)</strong> button in the top-right corner.</div></div>' +
'<div class="nwc-connect-step"><div class="nwc-step-num">4</div><div>On <strong>Wallet Configuration</strong>, tap the <strong>scan icon</strong> in the top-right corner, then scan the QR code above.</div></div>' +
'<div class="nwc-connect-step"><div class="nwc-step-num">5</div><div>Zeus detects the LND REST QR and fills in the connection details. Review them, then tap <strong>Save Wallet Config</strong>.</div></div>' +
'</div>' +
'<div class="nwc-connect-note"><strong>💡 Note:</strong> This is <em>not</em> the same as the NWC pairing QR shown in Lightning Wallet Connections — that gives your wallet sandboxed, limited access for everyday spending. LND REST connects Zeus directly to your node for full admin control.</div>' +
'</div>';
}
function _renderCredsHtml(credentials, unit) {
var html = "";
for (var i = 0; i < credentials.length; i++) {
@@ -24,10 +9,7 @@ function _renderCredsHtml(credentials, unit) {
var id = "cred-" + Math.random().toString(36).substring(2, 8);
var qrBlock = "";
if (cred.qrcode) {
var qrHint = (unit === "zeus-connect-setup.service")
? "This is an <strong>LND REST</strong> connection QR — in Zeus, add a wallet and use the scan icon (see steps below)."
: "In Zeus: <em>Wallets → + → scan icon</em>. This is an <strong>LND REST</strong> QR for direct node access.";
qrBlock = '<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + cred.qrcode + '" alt="QR Code for ' + escHtml(cred.label) + '"><div class="creds-qr-hint">' + qrHint + '</div></div>';
qrBlock = '<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + cred.qrcode + '" alt="QR Code for ' + escHtml(cred.label) + '"><div class="creds-qr-hint">Scan with Zeus app on your phone</div></div>';
}
// If qronly, render the label + QR block only — skip value and copy button
if (cred.qronly) {
@@ -76,562 +58,8 @@ function _attachCopyHandlers(container) {
});
}
var _nwcModalState = null;
function _isNwcServiceUnit(unit) {
// The dashboard tile is backed by Alby Hub, while older drafts referred to
// the logical feature as nwc-wallets.service. Support both so the Wallet
// Connections manager appears from the actual tile and any legacy configs.
return unit === "albyhub.service" || unit === "nwc-wallets.service";
}
function _nwcStateMessageHtml() {
if (!_nwcModalState || !_nwcModalState.message) return "";
var msgClass = _nwcModalState.messageKind === "success" ? "success" : "error";
return '<div class="matrix-form-result ' + msgClass + '">' + escHtml(_nwcModalState.message) + '</div>';
}
function _nwcSetMessage(kind, text) {
if (!_nwcModalState) return;
_nwcModalState.messageKind = kind || "error";
_nwcModalState.message = text || "";
}
function _nwcClearMessage() {
if (!_nwcModalState) return;
_nwcModalState.messageKind = "";
_nwcModalState.message = "";
}
function _nwcWalletSectionEl() {
return document.getElementById("nwc-wallets-body");
}
function _nwcFormatSats(value) {
var n = Number(value || 0);
if (!Number.isFinite(n)) n = 0;
return n.toLocaleString("en-US");
}
// "Go to Service & Setup" shortcut shown when no Lightning Address domain
// is configured yet — jumps to the sibling tab instead of dead-ending.
function _nwcWireDomainLink() {
var link = document.getElementById("nwc-domain-setup-link");
if (link) link.addEventListener("click", function() { _nwcActivateTab("setup"); });
}
// ── Lightning Wallet Connections: tabbed modal shell ──────────────
// The wallet manager is a small app in its own right, so it gets the full
// width of the dialog on a "Wallets" tab, while status / enable / domain /
// ports / restart live on a "Service & Setup" tab.
function _setCredsDialogWide(on) {
if (!$credsModal) return;
var dialog = $credsModal.querySelector(".creds-dialog");
if (dialog) dialog.classList.toggle("creds-dialog--wide", !!on);
}
function _nwcTabsShellHtml(opts) {
var domainChip = opts.domain
? '<span class="nwc-header-chip nwc-header-chip--domain" title="Lightning Address domain">🌐 ' + escHtml(opts.domain) + '</span>'
: '<span class="nwc-header-chip nwc-header-chip--warn" title="No Lightning Address domain configured yet">🌐 Domain not set</span>';
return '<div class="nwc-tabs" data-active="' + escHtml(opts.activeTab) + '">' +
'<div class="nwc-tabs-bar" role="tablist" aria-label="Lightning Wallet Connections sections">' +
'<div class="nwc-tabs-btns">' +
'<button class="nwc-tab-btn" role="tab" data-tab="wallets" id="nwc-tab-btn-wallets" aria-controls="nwc-tab-wallets">⚡ Wallets</button>' +
'<button class="nwc-tab-btn" role="tab" data-tab="setup" id="nwc-tab-btn-setup" aria-controls="nwc-tab-setup">⚙ Service &amp; Setup</button>' +
'</div>' +
'<div class="nwc-header-chips">' +
'<span class="nwc-header-chip"><span class="status-dot ' + opts.statusDotClass + '"></span>' + escHtml(opts.statusLabel) + '</span>' +
domainChip +
'</div>' +
'</div>' +
'<div class="nwc-tab-panel" role="tabpanel" id="nwc-tab-wallets" aria-labelledby="nwc-tab-btn-wallets" data-tab="wallets">' + opts.walletsHtml + '</div>' +
'<div class="nwc-tab-panel" role="tabpanel" id="nwc-tab-setup" aria-labelledby="nwc-tab-btn-setup" data-tab="setup">' + opts.setupHtml + '</div>' +
'</div>';
}
function _nwcActivateTab(tab) {
var root = document.querySelector(".nwc-tabs");
if (!root) return;
root.setAttribute("data-active", tab);
root.querySelectorAll(".nwc-tab-btn").forEach(function(btn) {
var on = btn.getAttribute("data-tab") === tab;
btn.classList.toggle("active", on);
btn.setAttribute("aria-selected", on ? "true" : "false");
});
root.querySelectorAll(".nwc-tab-panel").forEach(function(panel) {
panel.classList.toggle("active", panel.getAttribute("data-tab") === tab);
});
var body = document.getElementById("creds-body");
if (body) body.scrollTop = 0;
}
function _nwcWireTabs() {
var root = document.querySelector(".nwc-tabs");
if (!root) return;
root.querySelectorAll(".nwc-tab-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
_nwcActivateTab(btn.getAttribute("data-tab"));
});
});
_nwcActivateTab(root.getAttribute("data-active") || "wallets");
var gotoSetup = document.getElementById("nwc-goto-setup-btn");
if (gotoSetup) gotoSetup.addEventListener("click", function() { _nwcActivateTab("setup"); });
}
function _nwcRenderWalletState() {
var host = _nwcWalletSectionEl();
if (!host || !_nwcModalState) return;
var state = _nwcModalState;
var html = "";
html += _nwcStateMessageHtml();
if (state.view === "create") {
var selectedLimited = state.createForm.access_preset === "send_receive_limited";
html +=
'<div class="nwc-tab-intro-title">Create a Wallet</div>' +
'<p class="nwc-tab-intro-desc">Create a secure, sandboxed wallet for a specific app. Each wallet gets its own Lightning Address and optional spending limit.</p>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-name">Wallet Name</label>' +
'<input class="matrix-form-input" id="nwc-wallet-name" type="text" placeholder="My Wallet" value="' + escHtml(state.createForm.name || "") + '" autocomplete="off"></div>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-alias">Lightning Address Alias</label>' +
'<input class="matrix-form-input" id="nwc-wallet-alias" type="text" placeholder="my-wallet" value="' + escHtml(state.createForm.alias || "") + '" autocomplete="off">' +
'<div class="creds-qr-hint">Lowercase letters, numbers, "_" and "-" only.</div></div>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-preset">Access Preset</label>' +
'<select class="matrix-form-input" id="nwc-wallet-preset">' +
'<option value="receive_only"' + (state.createForm.access_preset === "receive_only" ? " selected" : "") + '>Receive only</option>' +
'<option value="send_receive_limited"' + (selectedLimited ? " selected" : "") + '>Send + receive (limited)</option>' +
'</select></div>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-limit">Spending Limit (sats)</label>' +
'<input class="matrix-form-input" id="nwc-wallet-limit" type="number" min="1" step="1" placeholder="50000" value="' + escHtml(state.createForm.spending_limit_sats || "") + '"' + (selectedLimited ? "" : " disabled") + "></div>" +
'<div class="matrix-form-actions">' +
'<button class="matrix-form-back" id="nwc-create-cancel-btn"' + (state.busy ? " disabled" : "") + '>← Back</button>' +
'<button class="matrix-form-submit" id="nwc-create-submit-btn"' + (state.busy ? " disabled" : "") + '>' + (state.busy ? "Creating…" : "Create Wallet") + '</button>' +
'</div>';
host.innerHTML = html;
var presetSel = document.getElementById("nwc-wallet-preset");
if (presetSel) {
presetSel.addEventListener("change", function() {
state.createForm.access_preset = presetSel.value;
_nwcRenderWalletState();
});
}
var cancelBtn = document.getElementById("nwc-create-cancel-btn");
if (cancelBtn) {
cancelBtn.addEventListener("click", function() {
if (state.busy) return;
state.view = state.wallets.length > 0 ? "list" : "empty";
_nwcClearMessage();
_nwcRenderWalletState();
});
}
var submitBtn = document.getElementById("nwc-create-submit-btn");
if (submitBtn) submitBtn.addEventListener("click", _nwcCreateWallet);
return;
}
if (state.view === "created" && state.lastCreated) {
var created = state.lastCreated;
var pairId = "nwc-pairing-uri-" + Math.random().toString(36).substring(2, 8);
html += '<div class="nwc-secret-warning">⚠ One-time pairing secret. Save it now — it will not be shown again.</div>';
if (created.pairing_qrcode) {
html += '<div class="creds-row"><div class="creds-label">QR Code</div>' +
'<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + created.pairing_qrcode + '" alt="QR code for Lightning Wallet Connections pairing secret"><div class="creds-qr-hint">This is an <strong>NWC</strong> pairing QR — in Zeus, add a wallet and use the scan icon (see steps below).</div></div></div>';
}
html += '<div class="creds-row"><div class="creds-label">Pairing URI</div>' +
'<div class="creds-value-wrap"><div class="creds-value" id="' + pairId + '">' + escHtml(created.pairing_uri || "Unavailable") + '</div><button class="creds-copy-btn" data-target="' + pairId + '">Copy</button></div></div>';
if (created.wallet && created.wallet.lightning_address) {
html += '<div class="creds-row"><div class="creds-label">Lightning Address</div>' +
'<div class="creds-value-wrap"><div class="creds-value">' + escHtml(created.wallet.lightning_address) + '</div></div></div>';
}
html += '<div class="nwc-connect-guide">' +
'<div class="nwc-connect-guide-title">📱 Connect to Zeus</div>' +
'<p class="nwc-connect-guide-intro">This pairing URI is an <strong>NWC (Nostr Wallet Connect)</strong> connection — the modern, mobile-friendly way to use Zeus with your node. It connects directly through your Lightning domain, so no Tor or port forwarding is needed on your phone.</p>' +
'<div class="nwc-connect-steps">' +
'<div class="nwc-connect-step"><div class="nwc-step-num">1</div><div><strong>Download Zeus</strong> from the App Store or Google Play.</div></div>' +
'<div class="nwc-connect-step"><div class="nwc-step-num">2</div><div>Open Zeus and open the <strong>Wallets</strong> screen.</div></div>' +
'<div class="nwc-connect-step"><div class="nwc-step-num">3</div><div>Tap the <strong>+ (Add Wallet)</strong> button in the top-right corner.</div></div>' +
'<div class="nwc-connect-step"><div class="nwc-step-num">4</div><div>On <strong>Wallet Configuration</strong>, tap the <strong>scan icon</strong> in the top-right corner, then scan the QR code above.</div></div>' +
'<div class="nwc-connect-step"><div class="nwc-step-num">5</div><div>Zeus detects the NWC QR and fills in <strong>Nostr Wallet Connect</strong>. Review it, then tap <strong>Save Wallet Config</strong>.</div></div>' +
'</div>' +
'<div class="nwc-connect-note"><strong>💡 Note:</strong> This is <em>not</em> the same as the LND REST / Tor QR shown on your LND tile — that connects Zeus directly to your Lightning node for full admin control. NWC gives your wallet sandboxed, limited access for everyday spending.</div>' +
'</div>';
html += '<div class="matrix-form-actions">' +
'<button class="matrix-form-back" id="nwc-created-another-btn"' + (state.busy ? " disabled" : "") + '>Create Another Wallet</button>' +
'<button class="matrix-form-submit" id="nwc-created-continue-btn"' + (state.busy ? " disabled" : "") + '>I Saved This Secret</button>' +
'</div>';
host.innerHTML = html;
_attachCopyHandlers(host);
var continueBtn = document.getElementById("nwc-created-continue-btn");
if (continueBtn) {
continueBtn.addEventListener("click", async function() {
if (state.busy) return;
state.lastCreated = null;
state.view = "list";
await _nwcRefreshWallets();
});
}
var anotherBtn = document.getElementById("nwc-created-another-btn");
if (anotherBtn) {
anotherBtn.addEventListener("click", function() {
if (state.busy) return;
state.view = "create";
_nwcClearMessage();
_nwcRenderWalletState();
});
}
return;
}
if (state.view === "share" && state.shareWallet) {
var sw = state.shareWallet;
var swId = encodeURIComponent(String(sw.id || sw.pubkey || ""));
var swAddress = sw.lightning_address || ((sw.alias && state.domain) ? sw.alias + "@" + state.domain : "");
var pngUrl = "/api/nwc/wallets/" + swId + "/lnurl-qr.png";
var shareAddrId = "nwc-share-addr-" + Math.random().toString(36).substring(2, 8);
var shareLnurlId = "nwc-share-lnurl-" + Math.random().toString(36).substring(2, 8);
html += '<p class="svc-detail-desc">Share the Lightning Address for <strong>' + escHtml(sw.name || "Wallet") + '</strong>. This QR never expires — anyone can scan it with any Lightning wallet to send sats, as many times as they like.</p>' +
'<div class="creds-qr-wrap">' +
'<img class="creds-qr-img" src="' + pngUrl + '" alt="LNURL QR code for ' + escHtml(swAddress) + '">' +
'<div class="creds-qr-hint">Scan with any Lightning wallet to pay ' + escHtml(swAddress) + '</div>' +
'</div>' +
(swAddress
? '<div class="creds-row"><div class="creds-label">Lightning Address</div><div class="creds-value-wrap"><div class="creds-value" id="' + shareAddrId + '">' + escHtml(swAddress) + '</div><button class="creds-copy-btn" data-target="' + shareAddrId + '">Copy</button></div></div>'
: "") +
'<div class="creds-row"><div class="creds-label">LNURL</div><div class="creds-value-wrap"><div class="creds-value nwc-lnurl-value" id="' + shareLnurlId + '">Loading…</div><button class="creds-copy-btn" data-target="' + shareLnurlId + '">Copy</button></div></div>' +
'<div class="nwc-share-actions">' +
'<button class="matrix-form-submit" id="nwc-share-dl-png-btn">⬇ Download PNG</button>' +
'<button class="matrix-form-back" id="nwc-share-dl-svg-btn">⬇ Download SVG</button>' +
'<button class="matrix-form-back" id="nwc-share-print-btn">🖨 Print…</button>' +
'<button class="matrix-form-back" id="nwc-share-back-btn">← Back</button>' +
'</div>' +
'<div class="creds-qr-hint nwc-share-formats-hint">PNG is ideal for websites and social posts. SVG stays sharp at any print size.</div>';
host.innerHTML = html;
_attachCopyHandlers(host);
apiFetch("/api/nwc/wallets/" + swId + "/lnurl").then(function(payload) {
var el = document.getElementById(shareLnurlId);
if (el) el.textContent = payload.lnurl || "Unavailable";
}).catch(function() {
var el = document.getElementById(shareLnurlId);
if (el) el.textContent = "Unavailable";
});
function _nwcShareDownload(url) {
var a = document.createElement("a");
a.href = url;
a.rel = "noopener";
document.body.appendChild(a);
a.click();
a.remove();
}
var dlPngBtn = document.getElementById("nwc-share-dl-png-btn");
if (dlPngBtn) dlPngBtn.addEventListener("click", function() { _nwcShareDownload(pngUrl + "?download=1"); });
var dlSvgBtn = document.getElementById("nwc-share-dl-svg-btn");
if (dlSvgBtn) dlSvgBtn.addEventListener("click", function() { _nwcShareDownload("/api/nwc/wallets/" + swId + "/lnurl-qr.svg?download=1"); });
var printBtn = document.getElementById("nwc-share-print-btn");
if (printBtn) printBtn.addEventListener("click", function() { window.open("/api/nwc/wallets/" + swId + "/lnurl-qr/print", "_blank", "noopener"); });
var shareBackBtn = document.getElementById("nwc-share-back-btn");
if (shareBackBtn) {
shareBackBtn.addEventListener("click", function() {
state.view = "list";
state.shareWallet = null;
_nwcClearMessage();
_nwcRenderWalletState();
});
}
return;
}
var canCreate = !!state.domain && !state.busy;
var walletCount = (state.wallets || []).length;
html += '<div class="nwc-toolbar">' +
'<div class="nwc-toolbar-count">' + (walletCount === 1 ? '1 wallet' : walletCount + ' wallets') + '</div>' +
'<div class="nwc-toolbar-actions">' +
'<button class="matrix-form-back nwc-toolbar-refresh" id="nwc-refresh-btn"' + (state.busy ? " disabled" : "") + '>↻ Refresh</button>' +
'<button class="matrix-action-btn" id="nwc-open-create-btn"' + (canCreate ? "" : " disabled") + '>➕ New Wallet</button>' +
'</div>' +
'</div>';
if (!state.domain) {
html += '<div class="nwc-domain-required">' +
'<strong>⚠ Lightning Address domain required</strong>' +
'<div class="nwc-domain-required-body">Set up a unique hostname such as <strong>lightning.yourdomain.com</strong> before creating wallets.</div>' +
'<button class="btn btn-primary nwc-domain-required-btn" id="nwc-domain-setup-link">⚙ Go to Service &amp; Setup</button>' +
'</div>';
}
if (!state.wallets || state.wallets.length === 0) {
html += '<div class="nwc-empty-state">' +
'<div class="nwc-empty-icon">⚡</div>' +
'<div class="nwc-empty-title">Ready to start spending</div>' +
'<p class="nwc-empty-desc">Create your first isolated wallet to connect to apps like Zeus (via NWC) or Nostr. Experience faster, more secure Lightning payments today.</p>' +
'</div>';
host.innerHTML = html;
_nwcWireDomainLink();
var openCreateBtn = document.getElementById("nwc-open-create-btn");
if (openCreateBtn) {
openCreateBtn.addEventListener("click", function() {
if (state.busy) return;
_nwcClearMessage();
state.view = "create";
_nwcRenderWalletState();
});
}
var refreshBtnEmpty = document.getElementById("nwc-refresh-btn");
if (refreshBtnEmpty) refreshBtnEmpty.addEventListener("click", _nwcRefreshWallets);
return;
}
html += '<div class="nwc-wallet-list">';
state.wallets.forEach(function(wallet) {
var id = wallet.id || wallet.pubkey || "";
var addressId = "nwc-wallet-addr-" + Math.random().toString(36).substring(2, 8);
var pending = Number(wallet.pending_transactions || 0);
html += '<div class="nwc-wallet-card">' +
'<div class="nwc-wallet-card-head">' +
'<div class="nwc-wallet-card-title">' + escHtml(wallet.name || "Wallet") + '</div>' +
'<div class="nwc-wallet-chips">' +
'<span class="nwc-wallet-chip nwc-wallet-chip--balance">' + escHtml(_nwcFormatSats(wallet.balance_sats)) + ' sats</span>' +
(pending > 0
? '<span class="nwc-wallet-chip nwc-wallet-chip--pending" title="Pending transactions">⏳ ' + escHtml(String(pending)) + ' pending</span>'
: '') +
'</div>' +
'</div>' +
(wallet.lightning_address
? '<div class="nwc-wallet-address">' +
'<div class="creds-label">Lightning Address</div>' +
'<div class="creds-value-wrap"><div class="creds-value" id="' + addressId + '">' + escHtml(wallet.lightning_address) + '</div>' +
'<button class="creds-copy-btn" data-target="' + addressId + '">Copy</button></div>' +
'</div>'
: '<div class="nwc-wallet-address"><div class="creds-label">Alias</div>' +
'<div class="creds-value-wrap"><div class="creds-value">' + escHtml(wallet.alias || "") + '</div></div></div>') +
'<div class="nwc-wallet-actions">' +
'<button class="matrix-action-btn nwc-wallet-action-btn" data-action="share" data-wallet-id="' + escHtml(id) + '"' + ((state.busy || !wallet.lightning_address) ? " disabled" : "") + '>⚡ Share QR</button>' +
'<button class="matrix-form-back nwc-wallet-action-btn" data-action="test" data-wallet-alias="' + escHtml(wallet.alias || "") + '"' + (state.busy ? " disabled" : "") + '>Verify</button>' +
'<span class="nwc-wallet-actions-spacer"></span>' +
'<button class="matrix-form-back nwc-wallet-action-btn nwc-wallet-danger-btn" data-action="drain" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Drain</button>' +
'<button class="matrix-form-back nwc-wallet-action-btn nwc-wallet-danger-btn" data-action="delete" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Delete</button>' +
'</div>' +
'</div>';
});
html += "</div>";
host.innerHTML = html;
_attachCopyHandlers(host);
_nwcWireDomainLink();
var createBtn = document.getElementById("nwc-open-create-btn");
if (createBtn) {
createBtn.addEventListener("click", function() {
if (state.busy) return;
_nwcClearMessage();
state.view = "create";
_nwcRenderWalletState();
});
}
var refreshBtn = document.getElementById("nwc-refresh-btn");
if (refreshBtn) refreshBtn.addEventListener("click", _nwcRefreshWallets);
host.querySelectorAll(".nwc-wallet-action-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
var action = btn.getAttribute("data-action");
if (action === "share") _nwcOpenShare(btn.getAttribute("data-wallet-id"));
else if (action === "test") _nwcVerifyWallet(btn.getAttribute("data-wallet-alias"));
else if (action === "drain") _nwcDrainWallet(btn.getAttribute("data-wallet-id"));
else if (action === "delete") _nwcDeleteWallet(btn.getAttribute("data-wallet-id"));
});
});
}
function _nwcOpenShare(walletId) {
if (!_nwcModalState || _nwcModalState.busy || !walletId) return;
var wallet = null;
for (var i = 0; i < _nwcModalState.wallets.length; i++) {
var w = _nwcModalState.wallets[i];
if (String(w.id || w.pubkey || "") === String(walletId)) { wallet = w; break; }
}
if (!wallet) {
_nwcSetMessage("error", "Wallet connection not found.");
_nwcRenderWalletState();
return;
}
_nwcClearMessage();
_nwcModalState.shareWallet = wallet;
_nwcModalState.view = "share";
_nwcRenderWalletState();
}
async function _nwcRefreshWallets() {
if (!_nwcModalState) return;
var host = _nwcWalletSectionEl();
if (host) host.innerHTML = '<p class="creds-loading">Loading wallet connections…</p>';
try {
var payload = await apiFetch("/api/nwc/wallets");
_nwcModalState.wallets = Array.isArray(payload.wallets) ? payload.wallets : [];
_nwcModalState.domain = payload.domain || null;
if (_nwcModalState.view !== "create" && _nwcModalState.view !== "created" && _nwcModalState.view !== "share") {
_nwcModalState.view = _nwcModalState.wallets.length > 0 ? "list" : "empty";
}
_nwcRenderWalletState();
} catch (err) {
_nwcSetMessage("error", (err && err.message) ? err.message : "Could not load wallet connections.");
_nwcRenderWalletState();
}
}
function _nwcBusy(on) {
if (!_nwcModalState) return;
_nwcModalState.busy = !!on;
}
async function _nwcCreateWallet() {
if (!_nwcModalState || _nwcModalState.busy) return;
var nameEl = document.getElementById("nwc-wallet-name");
var aliasEl = document.getElementById("nwc-wallet-alias");
var presetEl = document.getElementById("nwc-wallet-preset");
var limitEl = document.getElementById("nwc-wallet-limit");
if (!nameEl || !aliasEl || !presetEl || !limitEl) return;
var name = (nameEl.value || "").trim();
var alias = (aliasEl.value || "").trim().toLowerCase();
var preset = presetEl.value || "receive_only";
var limitRaw = (limitEl.value || "").trim();
var limit = null;
_nwcModalState.createForm = {
name: name,
alias: alias,
access_preset: preset,
spending_limit_sats: limitRaw
};
if (!name) {
_nwcSetMessage("error", "Wallet name is required.");
_nwcRenderWalletState();
return;
}
if (!/^[a-z0-9][a-z0-9_-]{0,31}$/.test(alias)) {
_nwcSetMessage("error", 'Alias must start with a letter or number and use only lowercase letters, numbers, "_" or "-".');
_nwcRenderWalletState();
return;
}
if (preset === "send_receive_limited") {
limit = parseInt(limitRaw, 10);
if (!Number.isFinite(limit) || limit <= 0) {
_nwcSetMessage("error", "A positive spending limit is required for limited send access.");
_nwcRenderWalletState();
return;
}
}
_nwcBusy(true);
_nwcClearMessage();
_nwcRenderWalletState();
try {
var payload = await apiFetch("/api/nwc/wallets", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name,
alias: alias,
access_preset: preset,
spending_limit_sats: preset === "send_receive_limited" ? limit : null
})
});
_nwcModalState.lastCreated = {
wallet: payload.wallet || null,
pairing_uri: payload.pairing_uri || "",
pairing_qrcode: payload.pairing_qrcode || "",
lightning_address: payload.lightning_address || null
};
_nwcModalState.view = "created";
_nwcSetMessage("success", "Wallet created. Save the one-time secret before you continue.");
_nwcBusy(false);
_nwcRenderWalletState();
} catch (err) {
_nwcBusy(false);
_nwcSetMessage("error", (err && err.message) ? err.message : "Failed to create wallet connection.");
_nwcRenderWalletState();
}
}
async function _nwcVerifyWallet(alias) {
if (!_nwcModalState || _nwcModalState.busy || !alias) return;
_nwcBusy(true);
_nwcClearMessage();
_nwcRenderWalletState();
try {
await apiFetch("/api/nwc/addresses/" + encodeURIComponent(alias) + "/test", { method: "POST" });
_nwcSetMessage("success", "Lightning Address verification succeeded for " + alias + ".");
} catch (err) {
_nwcSetMessage("error", (err && err.message) ? err.message : "Lightning Address verification failed.");
}
_nwcBusy(false);
_nwcRenderWalletState();
}
async function _nwcDrainWallet(walletId) {
if (!_nwcModalState || _nwcModalState.busy || !walletId) return;
if (!window.confirm("Drain this wallet connection now? This cannot be undone.")) return;
_nwcBusy(true);
_nwcClearMessage();
_nwcRenderWalletState();
try {
var resp = await apiFetch("/api/nwc/wallets/" + encodeURIComponent(walletId) + "/drain", { method: "POST" });
_nwcSetMessage("success", "Wallet drained (" + String(resp.drained_sats || 0) + " sats).");
_nwcBusy(false);
await _nwcRefreshWallets();
} catch (err) {
_nwcBusy(false);
_nwcSetMessage("error", (err && err.message) ? err.message : "Failed to drain wallet.");
_nwcRenderWalletState();
}
}
async function _nwcDeleteWallet(walletId) {
if (!_nwcModalState || _nwcModalState.busy || !walletId) return;
if (!window.confirm("Delete this wallet connection? This removes the wallet alias from Lightning Wallet Connections.")) return;
_nwcBusy(true);
_nwcClearMessage();
_nwcRenderWalletState();
try {
await apiFetch("/api/nwc/wallets/" + encodeURIComponent(walletId), { method: "DELETE" });
_nwcSetMessage("success", "Wallet connection deleted.");
_nwcBusy(false);
await _nwcRefreshWallets();
} catch (err) {
_nwcBusy(false);
_nwcSetMessage("error", (err && err.message) ? err.message : "Failed to delete wallet connection.");
_nwcRenderWalletState();
}
}
async function _nwcInitWalletFlow(unit, name, icon) {
_nwcModalState = {
unit: unit,
name: name,
icon: icon,
view: "empty",
wallets: [],
domain: null,
busy: false,
message: "",
messageKind: "",
lastCreated: null,
shareWallet: null,
createForm: {
name: "",
alias: "",
access_preset: "receive_only",
spending_limit_sats: ""
}
};
await _nwcRefreshWallets();
}
async function openServiceDetailModal(unit, name, icon) {
if (!$credsModal) return;
var isNwc = _isNwcServiceUnit(unit);
_setCredsDialogWide(isNwc);
if ($credsTitle) {
$credsTitle.innerHTML = '';
if (icon) {
@@ -653,38 +81,13 @@ async function openServiceDetailModal(unit, name, icon) {
var url = "/api/service-detail/" + encodeURIComponent(unit);
if (icon) url += "?icon=" + encodeURIComponent(icon);
var data = await apiFetch(url);
// Append version badge next to title if version is detected
var serviceVersion = data.version || data.bitcoin_version || '';
if (serviceVersion && $credsTitle) {
var existingBadge = $credsTitle.querySelector(".creds-title-version-badge");
if (existingBadge) {
existingBadge.textContent = serviceVersion;
} else {
var badge = document.createElement("span");
badge.className = "creds-title-version-badge";
badge.textContent = serviceVersion;
$credsTitle.appendChild(badge);
}
}
// Two content buckets. For most services everything lands in `html` and is
// rendered as one column, exactly as before. For Lightning Wallet
// Connections the day-to-day wallet manager (html) is split away from the
// service plumbing — status, enable/disable, domain, ports, restart
// (setupHtml) — and the two are rendered as tabs so neither feels cramped.
var html = "";
var setupHtml = "";
function addSetup(chunk) {
if (isNwc) setupHtml += chunk;
else html += chunk;
}
// Section A: Description
if (data.description) {
addSetup('<div class="svc-detail-section">' +
html += '<div class="svc-detail-section">' +
'<p class="svc-detail-desc">' + escHtml(data.description) + '</p>' +
'</div>');
'</div>';
}
// Section B: Status
@@ -696,13 +99,13 @@ async function openServiceDetailModal(unit, name, icon) {
: (data.health || data.status);
var sc = statusClass(effectiveHealth);
var st = statusText(effectiveHealth, effectiveEnabled);
addSetup('<div class="svc-detail-section">' +
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Status</div>' +
'<div class="svc-detail-status">' +
'<span class="status-dot ' + sc + '"></span>' +
'<span>' + escHtml(st) + '</span>' +
'</div>' +
'</div>');
'</div>';
// Section B2: BIP-110 live status (bip110 tile only)
if (icon === 'bip110' && data.bip110) {
@@ -744,130 +147,105 @@ async function openServiceDetailModal(unit, name, icon) {
domainActionHtml = '<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-reconfig-domain-btn">🔄 Reconfigure Domain</button>';
}
addSetup('<div class="svc-detail-section">' +
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Domain Diagnostic Checklist</div>' +
stepsHtml +
domainActionHtml +
'</div>');
'</div>';
if (data.router_ports && data.router_ports.length > 0) {
if (unit === "livekit.service" && data.extra_ports && data.extra_ports.length > 0) {
var trimmedInternalIp = data.internal_ip ? String(data.internal_ip).trim() : "";
addSetup('<div class="svc-detail-section">' +
var internalIp = trimmedInternalIp || "";
var internalIpHtml = internalIp ? escHtml(internalIp) : "Could not detect";
var routerIpHelp = internalIp
? "Use this IP address as the destination/internal IP when creating each router forwarding rule."
: "Use this computer’s internal IP as the destination/internal IP when creating each router forwarding rule.";
var routerNextStep = internalIp
? 'Next step: Log in to your router and create forwarding rules for the ports above. Set the destination/internal IP to <strong>' + internalIpHtml + '</strong>.'
: 'Next step: Log in to your router and create forwarding rules for the ports above. Use this computer’s internal IP as the destination/internal IP.';
var domainConfigured = !!(data.domain && String(data.domain).trim());
var extraRows = "";
data.extra_ports.forEach(function(p) {
var statusIcon, statusClass2;
if (!effectiveEnabled) {
statusIcon = "⚠ Configure Element Call first";
statusClass2 = "port-status-open";
} else if (!domainConfigured) {
statusIcon = "⚠ Configure domain first";
statusClass2 = "port-status-open";
} else if (p.status === "listening") {
statusIcon = "✅ Ready";
statusClass2 = "port-status-listening";
} else if (p.status === "firewall_open") {
statusIcon = "✅ Ready";
statusClass2 = "port-status-open";
} else if (p.status === "closed") {
statusIcon = "❌ Not ready yet";
statusClass2 = "port-status-closed";
} else {
statusIcon = "— Could not check";
statusClass2 = "port-status-unknown";
}
extraRows += '<tr>' +
'<td class="svc-detail-port-table-port">' + escHtml(p.port) + '</td>' +
'<td class="svc-detail-port-table-proto">' + escHtml(p.protocol) + '</td>' +
'<td class="svc-detail-port-table-desc">' + escHtml(p.description || "") + '</td>' +
'<td class="svc-detail-port-table-status ' + statusClass2 + '">' + statusIcon + '</td>' +
'</tr>';
});
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' +
renderPortForwardGuideHtml(data.router_ports, {
internalIp: trimmedInternalIp || null,
tableClass: "svc-detail-port-table",
introClass: "svc-detail-port-note",
noteClass: "svc-detail-port-note",
}) +
'</div>');
'<div class="svc-detail-port-note">Forward these ports in your router to this Sovran_SystemsOS computer.</div>' +
'<div class="svc-detail-port-note"><strong>Router Forward-To IP:</strong> ' + internalIpHtml + '</div>' +
'<div class="svc-detail-port-note">' + routerIpHelp + '</div>' +
'<table class="svc-detail-port-table">' +
'<thead><tr><th>Port</th><th>Protocol</th><th>Used For</th><th>Sovran_SystemsOS Status</th></tr></thead>' +
'<tbody>' + extraRows + '</tbody>' +
'</table>' +
'<div class="svc-detail-port-note">The Hub can check whether Sovran_SystemsOS is ready on this computer, but full public port verification requires an outside internet check.</div>' +
'<div class="svc-detail-port-note">' + routerNextStep + '</div>' +
'</div>';
}
} else if (data.port_statuses && data.port_statuses.length > 0) {
// Non-domain services (e.g. SSH): show what to forward, plus a short
// note if the service isn't actually listening on this computer yet.
var localInternalIp = data.internal_ip ? String(data.internal_ip).trim() : "";
var notListening = data.port_statuses.filter(function(p) {
return p.status === "closed";
// Non-domain services (SSH) keep local single-port checks.
var portTableRows = "";
data.port_statuses.forEach(function(p) {
var statusIcon, statusClass2;
if (p.status === "listening") {
statusIcon = "✅ Ready";
statusClass2 = "port-status-listening";
} else if (p.status === "firewall_open") {
statusIcon = "✅ Ready";
statusClass2 = "port-status-open";
} else if (p.status === "closed") {
statusIcon = "❌ Not ready";
statusClass2 = "port-status-closed";
} else {
statusIcon = "— Could not check";
statusClass2 = "port-status-unknown";
}
portTableRows += '<tr>' +
'<td class="svc-detail-port-table-port">' + escHtml(p.port) + '</td>' +
'<td class="svc-detail-port-table-proto">' + escHtml(p.protocol) + '</td>' +
'<td class="svc-detail-port-table-desc">' + escHtml(p.description || "") + '</td>' +
'<td class="svc-detail-port-table-status ' + statusClass2 + '">' + statusIcon + '</td>' +
'</tr>';
});
var localNote = notListening.length
? '<div class="svc-detail-port-note port-status-closed">' +
'⚠ Port ' + escHtml(notListening.map(function(p) { return p.port; }).join(", ")) +
' is not open on this computer yet — enable the service below first, then forward it in your router.' +
'</div>'
: "";
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' +
'<div class="svc-detail-port-note">Only needed if you want to reach this service from <strong>outside</strong> your home network. On your local network it already works without any router changes.</div>' +
renderPortForwardGuideHtml(data.port_statuses, {
internalIp: localInternalIp || null,
tableClass: "svc-detail-port-table",
introClass: "svc-detail-port-note",
noteClass: "svc-detail-port-note",
}) +
localNote +
'<div class="svc-detail-section-title">Port Requirements</div>' +
'<div class="svc-detail-port-note">This shows whether Sovran_SystemsOS is ready to use this port on this computer. If you need access from outside your home network, forward this port in your router.</div>' +
'<table class="svc-detail-port-table">' +
'<thead><tr><th>Port</th><th>Protocol</th><th>Used For</th><th>Sovran_SystemsOS Status</th></tr></thead>' +
'<tbody>' + portTableRows + '</tbody>' +
'</table>' +
'</div>';
}
// Section E: Credentials & Links
if (isNwc) {
if (effectiveEnabled || data.enabled) {
html += '<div class="nwc-tab-intro">' +
'<div class="nwc-intro-header">' +
'<div class="nwc-tab-intro-title">Lightning Wallet Connections</div>' +
'<p class="nwc-tab-intro-desc">Powerful, isolated wallets for your daily spending and modern apps.</p>' +
'</div>' +
'<div class="nwc-benefits-grid">' +
'<div class="nwc-benefit-item">' +
'<div class="nwc-benefit-icon">🛡️</div>' +
'<div class="nwc-benefit-content">' +
'<strong>Isolated & Secure</strong>' +
'<p>Create sandboxed wallets for quick spending while keeping your main node protected. Isolated access is faster than Tor and perfect for budgeting.</p>' +
'</div>' +
'</div>' +
'<div class="nwc-benefit-item">' +
'<div class="nwc-benefit-icon">🌐</div>' +
'<div class="nwc-benefit-content">' +
'<strong>Modern Ecosystem</strong>' +
'<p>Easily spend and receive bitcoin using LNURL and Nostr (NWC) across the growing ecosystem of decentralized apps.</p>' +
'</div>' +
'</div>' +
'<div class="nwc-benefit-item">' +
'<div class="nwc-benefit-icon">📱</div>' +
'<div class="nwc-benefit-content">' +
'<strong>Zeus on the Go (via NWC)</strong>' +
'<p>Connect Zeus to your wallet using NWC — no Tor, no port forwarding. Create a wallet below, scan the pairing QR, and start spending from your phone.</p>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'<div id="nwc-wallets-body"><p class="creds-loading">Loading wallets…</p></div>' +
'<details class="nwc-liquidity-details">' +
'<summary class="nwc-liquidity-summary">' +
'<span class="nwc-liquidity-icon">⚡</span>' +
'<span>' +
'<strong>Lightning Channel &amp; Liquidity Guide</strong>' +
'<span class="nwc-liquidity-subtitle">Why a brand-new node can\'t send or receive yet</span>' +
'</span>' +
'</summary>' +
'<div class="nwc-liquidity-guide-card">' +
'<p class="nwc-liquidity-body">' +
'Your wallets, pairing keys, and Lightning Address endpoints (like <code>name@yourdomain.com</code>) work the moment you create them. Moving actual bitcoin, though, needs a Lightning node with open channels.' +
'</p>' +
'<div class="nwc-liquidity-steps">' +
'<div class="nwc-liquidity-step">' +
'<div class="step-num">1</div>' +
'<div>' +
'<strong>Outbound Liquidity (Sending)</strong>' +
'<p class="nwc-liquidity-step-desc">Required so connected apps can pay invoices and send funds.</p>' +
'</div>' +
'</div>' +
'<div class="nwc-liquidity-step">' +
'<div class="step-num">2</div>' +
'<div>' +
'<strong>Inbound Liquidity (Receiving)</strong>' +
'<p class="nwc-liquidity-step-desc">Required so your Lightning Address can receive payments from anyone.</p>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="nwc-liquidity-action">' +
'<p class="nwc-liquidity-analogy">💧 <strong>The Plumbing Analogy:</strong> Channels are two-way water pipes. To pour water in (receive) or pump it out (send), you need open channels with liquidity on the right side.</p>' +
'<button class="matrix-action-btn nwc-rtl-btn" id="nwc-open-rtl-btn">🚀 Open Ride The Lightning (RTL) to Manage Channels</button>' +
'</div>' +
'</div>' +
'</details>';
} else {
html += '<div class="nwc-disabled-state">' +
'<div class="nwc-disabled-icon">⚡</div>' +
'<div class="nwc-disabled-title">Lightning Wallet Connections is turned off</div>' +
'<p class="nwc-disabled-desc">Enable it on the <strong>Service &amp; Setup</strong> tab and rebuild, then come back here to create and manage wallets.</p>' +
'<button class="btn btn-primary" id="nwc-goto-setup-btn">⚙ Go to Service &amp; Setup</button>' +
'</div>';
}
} else if (data.has_credentials && data.credentials && data.credentials.length > 0) {
if (data.has_credentials && data.credentials && data.credentials.length > 0) {
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Credentials &amp; Access</div>' +
_renderCredsHtml(data.credentials, unit) +
(unit === "zeus-connect-setup.service" ? _getZeusConnectGuideHtml() : "") +
(unit === "matrix-synapse.service" ?
'<hr class="matrix-actions-divider"><div class="matrix-actions-row">' +
'<button class="matrix-action-btn" id="matrix-add-user-btn">➕ Add New User</button>' +
@@ -923,7 +301,7 @@ async function openServiceDetailModal(unit, name, icon) {
conflictsHtml = '<div class="feature-conflict-warning">\u26A0 Mutually exclusive with: ' + escHtml(conflictNames.join(", ")) + '</div>';
}
addSetup('<div class="svc-detail-section">' +
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">' + addonSectionTitle + '</div>' +
'<p class="svc-detail-desc">' + escHtml(addonDesc) + '</p>' +
conflictsHtml +
@@ -931,45 +309,20 @@ async function openServiceDetailModal(unit, name, icon) {
'<span class="svc-detail-addon-status ' + addonStatusCls + '">' + addonStatusLabel + '</span>' +
'<button class="' + addonBtnCls + '" id="svc-detail-addon-btn">' + escHtml(addonBtnLabel) + '</button>' +
'</div>' +
'</div>');
'</div>';
}
if ((effectiveEnabled || data.enabled) && unit !== "phpfpm-nextcloud.service" && unit !== "phpfpm-wordpress.service") {
addSetup('<div class="svc-detail-section svc-detail-restart-section">' +
html += '<div class="svc-detail-section svc-detail-restart-section">' +
'<div class="svc-detail-section-title">Troubleshooting</div>' +
'<p class="svc-detail-desc">If you\'re experiencing issues with this service, try restarting it.</p>' +
'<button class="btn btn-warning svc-detail-restart-btn" id="svc-detail-restart-btn">🔄 Restart Service</button>' +
'<div class="svc-detail-restart-result" id="svc-detail-restart-result"></div>' +
'</div>');
}
if (isNwc) {
// Land on Setup when the service is off or the domain still needs
// configuring — otherwise the Wallets tab is the useful default.
var domainReady = !!data.domain;
var startTab = (effectiveEnabled || data.enabled) && domainReady ? "wallets" : "setup";
html = _nwcTabsShellHtml({
walletsHtml: html,
setupHtml: setupHtml,
statusDotClass: sc,
statusLabel: st,
domain: data.domain || null,
activeTab: startTab
});
'</div>';
}
$credsBody.innerHTML = html;
if (isNwc) _nwcWireTabs();
_attachCopyHandlers($credsBody);
if (_isNwcServiceUnit(unit) && (effectiveEnabled || data.enabled)) {
await _nwcInitWalletFlow(unit, name, icon);
var nwcRtlBtn = document.getElementById("nwc-open-rtl-btn");
if (nwcRtlBtn) {
nwcRtlBtn.addEventListener("click", function() {
openServiceDetailModal("rtl.service", "Ride The Lightning", "rtl");
});
}
}
if (unit === "matrix-synapse.service") {
var addBtn = document.getElementById("matrix-add-user-btn");
@@ -1055,7 +408,6 @@ async function openServiceDetailModal(unit, name, icon) {
async function openCredsModal(unit, name, icon) {
if (!$credsModal) return;
_setCredsDialogWide(false);
if ($credsTitle) {
$credsTitle.innerHTML = '';
if (icon) {
@@ -1085,9 +437,6 @@ async function openCredsModal(unit, name, icon) {
'<button class="matrix-action-btn" id="matrix-change-pw-btn">🔑 Change Password</button>' +
'</div>';
}
if (unit === "zeus-connect-setup.service") {
html += _getZeusConnectGuideHtml();
}
$credsBody.innerHTML = html;
_attachCopyHandlers($credsBody);
if (unit === "matrix-synapse.service") {
@@ -9,7 +9,6 @@ let _updatePollTimer = null;
let _updateLogOffset = 0;
let _serverWasDown = false;
let _updateFinished = false;
let _updatePollFailures = 0; // consecutive failed update-status polls
let _supportTimerInt = null;
let _supportEnabledAt = null;
let _supportStatus = null; // last fetched /api/support/status payload
@@ -26,7 +25,6 @@ let _rebuildLogOffset = 0;
let _rebuildPollTimer = null;
let _rebuildFinished = false;
let _rebuildServerDown = false;
let _rebuildPollFailures = 0; // consecutive failed rebuild-status polls
let _pendingToggle = null; // {feature, extra} waiting for domain/confirm
let _rebuildFeatureName = "";
let _rebuildIsEnabling = true;
+22 -118
View File
@@ -110,26 +110,12 @@ function renderSupportInactive() {
'</div>',
'<div class="support-steps"><div class="support-steps-title">What happens:</div><ol>',
'<li>A restricted <code>sovran-support</code> user is created with limited access</li>',
'<li>Support\'s SSH key is added only to that restricted account — not to root</li>',
'<li>Our SSH key is added only to that restricted account</li>',
'<li>Wallet files are locked via access controls — not visible to support</li>',
'<li>You control if and when wallet access is granted (time-limited)</li>',
'<li>All session events are logged for your audit</li>',
'<li>Access expires automatically after 24 hours</li>',
'</ol></div>',
'<div class="support-key-section">',
'<label class="support-key-label" for="support-ssh-pubkey">',
'<strong>Paste the support SSH public key provided by Sovran Systems:</strong>',
'</label>',
'<textarea id="support-ssh-pubkey" class="support-key-input" rows="3" ',
'placeholder="ssh-ed25519 AAAA… support-session" ',
'spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>',
'<p class="support-key-hint">',
'The key must start with <code>ssh-ed25519</code> or <code>ecdsa-sha2-nistp256</code>. ',
'Do not paste your own private key — only paste the one-time public key sent by Sovran Systems support.',
'</p>',
'</div>',
'<button class="btn support-btn-enable" id="btn-support-enable">Enable Support Access</button>',
'<p id="support-key-error" class="support-key-error" style="display:none;color:#c0392b;margin-top:8px;"></p>',
'<p class="support-fine-print">You can revoke access at any time. When you end the session, you\'ll be able to disable SSH to return to the default secure state.</p>',
'</div>',
].join("");
@@ -241,43 +227,16 @@ function renderSupportRemoved(verified) {
async function enableSupport() {
var btn = document.getElementById("btn-support-enable");
var errEl = document.getElementById("support-key-error");
var textarea = document.getElementById("support-ssh-pubkey");
if (errEl) { errEl.style.display = "none"; errEl.textContent = ""; }
var sshKey = textarea ? textarea.value.trim() : "";
if (!sshKey) {
if (errEl) { errEl.textContent = "Please paste the SSH public key provided by Sovran Systems support."; errEl.style.display = "block"; }
return;
}
// Client-side pre-validation: key must start with a known algorithm prefix
var validPrefixes = ["ssh-ed25519 ", "ecdsa-sha2-nistp256 ", "ecdsa-sha2-nistp384 ", "ecdsa-sha2-nistp521 ", "sk-ssh-ed25519@openssh.com "];
var hasValidPrefix = validPrefixes.some(function(p) { return sshKey.startsWith(p); });
if (!hasValidPrefix) {
if (errEl) { errEl.textContent = "Invalid key format. The key must start with ssh-ed25519 or ecdsa-sha2-nistp256. Do not paste a private key."; errEl.style.display = "block"; }
return;
}
if (sshKey.indexOf("\n") !== -1) {
if (errEl) { errEl.textContent = "The key must be a single line. Please check the pasted value."; errEl.style.display = "block"; }
return;
}
if (btn) { btn.disabled = true; btn.textContent = "Enabling…"; }
try {
await apiFetch("/api/support/enable", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ssh_public_key: sshKey }),
});
await apiFetch("/api/support/enable", { method: "POST" });
var status = await apiFetch("/api/support/status");
_supportStatus = status;
_supportEnabledAt = status.enabled_at;
renderSupportActive(status);
} catch (err) {
if (btn) { btn.disabled = false; btn.textContent = "Enable Support Access"; }
var detail = (err && err.detail) ? err.detail : "Failed to enable support access. Please check the key and try again.";
if (errEl) { errEl.textContent = detail; errEl.style.display = "block"; }
else { alert(detail); }
alert("Failed to enable support access. Please try again.");
}
}
@@ -514,99 +473,45 @@ function renderBackupReady(drives) {
].join("");
}
// ── Role-aware backup description ─────────────────────────────
// Desktop Only systems run no server or Bitcoin services and have no
// internal second drive, so the backup mirrors only the NixOS configuration
// and home directory. nix-bitcoin secrets, /var/lib system service data,
// and the database/blockchain caveats apply only to the Node and
// Server + Desktop roles.
var isDesktopOnly = (_currentRole === "desktop");
var introHtml;
if (isDesktopOnly) {
introHtml = [
'<div class="support-wallet-box support-wallet-protected" style="margin-bottom:16px;">',
'<p class="support-wallet-desc">',
'This manual backup lets you create a copy of your system on an external USB drive \u2014 ',
'storing your data in a second location, outside the computer, for maximum protection ',
'against hardware failure or physical damage.',
'</p>',
'</div>',
].join("");
} else {
introHtml = [
'<div class="support-wallet-box support-wallet-protected" style="margin-bottom:16px;">',
'<p class="support-wallet-desc">',
'Your Sovran Pro already backs up your data automatically to its internal second drive. ',
'This manual backup lets you create an additional copy on an external USB drive \u2014 ',
'storing your data in a third location, outside the computer, for maximum protection ',
'against hardware failure or physical damage.',
'</p>',
'</div>',
].join("");
}
var backupItemsHtml;
if (isDesktopOnly) {
backupItemsHtml = [
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
'<li>Home directory (<code>/home</code>)</li>',
].join("");
} else {
backupItemsHtml = [
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
'<li>nix-bitcoin secrets (<code>/etc/nix-bitcoin-secrets</code>)</li>',
'<li>System service data (<code>/var/lib</code>) — excluding databases and blockchain data (see note below)</li>',
'<li>Home directory (<code>/home</code>)</li>',
].join("");
}
// The database/blockchain caveat is only relevant when server services exist.
var dbNoteHtml = "";
if (!isDesktopOnly) {
dbNoteHtml = [
'<div class="support-wallet-box support-wallet-warning">',
'<div class="support-wallet-header">',
'<span class="support-wallet-icon">\u2139\ufe0f</span>',
'<span class="support-wallet-title">Database and Blockchain Data</span>',
'</div>',
'<p class="support-wallet-desc">Application databases stored in PostgreSQL or MariaDB/MySQL are <strong>not included</strong> in Manual Backup. Bitcoin blockchain and Electrs index data are also excluded (they are stored on the internal second drive). If you use Nextcloud, Matrix, or other database-backed applications, back up those databases separately with their native tools.</p>',
'</div>',
].join("");
}
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">\ud83d\udcbe</div>',
'<h3 class="support-heading">Manual Backup</h3>',
introHtml,
'<div class="support-wallet-box support-wallet-protected" style="margin-bottom:16px;">',
'<p class="support-wallet-desc">',
'Your Sovran Pro already backs up your data automatically to its internal second drive. ',
'This manual backup lets you create an additional copy on an external USB drive \u2014 ',
'storing your data in a third location, outside the computer, for maximum protection ',
'against hardware failure or physical damage.',
'</p>',
'</div>',
'<div class="support-steps">',
'<div class="support-steps-title">Requirements</div>',
'<ol class="support-backup-steps">',
'<li>USB hard drive plugged into one of the open USB ports on your Sovran Pro</li>',
'<li>Enough free space for your data (the backup checks this before starting)</li>',
'<li>Drive must be formatted as <strong>ext4</strong> (a Linux filesystem). Drives with exFAT, FAT32, or NTFS are not supported. To format a drive as ext4, use a Linux tool such as GParted or <code>mkfs.ext4</code> — note that formatting erases all data on the drive.</li>',
'<li>The drive is intended for Linux/Sovran recovery. It may not be directly readable by Windows or macOS without additional software.</li>',
'<li>At least 500 GB of free space on the drive</li>',
'<li>Drive must be formatted as <strong>exFAT</strong></li>',
'</ol>',
'</div>',
'<div class="support-steps">',
'<div class="support-steps-title">What gets backed up</div>',
'<ol class="support-backup-steps">',
backupItemsHtml,
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
'<li>nix-bitcoin secrets (<code>/etc/nix-bitcoin-secrets</code>)</li>',
'<li>System service data (<code>/var/lib</code>) including Vaultwarden, bitcoind, LND, sovran-hub, domains, and secrets</li>',
'<li>Home directory (<code>/home</code>)</li>',
'</ol>',
'</div>',
dbNoteHtml,
'<div class="support-wallet-box support-wallet-protected">',
'<div class="support-wallet-box support-wallet-warning">',
'<div class="support-wallet-header">',
'<span class="support-wallet-icon">\u23f1\ufe0f</span>',
'<span class="support-wallet-title">Time Estimate</span>',
'</div>',
'<p class="support-wallet-desc">The first backup may take a while depending on how much data you have. Later backups are much faster because only changed or new files are copied. Files are stored directly on the drive and can be browsed without any special software.</p>',
'<p class="support-wallet-desc">This backup can take <strong>up to 4 hours</strong> depending on the amount of data stored on your Sovran Pro and the speed of your external hard drive. Be patient\u2026</p>',
'</div>',
driveSelector,
@@ -679,10 +584,9 @@ async function pollBackupStatus() {
logDiv.scrollTop = logDiv.scrollHeight;
}
_backupLogOffset = data.offset;
const result = (data.result || "").toLowerCase();
if (result === "success" || result === "failed") {
if (!data.running) {
stopBackupPoll();
renderBackupDone(result === "success");
renderBackupDone(data.result === "success");
}
} catch (_) {}
}
@@ -714,7 +618,7 @@ function renderBackupDone(success) {
'<div class="support-section">',
'<div class="support-icon-big">\u26a0\ufe0f</div>',
'<h3 class="support-heading">Backup Failed</h3>',
'<p class="support-desc">The backup did not complete successfully. Please check that the USB drive is still connected, has enough free space, and is formatted as ext4. Then try again.</p>',
'<p class="support-desc">The backup did not complete successfully. Please check that the USB drive is still connected, has enough free space, and is formatted as exFAT. Then try again.</p>',
'<div class="modal-log" id="backup-log-fail" style="text-align:left;"></div>',
'<button class="btn support-btn-done" id="btn-backup-close">Close</button>',
'</div>',
+40 -3
View File
@@ -156,10 +156,13 @@ function buildTile(svc) {
var pct = Math.round((svc.sync_progress || 0) * 100);
var id = tileId(svc);
var eta = _calcBtcEta(id, svc.sync_progress || 0);
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-sync-container">' +
'<div class="tile-sync-label">\u23F3 Syncing Timechain</div>' +
'<div class="tile-sync-bar-row">' +
@@ -175,8 +178,10 @@ function buildTile(svc) {
return tile;
}
var ver = svc.version || svc.bitcoin_version || '';
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</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>' + bip110Badge + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></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 + 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() {
@@ -230,6 +235,22 @@ function updateTiles(services) {
if (fill) fill.style.width = pct + "%";
if (pctEl) pctEl.textContent = pct + "%";
if (etaEl) etaEl.textContent = etaText;
// Update or insert version label
var syncVer = svc.version || svc.bitcoin_version || '';
if (syncVer) {
var syncVerEl = tile.querySelector(".tile-version");
if (syncVerEl) {
syncVerEl.textContent = syncVer;
} else {
var syncNameEl = tile.querySelector(".tile-name");
if (syncNameEl) {
var newSyncVerEl = document.createElement("div");
newSyncVerEl.className = "tile-version";
newSyncVerEl.textContent = syncVer;
syncNameEl.insertAdjacentElement("afterend", newSyncVerEl);
}
}
}
} else {
// IBD finished or not syncing — if tile had sync layout rebuild it normally
if (tile.querySelector(".tile-sync-container")) {
@@ -244,6 +265,22 @@ function updateTiles(services) {
var text = tile.querySelector(".status-text");
if (dot) dot.className = "status-dot " + sc;
if (text) text.textContent = st;
// Update or insert version label for all service tiles
var tileVer = svc.version || svc.bitcoin_version || '';
if (tileVer) {
var verEl = tile.querySelector(".tile-version");
if (verEl) {
verEl.textContent = tileVer;
} else {
var nameEl = tile.querySelector(".tile-name");
if (nameEl) {
var newVerEl = document.createElement("div");
newVerEl.className = "tile-version";
newVerEl.textContent = tileVer;
nameEl.insertAdjacentElement("afterend", newVerEl);
}
}
}
// Update BIP-110 badge for bip110 tiles
if (svc.icon === 'bip110') {
var badgeHtml = _renderBip110Badge(svc.bip110);
@@ -253,8 +290,8 @@ function updateTiles(services) {
var newBadge = _firstElementFromHtml(badgeHtml);
if (newBadge) { badgeEl.replaceWith(newBadge); } else { badgeEl.remove(); }
} else if (badgeHtml) {
// Insert badge after the service name
var anchorEl = tile.querySelector(".tile-name");
// 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);
@@ -33,7 +33,6 @@ function _doOpenUpdateModal() {
_updateLogOffset = 0;
_serverWasDown = false;
_updateFinished = false;
_updatePollFailures = 0;
if ($modalLog) $modalLog.textContent = "";
if ($modalStatus) $modalStatus.textContent = "Starting update…";
if ($modalSpinner) $modalSpinner.classList.add("spinning");
@@ -95,7 +94,6 @@ async function pollUpdateStatus() {
if (_updateFinished) return;
try {
var data = await apiFetch("/api/updates/status?offset=" + _updateLogOffset);
_updatePollFailures = 0;
if (_serverWasDown) {
_serverWasDown = false;
if (!data.running) {
@@ -145,16 +143,6 @@ async function pollUpdateStatus() {
onUpdateDone(false);
}
} catch (err) {
_updatePollFailures += 1;
// Same guard as the rebuild modal: if polling stays broken long past a
// normal Hub restart, reload to re-authenticate and show the real state
// instead of spinning forever.
if (_updatePollFailures >= STATUS_POLL_MAX_FAILURES) {
_updateFinished = true;
stopUpdatePoll();
window.location.reload();
return;
}
if (!_serverWasDown) { _serverWasDown = true; appendLog("\n[Server restarting — waiting for it to come back…]\n"); if ($modalStatus) $modalStatus.textContent = "Server restarting…"; }
}
}
+137 -46
View File
@@ -1,16 +1,16 @@
/* Sovran_SystemsOS Hub — First-Boot Onboarding Wizard
Drives the 4-step post-install setup flow. */
Drives the 5-step post-install setup flow. */
"use strict";
// ── Constants ─────────────────────────────────────────────────────
const TOTAL_STEPS = 4;
const TOTAL_STEPS = 5;
// Steps to skip per role (step 3 involves domain setup)
// Steps to skip per role (steps 3 and 4 involve domain/port setup)
// Step 2 (timezone/locale) is NEVER skipped — all roles need it.
const ROLE_SKIP_STEPS = {
"desktop": [3],
"node": [3],
"desktop": [3, 4],
"node": [3, 4],
};
// ── Role state (loaded at init) ───────────────────────────────────
@@ -66,7 +66,7 @@ function setStatus(elId, msg, type) {
el.className = "onboarding-save-status" + (type ? " onboarding-save-status--" + type : "");
}
function updateCompleteChecklist() {
function updateStep5Checklist() {
var checklist = document.getElementById("onboarding-checklist");
if (!checklist) return;
var existing = document.getElementById("onboarding-migration-check");
@@ -135,7 +135,8 @@ function showStep(step) {
// Lazy-load step content
if (step === 2) loadStep2();
if (step === 3) loadStep3();
// Step 4 (Complete) is static — no lazy-load needed
if (step === 4) loadStep4();
// Step 5 (Complete) is static — no lazy-load needed
}
// Return the next step number, skipping over role-excluded steps
@@ -318,27 +319,20 @@ async function loadStep3() {
if (!body) return;
try {
// Fetch services and domains in parallel (network info is best-effort below)
// Fetch services, domains, and network info in parallel
var results = await Promise.all([
apiFetch("/api/services"),
apiFetch("/api/domains/status"),
apiFetch("/api/network"),
]);
_servicesData = results[0];
_domainsData = results[1];
var networkData = results[2];
} catch (err) {
body.innerHTML = '<p class="onboarding-error">⚠ Could not load service data: ' + escHtml(err.message) + '</p>';
return;
}
// Best-effort internal IP for the router note — never blocks this step
var internalIp = "";
try {
var networkData = await apiFetch("/api/network");
if (networkData && networkData.internal_ip && networkData.internal_ip !== "unavailable") {
internalIp = String(networkData.internal_ip).trim();
}
} catch (_) {}
// Build set of enabled service units
var enabledUnits = new Set();
(_servicesData || []).forEach(function(svc) {
@@ -350,35 +344,32 @@ async function loadStep3() {
return enabledUnits.has(d.unit);
});
var domainValues = (_domainsData && _domainsData.domains) || {};
var html = "";
if (relevantDomains.length === 0) {
html += '<p class="onboarding-body-text">No domain-based services are enabled for your role. You can skip this step.</p>';
} else {
// Shared instructions (single source of truth: static/js/domain-prereqs.js) —
// identical wording to the feature-enable domain modal shown for NWC,
// BTCPay Server, and every other domain-based feature, in every role.
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
+ renderDomainNeedsHtml({ serviceName: null, hostExample: "call" })
+ renderNjallaStepsHtml({ hostExample: "call", pasteHint: "next to its service below" })
+ '<p style="margin:0 0 8px;"><strong>Sovran_SystemsOS uses Njal.la for domains and Dynamic DNS.</strong></p>'
+ '<ol style="margin:8px 0 0 16px; padding:0; line-height:1.7;">'
+ '<li>Create an account at <a href="https://njal.la" target="_blank" style="color:var(--accent-color);">https://njal.la</a>.</li>'
+ '<li>Buy at least one domain. Each service below needs its own domain — you can either give each service its own subdomain of a single domain you buy (subdomains are free, and one domain can have many), OR use a separate domain for each. Your choice.</li>'
+ '<li>For each service, add a <strong>Dynamic</strong> record in Njal.la:'
+ '<ul style="margin:4px 0 0 16px;padding:0;line-height:1.7;">'
+ '<li>In the Njal.la <strong>Name</strong> field, type ONLY the host part — the word before your domain.<br>'
+ '(Example only, your choice — for &quot;call.yourdomain.com&quot; you&apos;d type just: <code>call</code>.)<br>'
+ 'If you bought a whole separate domain just for this service, leave Name blank or use <code>@</code>.<br>'
+ '&#9888; Do NOT type the full domain in the Name field — Njal.la adds it automatically.</li>'
+ '<li>A Dynamic record has NO IP field. You don&apos;t enter an IP anywhere — it auto-fills once Sovran_SystemsOS updates it (on save, and again after reboot).</li>'
+ '</ul>'
+ '</li>'
+ '<li>Njal.la gives you a curl command like:<br>'
+ '<code style="font-size:0.8em;">curl &quot;https://njal.la/update/?h=call.yourdomain.com&amp;k=abc123&amp;auto&quot;</code></li>'
+ '</ol>'
+ '</div>';
html += '<p class="onboarding-hint">Enter each service\'s full domain — a subdomain (e.g. <code>call.yourdomain.com</code>) or a separate domain (e.g. <code>call.com</code>) — and its Njal.la DDNS curl command.</p>';
// Router note (the same wording is shown again, per-service, whenever a
// domain-based feature is enabled, and lives on each service tile afterwards)
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
+ renderRouterPortsHtml({
internalIp: internalIp,
plural: true,
includeSsh: true,
extraNote: "Element Call needs a few extra ports (some UDP), and you’ll be shown exactly which when you enable it.",
})
+ '</div>';
relevantDomains.forEach(function(d) {
var currentVal = domainValues[d.name] || "";
var currentVal = (_domainsData && _domainsData[d.name]) || "";
html += '<div class="onboarding-domain-group">';
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) + '" />';
@@ -392,7 +383,7 @@ async function loadStep3() {
}
// SSL email section
var emailVal = domainValues["sslemail"] || "";
var emailVal = (_domainsData && _domainsData["sslemail"]) || "";
html += '<div class="onboarding-domain-group onboarding-domain-group--email">';
html += '<label class="onboarding-domain-label">📧 SSL Certificate Email</label>';
html += '<p class="onboarding-hint onboarding-hint--inline">Let\'s Encrypt uses this for certificate expiry notifications.</p>';
@@ -520,10 +511,106 @@ async function saveStep3() {
return true;
}
// ── Step 4: Complete ──────────────────────────────────────────────
// ── Step 4: Port Forwarding ───────────────────────────────────────
async function loadStep4() {
var body = document.getElementById("step-4-body");
if (!body) return;
body.innerHTML = '<p class="onboarding-loading">Loading router setup…</p>';
var networkData = null;
try {
networkData = await apiFetch("/api/network");
} catch (err) {
body.innerHTML = '<p class="onboarding-error">⚠ Could not load network data: ' + escHtml(err.message) + '</p>';
return;
}
var trimmedInternalIp = (networkData && networkData.internal_ip) ? String(networkData.internal_ip).trim() : "";
var internalIp = trimmedInternalIp || "";
var hasInternalIp = !!internalIp;
var ip = escHtml(internalIp || "Could not detect");
var routerIpHelp = hasInternalIp
? "Use this IP address as the destination/internal IP when creating each router forwarding rule."
: "Use this computer’s internal IP as the destination/internal IP when creating each router forwarding rule.";
var destinationInstruction = hasInternalIp
? 'Set the destination/internal IP to <strong>' + ip + '</strong>'
: 'Use this computer’s internal IP as the destination/internal IP';
var html = '<p class="onboarding-port-note" style="margin-bottom:14px;">'
+ '⚠ <strong>Each port only needs to be forwarded once — all services share the same ports.</strong>'
+ '</p>';
html += '<div class="onboarding-port-ip">';
html += ' <span class="onboarding-port-ip-label">Forward router traffic to this Sovran_SystemsOS computer:</span>';
html += ' <span class="port-req-internal-ip">' + ip + '</span>';
html += '</div>';
html += '<div class="onboarding-port-note" style="margin:8px 0 16px;">' + routerIpHelp + '</div>';
// Required 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:8px;">Required Router Rules</div>';
html += '<table class="onboarding-port-table">';
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward&nbsp;To</th><th>Used For</th></tr></thead>';
html += '<tbody>';
html += '<tr><td class="port-req-port">80</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">HTTP / SSL setup</td></tr>';
html += '<tr><td class="port-req-port">443</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">HTTPS</td></tr>';
html += '<tr><td class="port-req-port">22</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">Remote SSH access</td></tr>';
html += '</tbody></table>';
html += '</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;">Element Call Router Rules</div>';
html += '<div style="font-size:0.88em;margin-bottom:8px;color:var(--color-text-muted,#888);">Only add these if you enable Element Call. These ports help video and audio calls connect reliably.</div>';
html += '<table class="onboarding-port-table">';
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward&nbsp;To</th><th>Used For</th></tr></thead>';
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">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">30000-40000</td><td class="port-req-proto">TCP &amp; UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN relay (WebRTC)</td></tr>';
html += '</tbody></table>';
html += '<div style="font-size:0.85em;margin-top:6px;color:var(--color-text-muted,#888);">ℹ The <strong>30000-40000</strong> range is a single forwarding rule — just set its protocol to <strong>both TCP and UDP</strong> (often shown as "Both" or "TCP/UDP" on your router).</div>';
html += '</div>';
// Totals
html += '<div class="onboarding-port-totals">';
html += '<strong>Total port openings: 3</strong> (without Element Call)<br>';
html += '<strong>Total port openings: 8</strong> (with Element Call — 3 required + 5 optional)';
html += '</div>';
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
+ '⚠ <strong>Ports 80 and 443 must be forwarded first.</strong> '
+ 'Caddy uses these to obtain SSL certificates from Let\'s Encrypt. '
+ 'If they are closed, HTTPS will not work and your services will be unreachable from outside your network.'
+ '</div>';
html += '<details class="onboarding-port-details" style="margin-bottom:16px;">'
+ '<summary class="onboarding-port-details-summary">How to set up port forwarding</summary>'
+ '<ol style="margin:12px 0 0 16px; padding:0; line-height:1.8;">'
+ '<li>Open your router\'s admin panel — usually <code>http://192.168.1.1</code> or <code>http://192.168.0.1</code></li>'
+ '<li>Look for <strong>"Port Forwarding"</strong>, <strong>"NAT"</strong>, or <strong>"Virtual Server"</strong> in the settings</li>'
+ '<li>Create a new rule for each port listed above</li>'
+ '<li>' + destinationInstruction + '</li>'
+ '<li>Set both internal and external port to the same number</li>'
+ '<li>Save and apply changes</li>'
+ '</ol>'
+ '</details>';
html += '<div class="onboarding-port-note" style="margin-top:12px;">'
+ '<strong>Important:</strong> The Hub can show which ports Sovran_SystemsOS needs, but it cannot fully confirm router forwarding from inside your home network. Full public port verification requires an outside internet check.'
+ '</div>';
body.innerHTML = html;
}
// ── Step 5: Complete ──────────────────────────────────────────────
async function completeOnboarding() {
var btn = document.getElementById("step-4-finish");
var btn = document.getElementById("step-5-finish");
if (btn) { btn.disabled = true; btn.textContent = "Finishing…"; }
try {
@@ -546,7 +633,7 @@ function wireNavButtons() {
try {
await apiFetch("/api/migration/password-acknowledge", { method: "POST" });
_migrationOccurred = true;
updateCompleteChecklist();
updateStep5Checklist();
showStep1FromMigration();
} catch (err) {
setStatus("migration-password-status", "⚠ " + err.message, "error");
@@ -582,9 +669,13 @@ function wireNavButtons() {
showStep(nextStep(3));
});
// Step 4: finish
var s4finish = document.getElementById("step-4-finish");
if (s4finish) s4finish.addEventListener("click", completeOnboarding);
// Step 4 → 5 (port forwarding — no save needed)
var s4next = document.getElementById("step-4-next");
if (s4next) s4next.addEventListener("click", function() { showStep(nextStep(4)); });
// Step 5: finish
var s5finish = document.getElementById("step-5-finish");
if (s5finish) s5finish.addEventListener("click", completeOnboarding);
// Back buttons
document.querySelectorAll(".onboarding-btn-back").forEach(function(btn) {
@@ -616,13 +707,13 @@ document.addEventListener("DOMContentLoaded", async function() {
try {
var migration = await apiFetch("/api/migration/password-status");
if (migration && migration.pending) {
updateCompleteChecklist();
updateStep5Checklist();
showMigrationStep(migration.password || "");
return;
}
} catch (_) {}
updateCompleteChecklist();
updateStep5Checklist();
showStep(1);
loadStep1();
});
-283
View File
@@ -1,283 +0,0 @@
"""Sovran Hub — injectable support session operations.
Functions here handle legacy migration, root-key removal, and support-session
expiry. All filesystem paths, clocks, and callback functions are injectable
so that the test suite can exercise the exact production implementations with
temporary files and mocks rather than maintaining separate copies.
All functions depend only on the Python standard library and the co-located
``security_helpers`` module.
"""
from __future__ import annotations
import json
import os
import re
import tempfile
import time as _time_module
from typing import Callable
# ── Legacy Njalla curl line regex ─────────────────────────────────────────────
#
# Matches both forms written by old Hub versions:
# curl [flags] https://njal.la/... (unquoted)
# curl [flags] "https://njal.la/..." (quoted — historical form)
#
# Optional flags (in order): --silent, --max-time N, --fail
#
# Rejected outright: semicolons, pipes, backticks, redirects, newlines,
# ${...} except the literal ${IP} placeholder, and any extra arguments.
_LEGACY_NJALLA_CURL_RE = re.compile(
r'^curl\s+(?:--silent\s+)?(?:--max-time\s+\d+\s+)?(?:--fail\s+)?'
r'(?:'
r'"(https://(?:www\.)?njal\.la/(?:[^\s;|`$\x00-\x1f"]|\$\{IP\})+)"' # group 1: quoted
r'|(https://(?:www\.)?njal\.la/(?:[^\s;|`$\x00-\x1f"]|\$\{IP\})+)' # group 2: unquoted
r')$'
)
# The exact base64 blob of the historical fleet-wide root support key that
# was shipped with old releases of Sovran_SystemsOS and must be removed from
# /root/.ssh/authorized_keys on upgrade.
LEGACY_ROOT_KEY_BLOB = (
"AAAAC3NzaC1lZDI1NTE5AAAAIPxPF2Qm11FQxC20wydKtlmn/Bo07YnDda3b9/CyXxQP"
)
def remove_legacy_root_key(
authorized_keys_path: str,
target_blob: str,
*,
audit_fn: Callable[[str, str], None] | None = None,
) -> bool:
"""Remove the exact historical fleet-wide support key from an authorized_keys file.
Identifies the key by its exact base64 blob (``parts[1]``), regardless of
algorithm prefix or comment field. All other keys, blank lines, and comment
lines are preserved unchanged. The file is written back atomically.
Args:
authorized_keys_path: Path to the authorized_keys file to modify.
target_blob: The exact base64 key blob to remove. Only lines whose
second whitespace-delimited field matches this value are removed;
no substring or comment matching is performed.
audit_fn: Optional callback ``(event: str, details: str)`` for audit
logging. The full key blob is **never** passed to this callback.
Returns:
``True`` if the file was modified (at least one line removed),
``False`` if unchanged or absent.
"""
def _audit(event: str, details: str = "") -> None:
if audit_fn:
audit_fn(event, details)
try:
with open(authorized_keys_path, "r") as f:
lines = f.readlines()
except FileNotFoundError:
return False
except OSError:
return False
kept: list[str] = []
removed_count = 0
for line in lines:
stripped = line.rstrip("\n")
parts = stripped.split()
# Key lines have at least two space-separated fields: algorithm + blob.
# Remove only lines whose blob (parts[1]) matches exactly — no
# substring matching, no comment matching.
if len(parts) >= 2 and parts[1] == target_blob:
removed_count += 1
# Audit without logging the key blob itself.
_audit("LEGACY_ROOT_KEY_REMOVED", "removed exact historical root support key")
else:
kept.append(line)
if removed_count == 0:
return False
# Atomic write: mkstemp in same directory + os.replace
auth_dir = os.path.dirname(os.path.abspath(authorized_keys_path))
fd, tmp = tempfile.mkstemp(dir=auth_dir, prefix=".authorized_keys_tmp")
try:
with os.fdopen(fd, "w") as f:
f.writelines(kept)
os.chmod(tmp, 0o600)
os.replace(tmp, authorized_keys_path)
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
return False
_audit(
"LEGACY_ROOT_KEY_CLEANUP_COMPLETE",
f"removed={removed_count} keys_retained={len(kept)}",
)
return True
def migrate_legacy_njalla_script(
script_path: str,
validate_fn: Callable[[str], str],
save_fn: Callable[[list[str]], None],
load_fn: Callable[[], list[str]],
*,
audit_fn: Callable[[str, str], None] | None = None,
) -> None:
"""Safely migrate legacy curl DDNS lines from a njalla.sh script to JSON store.
Reads the script **without** executing or sourcing it. Parses only the
exact narrow curl-pattern lines (quoted or unquoted) written by old Hub
versions. Any other line is silently discarded — never executed or logged
(it may contain secret tokens).
On successful persistence the script is archived with mode ``0o000`` so
it can no longer be executed. If persistence fails the script is left
**untouched**.
Args:
script_path: Path to the legacy njalla.sh file.
validate_fn: URL validation function; raises ``ValueError`` on invalid
URLs. Callers must substitute the ``${IP}`` placeholder before
calling — this function passes ``url.replace("${IP}", "127.0.0.1")``
to the validator.
save_fn: Callable that atomically writes a ``list[str]`` URL list to
the persistent JSON store.
load_fn: Callable that returns the current ``list[str]`` URL list from
the persistent store.
audit_fn: Optional callback ``(event: str, details: str)`` for audit
logging. Token-bearing URLs are **never** passed to this callback.
"""
def _audit(event: str, details: str = "") -> None:
if audit_fn:
audit_fn(event, details)
try:
with open(script_path, "r") as f:
content = f.read()
except FileNotFoundError:
return
except OSError:
return
existing_urls = load_fn()
new_urls: list[str] = []
for raw_line in content.splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or line.startswith("IP=") or line.startswith("#!/"):
continue
m = _LEGACY_NJALLA_CURL_RE.match(line)
if not m:
# Unrecognised line — discard silently, do NOT log (may contain tokens)
continue
# group(1) = quoted form, group(2) = unquoted form
raw_url = m.group(1) or m.group(2)
# Substitute placeholder so host/scheme/path validation works
url_to_validate = raw_url.replace("${IP}", "127.0.0.1")
try:
validate_fn(url_to_validate)
except ValueError:
continue # Silently discard invalid/non-Njal.la URLs
if raw_url not in existing_urls and raw_url not in new_urls:
new_urls.append(raw_url)
if new_urls:
combined = existing_urls + new_urls
try:
save_fn(combined)
except Exception:
# Persistence failed — leave the script untouched, return without
# archiving so the migration can be retried.
return
_audit("NJALLA_MIGRATION", f"migrated {len(new_urls)} DDNS URLs from legacy script")
# Archive: remove all permission bits so cron/any mechanism cannot run it
try:
os.chmod(script_path, 0o000)
except OSError:
pass
def expire_if_stale(
status_file: str,
*,
clock_fn: Callable[[], float] | None = None,
disable_fn: Callable[[], bool] | None = None,
audit_fn: Callable[[str, str], None] | None = None,
session_id: str | None = None,
expected_expiry: float | None = None,
max_session_seconds: float = 86400.0,
) -> bool:
"""Expire a support session if its deadline has passed.
**Stale-timer guard:** when ``session_id`` and/or ``expected_expiry`` are
provided (used by the server-side timer callback), the stored session
metadata is compared field-by-field. A mismatch means a replacement
session has been started after this timer was scheduled; in that case the
function returns ``False`` without touching anything.
Args:
status_file: Path to the JSON session metadata file.
clock_fn: Callable returning current Unix time (default: ``time.time``).
disable_fn: Callable that performs the full disable sequence — removes
the support key, removes wallet-unlock metadata, restores deny
ACLs, clears session metadata, and audits the event. If ``None``,
expiry is detected but no action is taken (useful for tests that
want to inspect detection only).
audit_fn: Optional callback ``(event: str, details: str)`` for audit
logging.
session_id: If given, expiry is skipped unless the stored
``session_id`` field matches exactly.
expected_expiry: If given, expiry is skipped unless the stored
``expires_at`` field matches exactly.
max_session_seconds: Legacy fallback: maximum age (from ``enabled_at``)
when ``expires_at`` is absent.
Returns:
``True`` if a session was expired, ``False`` otherwise.
"""
_now = clock_fn if clock_fn is not None else _time_module.time
def _audit(event: str, details: str = "") -> None:
if audit_fn:
audit_fn(event, details)
def _disable() -> bool:
return disable_fn() if disable_fn is not None else True
try:
with open(status_file, "r") as f:
info = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return False
# Stale-timer guard
if session_id is not None and info.get("session_id") != session_id:
return False
if expected_expiry is not None and info.get("expires_at") != expected_expiry:
return False
expires_at = info.get("expires_at")
now = _now()
if expires_at is None:
enabled_at = info.get("enabled_at", 0)
if enabled_at and (now - enabled_at) > max_session_seconds:
_audit("SUPPORT_EXPIRED", "legacy session without expires_at exceeded max duration")
_disable()
return True
return False
if now >= expires_at:
_audit("SUPPORT_EXPIRED", f"session expired at {expires_at:.0f}")
_disable()
return True
return False
-39
View File
@@ -18,45 +18,6 @@ def is_active(unit: str, scope: Literal["system", "user"] = "system") -> str:
return _run(["systemctl", f"--{scope}", "is-active", unit]) or "unknown"
def active_states(units_by_scope: dict[str, list[str]], timeout: float = 5) -> dict[tuple[str, str], str]:
"""Return active states for many units with one systemctl call per scope.
The dashboard polls service state frequently. Spawning one ``systemctl``
process per tile makes a slow or unavailable system bus multiply the delay
(and can make the first dashboard render wait through many timeouts). The
``is-active`` command accepts multiple units, so batch them and cap the
whole batch at a single timeout.
"""
states: dict[tuple[str, str], str] = {}
valid_scopes = {"system", "user"}
for scope, units in units_by_scope.items():
unique_units = list(dict.fromkeys(unit for unit in units if unit))
if not unique_units:
continue
if scope not in valid_scopes:
for unit in unique_units:
states[(scope, unit)] = "unknown"
continue
try:
result = subprocess.run(
["systemctl", f"--{scope}", "is-active", "--", *unique_units],
capture_output=True,
text=True,
timeout=timeout,
)
lines = result.stdout.splitlines()
for index, unit in enumerate(unique_units):
state = lines[index].strip() if index < len(lines) else ""
states[(scope, unit)] = state or "unknown"
except Exception:
for unit in unique_units:
states[(scope, unit)] = "unknown"
return states
def is_enabled(unit: str, scope: Literal["system", "user"] = "system") -> str:
return _run(["systemctl", f"--{scope}", "is-enabled", unit]) or "unknown"
+2 -15
View File
@@ -21,14 +21,7 @@
<!-- Header bar -->
<header class="header-bar">
<img src="/static/sovran-hub-icon.svg" alt="Sovran Hub" class="header-logo" />
<div class="title-group">
<span class="title">Sovran_SystemsOS Hub</span>
<!-- OS Version Badge — styled identically to the version badge in service modals -->
<span class="os-version-badge" id="os-version-badge" title="Sovran_SystemsOS Version">v{{ sovran_version }}</span>
</div>
<span class="title">Sovran_SystemsOS Hub</span>
<div class="header-buttons">
<span class="role-badge" id="role-badge">Loading…</span>
<button class="btn btn-header-reboot" id="btn-header-reboot" title="Restart the entire computer">Reboot</button>
@@ -55,12 +48,7 @@
<div id="sidebar-support"></div>
<div id="sidebar-features"></div>
</aside>
<div id="tiles-area">
<div class="dashboard-loading" role="status" aria-live="polite">
<span class="dashboard-loading-spinner" aria-hidden="true"></span>
<span>Loading service status…</span>
</div>
</div>
<div id="tiles-area"></div>
</main>
<!-- Update modal -->
@@ -314,7 +302,6 @@
<script src="/static/js/constants.js?v={{ asset_version }}"></script>
<script src="/static/js/state.js?v={{ asset_version }}"></script>
<script src="/static/js/helpers.js?v={{ asset_version }}"></script>
<script src="/static/js/domain-prereqs.js?v={{ asset_version }}"></script>
<script src="/static/js/tiles.js?v={{ asset_version }}"></script>
<script src="/static/js/service-detail.js?v={{ asset_version }}"></script>
<script src="/static/js/support.js?v={{ asset_version }}"></script>
@@ -34,6 +34,8 @@
<span class="onboarding-step-dot" data-step="3">3</span>
<span class="onboarding-step-connector"></span>
<span class="onboarding-step-dot" data-step="4">4</span>
<span class="onboarding-step-connector"></span>
<span class="onboarding-step-dot" data-step="5">5</span>
</div>
<!-- Step panels -->
@@ -125,8 +127,9 @@
<span class="onboarding-step-icon">🌐</span>
<h2 class="onboarding-step-title">Domain Configuration</h2>
<p class="onboarding-step-desc">
Sovran_SystemsOS uses <strong><a href="https://njal.la" target="_blank" style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS, and your router needs ports <strong>80</strong> and <strong>443</strong> (TCP) forwarded to this computer.
Everything you need — Njal.la account, Dynamic records, and the one router task — is laid out step by step below.
Sovran_SystemsOS uses <strong><a href="https://njal.la" target="_blank" style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS.
Create an account at Njal.la, then for each service below, add a <strong>Dynamic</strong> record — no IP needed, it auto-populates once the DDNS curl command runs.
Paste the curl command from your Njal.la dashboard for each service.
</p>
</div>
<div class="onboarding-card" id="step-3-body">
@@ -141,8 +144,29 @@
</div>
</div>
<!-- ── Step 4: Complete ── -->
<!-- ── Step 4: Port Forwarding ── -->
<div class="onboarding-panel" id="step-4" style="display:none">
<div class="onboarding-step-header">
<span class="onboarding-step-icon">🔌</span>
<h2 class="onboarding-step-title">Router Setup</h2>
<p class="onboarding-step-desc">
Forward these ports in your router to this Sovran_SystemsOS computer. These rules let people reach your services from outside your home network.
<strong>Ports 80 and 443 are required for HTTPS and SSL certificates.</strong>
</p>
</div>
<div class="onboarding-card" id="step-4-body">
<p class="onboarding-loading">Loading router setup…</p>
</div>
<div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
<button class="btn btn-primary onboarding-btn-next" id="step-4-next">
Continue →
</button>
</div>
</div>
<!-- ── Step 5: Complete ── -->
<div class="onboarding-panel" id="step-5" style="display:none">
<div class="onboarding-hero">
<div class="onboarding-logo">✅</div>
<h1 class="onboarding-title">Your Sovran_SystemsOS is Ready!</h1>
@@ -156,11 +180,12 @@
<ul class="onboarding-checklist" id="onboarding-checklist">
<li>✅ Timezone &amp; locale configured</li>
<li>✅ Domain configuration saved</li>
<li>✅ Port forwarding reviewed</li>
</ul>
</div>
<div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
<button class="btn btn-primary" id="step-4-finish">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="4">← Back</button>
<button class="btn btn-primary" id="step-5-finish">
Go to Dashboard →
</button>
</div>
@@ -169,7 +194,6 @@
</div><!-- /panel-wrap -->
</div><!-- /shell -->
<script src="/static/js/domain-prereqs.js?v={{ asset_version }}"></script>
<script src="/static/onboarding.js?v={{ onboarding_js_hash }}"></script>
</body>
</html>
-16
View File
@@ -1,16 +0,0 @@
{
"caddy.service": "2.8.4",
"tor.service": "0.4.8.12",
"gnome-remote-desktop.service": "46.0",
"bitcoind.service": "27.1.0",
"electrs.service": "0.10.6",
"lnd.service": "0.18.0",
"rtl.service": "0.15.8",
"btcpayserver.service": "2.4.2",
"albyhub.service": "1.8.0",
"mempool.service": "3.2.1",
"matrix-synapse.service": "1.115.0",
"livekit.service": "1.5.2",
"vaultwarden.service": "1.32.0",
"haven-relay.service": "0.1.0"
}
+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()
+44
View File
@@ -0,0 +1,44 @@
import unittest
from pathlib import Path
HUB_NIX = Path(__file__).resolve().parents[2] / "modules" / "core" / "sovran-hub.nix"
def _section(source: str, start: str, end: str) -> str:
start_idx = source.find(start)
if start_idx == -1:
raise AssertionError(f"Expected section start not found: {start!r}")
end_idx = source.find(end, start_idx)
if end_idx == -1:
raise AssertionError(f"Expected section end not found: {end!r}")
return source[start_idx:end_idx]
class HubUpdateBootStagingTests(unittest.TestCase):
def setUp(self):
self.source = HUB_NIX.read_text()
self.update_section = _section(
self.source,
'update-script = pkgs.writeShellScript "sovran-hub-update.sh" \'\'',
"# ── Rebuild wrapper script",
)
self.rebuild_section = _section(
self.source,
'rebuild-script = pkgs.writeShellScript "sovran-hub-rebuild.sh" \'\'',
"# ── Brave launcher wrapper",
)
def test_full_update_uses_boot_not_switch(self):
self.assertIn("nixos-rebuild boot --flake /etc/nixos", self.update_section)
self.assertNotIn("nixos-rebuild switch --flake /etc/nixos", self.update_section)
def test_full_update_marks_reboot_required(self):
self.assertIn('echo "REBOOT_REQUIRED" > "$STATUS"', self.update_section)
def test_rebuild_path_keeps_switch_semantics(self):
self.assertIn("nixos-rebuild switch --flake /etc/nixos", self.rebuild_section)
if __name__ == "__main__":
unittest.main()
+399
View File
@@ -0,0 +1,399 @@
"""Tests for server-local loopback diagnostics and domain validation.
Covers:
- Domain value validation and injection prevention.
- Loopback address detection (IPv4 and IPv6).
- _resolve_all_addresses returning multiple addresses.
- _check_domain_health_fast with loopback resolution.
- _evaluate_domain_checklist with loopback override — no false dns_mismatch.
- _evaluate_domain_checklist with genuine DNS mismatch — still reports error.
- api_services health stays "healthy" when domain resolves to loopback.
- api_services health stays "needs_attention" when DNS is genuinely wrong.
- api_domains_check returns "local_override" for loopback-resolved domains.
"""
import unittest
from pathlib import Path
from unittest.mock import MagicMock, mock_open, patch
import sys
import types
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
# ---------------------------------------------------------------------------
# Minimal stubs so server.py can be imported without the full FastAPI stack.
# ---------------------------------------------------------------------------
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 # noqa: E402
# ===========================================================================
# Domain value validation
# ===========================================================================
class TestValidateDomainValue(unittest.TestCase):
"""_validate_domain_value must reject anything that could corrupt /etc/hosts."""
def _v(self, value: str) -> bool:
return server._validate_domain_value(value)
# -- Valid values --------------------------------------------------------
def test_simple_domain_valid(self):
self.assertTrue(self._v("cloud.example.com"))
def test_subdomain_valid(self):
self.assertTrue(self._v("matrix.home.example.org"))
def test_single_label_with_tld_valid(self):
self.assertTrue(self._v("example.com"))
def test_hyphen_in_domain_valid(self):
self.assertTrue(self._v("my-nextcloud.example.com"))
# -- Injection / malformed values ----------------------------------------
def test_empty_string_invalid(self):
self.assertFalse(self._v(""))
def test_newline_injection_invalid(self):
self.assertFalse(self._v("evil.com\n127.0.0.1 other.host"))
def test_carriage_return_injection_invalid(self):
self.assertFalse(self._v("evil.com\r127.0.0.1 other.host"))
def test_space_injection_invalid(self):
self.assertFalse(self._v("evil.com 127.0.0.1"))
def test_hash_comment_injection_invalid(self):
self.assertFalse(self._v("evil.com# comment"))
def test_bare_hostname_no_dot_invalid(self):
self.assertFalse(self._v("localhost"))
def test_bare_ip_invalid(self):
self.assertFalse(self._v("192.168.1.1"))
def test_too_long_invalid(self):
self.assertFalse(self._v("a" * 254 + ".com"))
def test_leading_dot_invalid(self):
self.assertFalse(self._v(".example.com"))
def test_trailing_dot_invalid(self):
self.assertFalse(self._v("example.com."))
# ===========================================================================
# Loopback address detection
# ===========================================================================
class TestIsLoopbackAddress(unittest.TestCase):
def test_ipv4_loopback(self):
self.assertTrue(server._is_loopback_address("127.0.0.1"))
def test_ipv4_loopback_other(self):
self.assertTrue(server._is_loopback_address("127.0.0.2"))
def test_ipv4_loopback_high(self):
self.assertTrue(server._is_loopback_address("127.255.255.255"))
def test_ipv6_loopback(self):
self.assertTrue(server._is_loopback_address("::1"))
def test_public_ipv4_not_loopback(self):
self.assertFalse(server._is_loopback_address("203.0.113.10"))
def test_private_ipv4_not_loopback(self):
self.assertFalse(server._is_loopback_address("192.168.1.50"))
def test_ipv6_public_not_loopback(self):
self.assertFalse(server._is_loopback_address("2001:db8::1"))
def test_invalid_string_not_loopback(self):
self.assertFalse(server._is_loopback_address("not-an-ip"))
# ===========================================================================
# _check_domain_health_fast
# ===========================================================================
class TestCheckDomainHealthFast(unittest.TestCase):
"""_check_domain_health_fast returns True when there is an issue,
False when everything looks fine."""
def _fast(self, domain, external_ip, resolved_addrs):
with patch.object(server, "_resolve_all_addresses", return_value=resolved_addrs):
return server._check_domain_health_fast(domain, external_ip)
def test_no_domain_no_issue(self):
# None/empty domain: the fast check reports True (handled by checklist).
result = server._check_domain_health_fast(None, "203.0.113.10")
self.assertTrue(result)
def test_empty_domain_no_issue(self):
result = server._check_domain_health_fast("", "203.0.113.10")
self.assertTrue(result)
def test_loopback_ipv4_no_issue(self):
"""Loopback override must not be flagged as a DNS mismatch."""
result = self._fast("cloud.example.com", "203.0.113.10", ["127.0.0.1"])
self.assertFalse(result)
def test_loopback_ipv6_no_issue(self):
result = self._fast("cloud.example.com", "203.0.113.10", ["::1"])
self.assertFalse(result)
def test_matches_external_ip_no_issue(self):
result = self._fast("cloud.example.com", "203.0.113.10", ["203.0.113.10"])
self.assertFalse(result)
def test_mismatch_is_an_issue(self):
result = self._fast("cloud.example.com", "203.0.113.10", ["198.51.100.1"])
self.assertTrue(result)
def test_unavailable_external_ip_no_issue(self):
result = self._fast("cloud.example.com", "unavailable", ["198.51.100.1"])
self.assertFalse(result)
def test_multiple_addresses_one_matches_no_issue(self):
"""If any resolved address matches external_ip the check should pass."""
result = self._fast(
"cloud.example.com", "203.0.113.10",
["198.51.100.1", "203.0.113.10"],
)
self.assertFalse(result)
# ===========================================================================
# _evaluate_domain_checklist — loopback override path
# ===========================================================================
class TestEvaluateDomainChecklistLoopback(unittest.TestCase):
def _eval(self, domain, external_ip, resolved_addrs, reachable_result=None):
with (
patch.object(server, "_resolve_all_addresses", return_value=resolved_addrs),
patch.object(server, "_check_domain_reachable",
return_value=reachable_result or {"reachable": True, "status_code": 200}),
):
return server._evaluate_domain_checklist(domain, external_ip)
def test_loopback_dns_step_is_ok_not_error(self):
result = self._eval("cloud.example.com", "203.0.113.10", ["127.0.0.1"])
dns_step = next(s for s in result["domain_check_steps"] if s["step"] == 2)
self.assertEqual(dns_step["status"], "ok")
self.assertNotIn("mismatch", dns_step.get("detail", "").lower())
def test_loopback_domain_status_is_local_override(self):
result = self._eval("cloud.example.com", "203.0.113.10", ["127.0.0.1"])
self.assertEqual(result["domain_status"]["status"], "local_override")
def test_loopback_has_no_issues_when_reachable(self):
result = self._eval(
"cloud.example.com", "203.0.113.10", ["127.0.0.1"],
reachable_result={"reachable": True, "status_code": 200},
)
self.assertFalse(result["has_issues"])
def test_loopback_has_issues_when_caddy_unreachable(self):
"""A loopback override with Caddy down should still report an issue."""
result = self._eval(
"cloud.example.com", "203.0.113.10", ["127.0.0.1"],
reachable_result={"reachable": False, "error": "connection refused"},
)
self.assertTrue(result["has_issues"])
def test_ipv6_loopback_no_issue(self):
result = self._eval("cloud.example.com", "203.0.113.10", ["::1"])
self.assertEqual(result["domain_status"]["status"], "local_override")
self.assertFalse(result["has_issues"])
def test_genuine_mismatch_still_reports_error(self):
result = self._eval("cloud.example.com", "203.0.113.10", ["198.51.100.1"])
self.assertEqual(result["domain_status"]["status"], "dns_mismatch")
self.assertTrue(result["has_issues"])
def test_correct_public_dns_still_reports_ok(self):
result = self._eval("cloud.example.com", "203.0.113.10", ["203.0.113.10"])
self.assertEqual(result["domain_status"]["status"], "connected")
self.assertFalse(result["has_issues"])
def test_no_domain_has_issues(self):
result = self._eval(None, "203.0.113.10", [])
self.assertTrue(result["has_issues"])
# ===========================================================================
# api_services — composite health with loopback
# ===========================================================================
class TestApiServicesLoopbackHealth(unittest.IsolatedAsyncioTestCase):
async def _get_health(self, resolved_addrs, cached_reachable):
"""Return the health value for a single domain-requiring service."""
service_cfg = {
"services": [
{"unit": "caddy.service", "icon": "nextcloud", "enabled": True, "type": "system"}
]
}
with (
patch.object(server, "load_config", return_value=service_cfg),
patch.object(server, "_read_hub_overrides", return_value=({}, None, None)),
patch.object(server.sysctl, "is_active", return_value="active"),
patch.dict(server.SERVICE_DOMAIN_MAP, {"caddy.service": "nextcloud"}, clear=False),
patch("builtins.open", mock_open(read_data="cloud.example.com\n")),
patch.object(server, "_resolve_all_addresses", return_value=resolved_addrs),
patch.object(server, "_is_domain_reachable_cached", return_value=cached_reachable),
patch.object(server, "_get_listening_ports",
return_value={"tcp": {80, 443}, "udp": set()}),
patch.object(server, "_get_firewall_allowed_ports",
return_value={"tcp": set(), "udp": set()}),
patch.object(server, "_cached_external_ip", "203.0.113.10"),
):
results = await server.api_services()
return results[0]["health"]
async def test_loopback_and_reachable_is_healthy(self):
"""Loopback override + Caddy reachable → healthy, not needs_attention."""
health = await self._get_health(["127.0.0.1"], cached_reachable=True)
self.assertEqual(health, "healthy")
async def test_loopback_and_caddy_down_is_needs_attention(self):
"""Loopback override + Caddy unreachable → needs_attention (genuine issue)."""
health = await self._get_health(["127.0.0.1"], cached_reachable=False)
self.assertEqual(health, "needs_attention")
async def test_correct_dns_and_reachable_is_healthy(self):
health = await self._get_health(["203.0.113.10"], cached_reachable=True)
self.assertEqual(health, "healthy")
async def test_dns_mismatch_is_needs_attention(self):
health = await self._get_health(["198.51.100.1"], cached_reachable=True)
self.assertEqual(health, "needs_attention")
# ===========================================================================
# api_domains_check — loopback detection
# ===========================================================================
class TestApiDomainsCheckLoopback(unittest.IsolatedAsyncioTestCase):
async def _check(self, resolved_addrs, external_ip="203.0.113.10"):
with (
patch.object(server, "_resolve_all_addresses", return_value=resolved_addrs),
patch.object(server, "_cached_external_ip", external_ip),
):
result = await server.api_domains_check(
MagicMock(domains=["cloud.example.com"])
)
return result["domains"][0]
async def test_loopback_ipv4_returns_local_override(self):
result = await self._check(["127.0.0.1"])
self.assertEqual(result["status"], "local_override")
async def test_loopback_ipv6_returns_local_override(self):
result = await self._check(["::1"])
self.assertEqual(result["status"], "local_override")
async def test_correct_dns_returns_connected(self):
result = await self._check(["203.0.113.10"])
self.assertEqual(result["status"], "connected")
async def test_mismatch_returns_dns_mismatch(self):
result = await self._check(["198.51.100.1"])
self.assertEqual(result["status"], "dns_mismatch")
async def test_no_resolution_returns_unresolvable(self):
result = await self._check([])
self.assertEqual(result["status"], "unresolvable")
if __name__ == "__main__":
unittest.main()
+122
View File
@@ -0,0 +1,122 @@
import unittest
from pathlib import Path
RDP_NIX = Path(__file__).resolve().parents[2] / "modules" / "rdp.nix"
USERNAME_READ = "USERNAME=\"$(tr -d '\\n' < \"$USERNAME_FILE\")\""
USERNAME_LENGTH_GUARD = "if [ \"''${#USERNAME}\" -gt 32 ]; then"
SHORT_PASSWORD_GUARD = 'if [ "\'\'${#PASSWORD}" -lt 8 ]; then'
def _section(source: str, start: str, end: str) -> str:
start_idx = source.find(start)
if start_idx == -1:
raise AssertionError(f"Expected section start not found: {start!r}")
end_idx = source.find(end, start_idx)
if end_idx == -1:
raise AssertionError(f"Expected section end not found: {end!r}")
return source[start_idx:end_idx]
class RdpModuleBootSetupTests(unittest.TestCase):
def setUp(self):
self.source = RDP_NIX.read_text()
self.gnome_service = _section(
self.source,
"systemd.services.gnome-remote-desktop = {",
"systemd.tmpfiles.rules = [",
)
self.setup_service = _section(
self.source,
"systemd.services.gnome-remote-desktop-setup = {",
"};\n}",
)
def test_does_not_redeclare_gnome_remote_desktop_user(self):
self.assertNotIn("users.users.gnome-remote-desktop", self.source)
self.assertNotIn("createHome = true;", self.source)
def test_main_service_requires_setup_before_starting(self):
self.assertIn('wantedBy = [ "graphical.target" ];', self.gnome_service)
self.assertIn('after = [ "gnome-remote-desktop-setup.service" ];', self.gnome_service)
self.assertIn('requires = [ "gnome-remote-desktop-setup.service" ];', self.gnome_service)
def test_setup_waits_for_configuration_service_and_bounded_timeout(self):
self.assertIn('wantedBy = [ "graphical.target" ];', self.setup_service)
self.assertIn('before = [ "gnome-remote-desktop.service" ];', self.setup_service)
self.assertIn('"dbus.service"', self.setup_service)
self.assertIn('"gnome-remote-desktop-configuration.service"', self.setup_service)
self.assertNotIn("RemainAfterExit", self.setup_service)
self.assertIn('TimeoutStartSec = "2min";', self.setup_service)
self.assertIn('timeout --kill-after=5s 10s', self.setup_service)
self.assertIn('echo "grdctl command timed out: $*" >&2', self.setup_service)
self.assertIn('echo "grdctl command failed (exit $rc): $*" >&2', self.setup_service)
def test_setup_runs_grdctl_directly_as_root(self):
# The oneshot service runs as root; grdctl --system is called directly.
# GRD 50.x invokes pkexec internally, but the call itself is plain
# grdctl --system, not a manual pkexec invocation.
self.assertIn('grdctl --system "$@"', self.setup_service)
self.assertNotIn("runuser", self.setup_service)
self.assertNotIn("sudo", self.setup_service)
# No direct Nix-store pkexec invocation (pkgs.polkit}/bin/pkexec).
self.assertNotIn("pkgs.polkit}/bin/pkexec", self.setup_service)
def test_privilege_escalation_packages_absent_from_setup_path(self):
self.assertNotIn("pkgs.polkit", self.setup_service)
self.assertNotIn("pkgs.util-linux", self.setup_service)
def test_run_wrappers_bin_prepended_to_path(self):
# /run/wrappers/bin must be prepended to PATH before any grdctl_system
# invocation so that grdctl --system resolves the NixOS setuid pkexec.
path_export = 'export PATH="/run/wrappers/bin:$PATH"'
grdctl_marker = "grdctl_system"
script = self.setup_service
path_idx = script.find(path_export)
grdctl_idx = script.find(grdctl_marker)
self.assertGreater(path_idx, -1, f"{path_export!r} not found in setup script")
self.assertGreater(
grdctl_idx, path_idx,
"PATH export must appear before the first grdctl_system usage",
)
def test_pkexec_preflight_check(self):
# A preflight must confirm /run/wrappers/bin/pkexec is executable
# with a clear error message before any GRD configuration changes.
self.assertIn("test -x /run/wrappers/bin/pkexec", self.setup_service)
self.assertIn(
"/run/wrappers/bin/pkexec is absent or not executable",
self.setup_service,
)
def test_hub_files_are_the_source_of_truth_for_username_and_password(self):
self.assertIn('DEFAULT_USERNAME="sovran"', self.setup_service)
self.assertIn('if [ ! -f "$USERNAME_FILE" ]; then', self.setup_service)
self.assertIn(USERNAME_READ, self.setup_service)
self.assertIn(USERNAME_LENGTH_GUARD, self.setup_service)
self.assertIn('case "$USERNAME" in', self.setup_service)
self.assertIn('[A-Za-z_][A-Za-z0-9_-]*)', self.setup_service)
self.assertIn("RDP username is too long (''${#USERNAME} characters, maximum 32)", self.setup_service)
self.assertIn("RDP username must start with a letter or underscore and contain only letters, numbers, underscores, and hyphens", self.setup_service)
self.assertIn('if [ ! -f "$PASSWORD_FILE" ]; then', self.setup_service)
self.assertIn("tr -d '\\n'", self.setup_service)
self.assertIn('"$PASSWORD_FILE"', self.setup_service)
self.assertIn(SHORT_PASSWORD_GUARD, self.setup_service)
self.assertIn("RDP password is too short (''${#PASSWORD} characters, minimum 8)", self.setup_service)
self.assertIn('grdctl_system rdp set-credentials "$USERNAME" "$PASSWORD"', self.setup_service)
self.assertNotIn('grdctl --system rdp set-credentials sovran "$PASSWORD"', self.setup_service)
def test_secure_permissions_are_enforced_for_state_and_secret_files(self):
self.assertIn('"d /var/lib/gnome-remote-desktop/tls 0700', self.source)
self.assertIn("chmod 700", self.setup_service)
self.assertIn('chmod 600 "$USERNAME_FILE"', self.setup_service)
self.assertIn('chmod 600 "$PASSWORD_FILE"', self.setup_service)
self.assertIn('chmod 600 "$CRED_FILE"', self.setup_service)
self.assertIn('chmod 600 "$TLS_DIR/rdp-tls.key"', self.setup_service)
self.assertIn('chmod 644 "$TLS_DIR/rdp-tls.crt"', self.setup_service)
self.assertIn('LOCAL_IP="$(hostname -I | awk \'{print $1}\')"', self.setup_service)
self.assertIn('LOCAL_IP="127.0.0.1"', self.setup_service)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,171 @@
import unittest
from pathlib import Path
from unittest.mock import mock_open, patch
import sys
import types
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
def _install_web_stubs():
if "fastapi" in sys.modules:
return
class _HTTPException(Exception):
def __init__(self, status_code=None, detail=None):
super().__init__(detail)
self.status_code = status_code
self.detail = detail
class _FastAPI:
def __init__(self, *args, **kwargs):
pass
def mount(self, *args, **kwargs):
return None
def add_middleware(self, *args, **kwargs):
return None
def __getattr__(self, _name):
def _decorator_factory(*args, **kwargs):
def _decorator(func):
return func
return _decorator
return _decorator_factory
class _BaseModel:
pass
class _StaticFiles:
def __init__(self, *args, **kwargs):
pass
class _Jinja2Templates:
def __init__(self, *args, **kwargs):
pass
class _BaseHTTPMiddleware:
pass
fastapi_module = types.ModuleType("fastapi")
fastapi_module.FastAPI = _FastAPI
fastapi_module.HTTPException = _HTTPException
sys.modules["fastapi"] = fastapi_module
responses_module = types.ModuleType("fastapi.responses")
responses_module.HTMLResponse = object
responses_module.JSONResponse = object
responses_module.RedirectResponse = object
sys.modules["fastapi.responses"] = responses_module
staticfiles_module = types.ModuleType("fastapi.staticfiles")
staticfiles_module.StaticFiles = _StaticFiles
sys.modules["fastapi.staticfiles"] = staticfiles_module
templating_module = types.ModuleType("fastapi.templating")
templating_module.Jinja2Templates = _Jinja2Templates
sys.modules["fastapi.templating"] = templating_module
requests_module = types.ModuleType("fastapi.requests")
requests_module.Request = object
sys.modules["fastapi.requests"] = requests_module
pydantic_module = types.ModuleType("pydantic")
pydantic_module.BaseModel = _BaseModel
sys.modules["pydantic"] = pydantic_module
starlette_base_module = types.ModuleType("starlette.middleware.base")
starlette_base_module.BaseHTTPMiddleware = _BaseHTTPMiddleware
sys.modules["starlette.middleware.base"] = starlette_base_module
starlette_middleware_module = types.ModuleType("starlette.middleware")
starlette_middleware_module.base = starlette_base_module
sys.modules["starlette.middleware"] = starlette_middleware_module
starlette_module = types.ModuleType("starlette")
starlette_module.middleware = starlette_middleware_module
sys.modules["starlette"] = starlette_module
_install_web_stubs()
from sovran_systemsos_web import server
class ServiceDetailRouterWordingTests(unittest.IsolatedAsyncioTestCase):
async def test_livekit_service_detail_includes_internal_ip(self):
service_cfg = {
"services": [
{"unit": "livekit.service", "icon": "element-call", "enabled": True, "type": "system"}
]
}
domain_eval = {
"domain_status": {"status": "ok"},
"domain_reachable": {"reachable": True},
"domain_check_steps": [],
"has_issues": False,
}
with (
patch.object(server, "load_config", return_value=service_cfg),
patch.object(server, "_read_hub_overrides", return_value=({}, None, None)),
patch.object(server.sysctl, "is_active", return_value="active"),
patch.dict(server.SERVICE_DOMAIN_MAP, {"livekit.service": "element-call"}, clear=False),
patch.dict(
server.SERVICE_PORT_REQUIREMENTS,
{"livekit.service": [{"port": "7881", "protocol": "TCP", "description": "LiveKit"}]},
clear=False,
),
patch("builtins.open", mock_open(read_data="call.example.com\n")),
patch.object(server, "_evaluate_domain_checklist", return_value=domain_eval),
patch.object(server, "_get_internal_ip", return_value="192.168.1.44"),
patch.object(server, "_save_internal_ip"),
patch.object(server, "_get_listening_ports", return_value={"tcp": {7881}, "udp": set()}),
patch.object(server, "_get_firewall_allowed_ports", return_value={"tcp": set(), "udp": set()}),
):
result = await server.api_service_detail("livekit.service")
self.assertEqual(result["internal_ip"], "192.168.1.44")
self.assertEqual(result["extra_ports"][0]["status"], "listening")
self.assertEqual(result["domain_check_steps"][-1]["label"], "Router Setup Needed")
async def test_livekit_router_step_uses_not_ready_yet_wording(self):
service_cfg = {
"services": [
{"unit": "livekit.service", "icon": "element-call", "enabled": True, "type": "system"}
]
}
domain_eval = {
"domain_status": {"status": "ok"},
"domain_reachable": {"reachable": True},
"domain_check_steps": [],
"has_issues": False,
}
with (
patch.object(server, "load_config", return_value=service_cfg),
patch.object(server, "_read_hub_overrides", return_value=({}, None, None)),
patch.object(server.sysctl, "is_active", return_value="active"),
patch.dict(server.SERVICE_DOMAIN_MAP, {"livekit.service": "element-call"}, clear=False),
patch.dict(
server.SERVICE_PORT_REQUIREMENTS,
{"livekit.service": [{"port": "7881", "protocol": "TCP", "description": "LiveKit"}]},
clear=False,
),
patch("builtins.open", mock_open(read_data="call.example.com\n")),
patch.object(server, "_evaluate_domain_checklist", return_value=domain_eval),
patch.object(server, "_get_internal_ip", return_value="192.168.1.44"),
patch.object(server, "_save_internal_ip"),
patch.object(server, "_get_listening_ports", return_value={"tcp": set(), "udp": set()}),
patch.object(server, "_get_firewall_allowed_ports", return_value={"tcp": set(), "udp": set()}),
):
result = await server.api_service_detail("livekit.service")
self.assertEqual(result["extra_ports"][0]["status"], "closed")
self.assertIn("Not ready yet", result["domain_check_steps"][-1]["detail"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,99 @@
"""Regression test for Starlette 1.1.0+ TemplateResponse keyword-argument style.
Prior to this fix, the three HTML routes called:
templates.TemplateResponse("name.html", {"request": request, ...})
which passes the context dict as the second positional argument. With the
updated Starlette/FastAPI versions shipped in NixOS unstable (Starlette 1.1.0,
FastAPI 0.136.3) that positional argument is the template name, causing Jinja2
to receive a dict as a cache key and raise:
TypeError: unhashable type: 'dict'
The fix updates every call to use keyword arguments:
templates.TemplateResponse(request=request, name="name.html", context={...})
"""
import ast
import unittest
from pathlib import Path
SERVER_PY = Path(__file__).resolve().parents[1] / "sovran_systemsos_web" / "server.py"
def _template_response_calls(source: str):
"""Return a list of ast.Call nodes that are TemplateResponse calls."""
tree = ast.parse(source)
calls = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
if isinstance(func, ast.Attribute) and func.attr == "TemplateResponse":
calls.append(node)
return calls
class TemplateResponseSignatureTests(unittest.TestCase):
def setUp(self):
self.source = SERVER_PY.read_text()
self.calls = _template_response_calls(self.source)
def test_at_least_one_template_response_call_found(self):
self.assertGreater(len(self.calls), 0, "No TemplateResponse calls found in server.py")
def test_no_old_style_positional_dict_context(self):
"""No TemplateResponse call should pass a dict literal as its second positional arg.
The old style was:
templates.TemplateResponse("name.html", {"request": request, ...})
where args[0] is a string and args[1] is a Dict node. That pattern
triggers the Starlette 1.1.0 bug.
"""
for call in self.calls:
positional = call.args
if len(positional) >= 2 and isinstance(positional[1], ast.Dict):
self.fail(
f"Found old-style TemplateResponse call at line {call.lineno}: "
"second positional argument is a dict literal. "
"Use keyword arguments (request=, name=, context=) instead."
)
def test_request_not_duplicated_in_context(self):
"""The 'request' key must not appear inside the context= dict when
request= is already passed as a dedicated keyword argument."""
for call in self.calls:
kw_dict = {kw.arg: kw.value for kw in call.keywords if isinstance(kw, ast.keyword)}
if "request" not in kw_dict:
continue # no request= kwarg, nothing to check
context_node = kw_dict.get("context")
if not isinstance(context_node, ast.Dict):
continue
for key_node in context_node.keys:
if isinstance(key_node, ast.Constant) and key_node.value == "request":
self.fail(
f"TemplateResponse at line {call.lineno} passes 'request' both as "
"request= keyword argument and inside the context dict."
)
def test_all_calls_use_keyword_arguments(self):
"""Every TemplateResponse call should use keyword arguments for request, name,
and context rather than relying on positional ordering."""
for call in self.calls:
kw_args = {kw.arg for kw in call.keywords if isinstance(kw, ast.keyword)}
self.assertIn(
"request",
kw_args,
f"TemplateResponse at line {call.lineno} is missing keyword argument 'request='.",
)
self.assertIn(
"name",
kw_args,
f"TemplateResponse at line {call.lineno} is missing keyword argument 'name='.",
)
if __name__ == "__main__":
unittest.main()
+6 -3
View File
@@ -192,9 +192,12 @@ backup /etc/nix-bitcoin-secrets/ localhost/
};
# ── Cron ───────────────────────────────────────────────────
# The legacy njalla.sh root cron job has been replaced by the systemd timer
# defined in modules/core/njalla.nix (sovran-ddns-update.timer). Cron is
# retained so that rsnapshot and other module-defined cron jobs continue to run.
services.cron = {
enable = true;
systemCronJobs = [
"*/15 * * * * root /run/current-system/sw/bin/bash /var/lib/njalla/njalla.sh"
];
};
# ── Tor ────────────────────────────────────────────────────
services.tor = { enable = true; client.enable = true; torsocks.enable = true; };
Generated
+154 -23
View File
@@ -5,11 +5,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1785778881,
"narHash": "sha256-yHJJTm3o7ZhiVp68G+hh+dz1GKV+ACOT6mQj+eYvJLQ=",
"lastModified": 1783519926,
"narHash": "sha256-2zwAN4lNitHFrHVnRZG3YcvpdtWOoF0cOBstxMeB1KI=",
"owner": "emmanuelrosa",
"repo": "btc-clients-nix",
"rev": "fc1aca94d839f82e7501fe0d01279d5c1fba6e63",
"rev": "731a1e11c2fefb14f0aa4b1f03cfa85c19c28d71",
"type": "github"
},
"original": {
@@ -18,6 +18,32 @@
"type": "github"
}
},
"extra-container": {
"inputs": {
"flake-utils": [
"nix-bitcoin",
"flake-utils"
],
"nixpkgs": [
"nix-bitcoin",
"nixpkgs"
]
},
"locked": {
"lastModified": 1766155727,
"narHash": "sha256-XGp4HHH6D6ZKiO5RnMzqYJYnZB538EnEflvlTsOKpvo=",
"owner": "erikarvstedt",
"repo": "extra-container",
"rev": "b450bdb24fca1076973c852d87bcb49b8eb5fd49",
"type": "github"
},
"original": {
"owner": "erikarvstedt",
"ref": "0.14",
"repo": "extra-container",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
@@ -39,13 +65,54 @@
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-bitcoin": {
"inputs": {
"extra-container": "extra-container",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2",
"nixpkgs-25_05": "nixpkgs-25_05",
"nixpkgs-unstable": "nixpkgs-unstable"
},
"locked": {
"lastModified": 1779253922,
"narHash": "sha256-k5DpYVfyy27ELuEiV+51EfVg7B6vKUW63NWeA6eKGd0=",
"owner": "fort-nix",
"repo": "nix-bitcoin",
"rev": "1496f842477976c085cd96f1837ea12444014088",
"type": "github"
},
"original": {
"owner": "fort-nix",
"ref": "release",
"repo": "nix-bitcoin",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1785590095,
"narHash": "sha256-CNO2szJbdLjVN/Hi1BML9MSALz1GM2fIdwnzs404QO8=",
"lastModified": 1782911660,
"narHash": "sha256-PbR+tJ5E/Ux+01UtdFKqblccVA4/FgWbkym4ev3VHHQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e568f3b19d54b08f48bfae9b12b3e124d1a28002",
"rev": "cf720c15e108d432d29041cc5a185630809acefb",
"type": "github"
},
"original": {
@@ -54,13 +121,29 @@
"type": "github"
}
},
"nixpkgs-25_05": {
"locked": {
"lastModified": 1767313136,
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1786201459,
"narHash": "sha256-CiOTEjmwAmG2AWnaIno9YaCJJmpca2FXPhMAsnrolCg=",
"lastModified": 1783856661,
"narHash": "sha256-ZGP04e+Q6WyQJGA9ZvI5CL6+heGQldbAG9U1T9NGvmU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "8b8c811c7c2541c30382c5de7ed26be055569c60",
"rev": "569d578509928497eddc3fdbf94a799027050be4",
"type": "github"
},
"original": {
@@ -70,13 +153,45 @@
"type": "github"
}
},
"nixpkgs_2": {
"nixpkgs-unstable": {
"locked": {
"lastModified": 1786106723,
"narHash": "sha256-zDSUbpoeo/9ZmD2+wXnzxoo1+uhL8vxc0b8yuYMKYq0=",
"lastModified": 1778869304,
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f13ff45afd1bb73e640eaa08a7066dbed07e3238",
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1778737229,
"narHash": "sha256-6xWoytx8jFW4PF1GjRm/i/53trbpKGfz6zjzQGBr4cI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d7a713c0b7e47c908258e71cba7a2d77cc8d71d5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1783776592,
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
"type": "github"
},
"original": {
@@ -86,13 +201,13 @@
"type": "github"
}
},
"nixpkgs_3": {
"nixpkgs_4": {
"locked": {
"lastModified": 1784555310,
"narHash": "sha256-/FCliTPgiuV1owejZFNx3Ch9irdvkOfOFl+HHZ+DrtM=",
"lastModified": 1783791668,
"narHash": "sha256-zbcZ1dmBTPfJ7Mlqh/yLEPGpgJnwuv4Xr1xucy2WqMA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "421eebfd0ec7bccd4abe826ce62d7e6e83129493",
"rev": "716c7a2664ca8325617b8a7fbb609273f2c4cae7",
"type": "github"
},
"original": {
@@ -105,15 +220,15 @@
"nixvim": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs_3",
"systems": "systems"
"nixpkgs": "nixpkgs_4",
"systems": "systems_2"
},
"locked": {
"lastModified": 1785763201,
"narHash": "sha256-wA373y/B9orM3HatLu9oS+Ke5lmdZyBl/bdjd2gLMq4=",
"lastModified": 1783941741,
"narHash": "sha256-F+3M1IZrJa920cx2/k2AMKqedEodxLF7COJVkLJwUBo=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "c7be49306b23a952c0151cf4bbeacd944ed82f2a",
"rev": "e6715f01d9f56f07a27a01386b85ae22b06f0705",
"type": "github"
},
"original": {
@@ -125,12 +240,28 @@
"root": {
"inputs": {
"btc-clients": "btc-clients",
"nixpkgs": "nixpkgs_2",
"nix-bitcoin": "nix-bitcoin",
"nixpkgs": "nixpkgs_3",
"nixpkgs-stable": "nixpkgs-stable",
"nixvim": "nixvim"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1774449309,
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
Regular → Executable
+7 -22
View File
@@ -3,12 +3,13 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
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-26.05";
};
outputs = { self, nixpkgs, nixvim, btc-clients, nixpkgs-stable, ... }:
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, ... }:
let
overlay-stable = final: prev: {
@@ -21,7 +22,7 @@
{
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; }
{ nixpkgs.hostPlatform = "x86_64-linux"; }
self.nixosModules.Sovran_SystemsOS
./hardware-configuration.nix
./role-state.nix
@@ -31,9 +32,10 @@
nixosConfigurations.sovran_systemsos-iso = nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; }
{ nixpkgs.hostPlatform = "x86_64-linux"; }
({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; })
./iso/common.nix
./modules/bitcoin
nix-bitcoin.nixosModules.default
nixvim.nixosModules.nixvim
];
};
@@ -44,7 +46,7 @@
nixpkgs.overlays = [ overlay-stable ];
})
./configuration.nix
./modules/bitcoin
nix-bitcoin.nixosModules.default
nixvim.nixosModules.nixvim
];
config = {
@@ -55,22 +57,5 @@
];
};
};
checks.x86_64-linux = let
pkgs = import nixpkgs {
system = "x86_64-linux";
};
fetchNodeModules =
pkgs.callPackage ./packages/build-support/fetch-node-modules.nix {};
mempoolPkgs =
pkgs.callPackage ./packages/mempool { inherit fetchNodeModules; };
in {
bitcoin-btcpay-hardening = import ./tests/bitcoin-btcpay-hardening.nix {
inherit nixpkgs overlay-stable;
};
mempool-backend = mempoolPkgs.mempool-backend;
mempool-frontend = mempoolPkgs.mempool-frontend;
rtl = pkgs.callPackage ./packages/rtl { inherit fetchNodeModules; };
};
};
}
+1 -10
View File
@@ -3,14 +3,6 @@
let
sovranSource = builtins.path { path = ../.; name = "sovran-systemsos"; };
# Read version from VERSION file if it exists, otherwise fall back to "dev"
versionFile = if builtins.pathExists ../VERSION
then builtins.readFile ../VERSION
else "dev";
# Clean version (remove 'v' prefix and newlines)
cleanVersion = builtins.replaceStrings ["v" "\n" "\r"] ["" "" ""] versionFile;
pythonEnv = pkgs.python3.withPackages (ps: [ ps.pygobject3 ps.pycairo ]);
installerPy = pkgs.writeShellScriptBin "sovran-install" ''
@@ -26,8 +18,7 @@ in
"${modulesPath}/installer/cd-dvd/installation-cd-graphical-gnome.nix"
];
# Dynamic ISO filename with version
image.baseName = lib.mkForce "Sovran_SystemsOS-${cleanVersion}";
image.baseName = lib.mkForce "Sovran_SystemsOS";
isoImage.splashImage = ./assets/splash-logo.png;
services.gnome.gnome-initial-setup.enable = false;
+31 -194
View File
@@ -103,81 +103,25 @@ def human_size(nbytes):
return f"{nbytes:.1f} PB"
def check_internet():
"""Return True if the machine can reach the internet.
Some VM NAT setups and managed networks block ICMP even when HTTPS works,
so keep the existing ping checks but fall back to small HTTPS requests.
"""
checks = [
["ping", "-c", "1", "-W", "5", "nixos.org"],
["curl", "--location", "--fail", "--silent", "--show-error", "--connect-timeout", "5", "--max-time", "10", "--output", "/dev/null", "https://cache.nixos.org/nix-cache-info"],
["curl", "--location", "--fail", "--silent", "--show-error", "--connect-timeout", "5", "--max-time", "10", "--output", "/dev/null", "https://nixos.org/"],
["ping", "-c", "1", "-W", "5", "1.1.1.1"],
]
for cmd in checks:
try:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
return True
log(f"Connectivity check failed ({' '.join(cmd)}): {result.stderr.strip() or result.stdout.strip()}")
except Exception as e:
log(f"Connectivity check failed ({' '.join(cmd)}): {e}")
return False
def detect_virtualization():
"""Return the VM technology name when running inside a VM, else None."""
"""Return True if the machine can reach the internet."""
try:
result = subprocess.run(
["systemd-detect-virt", "--vm"],
["ping", "-c", "1", "-W", "5", "nixos.org"],
capture_output=True, text=True
)
if result.returncode == 0:
name = result.stdout.strip()
if name == "none":
return None
return name or "virtual machine"
return True
except Exception:
pass
# Fallback for minimal environments where systemd-detect-virt is absent.
dmi_paths = [
"/sys/class/dmi/id/product_name",
"/sys/class/dmi/id/sys_vendor",
"/sys/class/dmi/id/board_vendor",
]
markers = (
"kvm", "qemu", "virtualbox", "vmware", "hyper-v",
"bhyve", "parallels", "xen", "bochs", "virtual",
)
for dmi_path in dmi_paths:
try:
value = open(dmi_path, "r", encoding="utf-8", errors="ignore").read().strip()
except OSError:
continue
lowered = value.lower()
if any(marker in lowered for marker in markers):
return value or "virtual machine"
return None
def detect_boot_mode():
"""Return the firmware mode used to boot the installer ISO."""
return "uefi" if os.path.isdir("/sys/firmware/efi") else "bios"
def boot_mode_label(mode):
if mode == "uefi":
return "UEFI (systemd-boot)"
return "Legacy BIOS (GRUB)"
def nix_string(value):
"""Quote a Python string for a simple Nix string literal."""
escaped = value.replace("\\", "\\\\")
escaped = escaped.replace('"', '\\"')
escaped = escaped.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
escaped = escaped.replace("${", "\\${")
return f'"{escaped}"'
# Fallback: try a second host in case DNS for nixos.org is down
try:
result = subprocess.run(
["ping", "-c", "1", "-W", "5", "1.1.1.1"],
capture_output=True, text=True
)
return result.returncode == 0
except Exception:
return False
def symbolic_icon(name):
"""Create a crisp symbolic icon suitable for use as an ActionRow prefix."""
@@ -215,10 +159,6 @@ class InstallerWindow(Adw.ApplicationWindow):
self.data_size = None
self.data_drive_has_timechain = False
self.free_password = None
self.virtualization = detect_virtualization()
self.boot_mode = detect_boot_mode()
log(f"Installer environment: boot_mode={self.boot_mode}, virtualization={self.virtualization or 'none'}")
# Root navigation view
self.nav = Adw.NavigationView()
@@ -403,7 +343,7 @@ class InstallerWindow(Adw.ApplicationWindow):
if os.path.exists(LOGO):
try:
img = Gtk.Image.new_from_file(LOGO)
img.set_pixel_size(320 if self.virtualization else 480)
img.set_pixel_size(480)
hero.append(img)
except Exception:
pass
@@ -457,42 +397,6 @@ class InstallerWindow(Adw.ApplicationWindow):
notice_frame.set_child(notice_box)
outer.append(notice_frame)
if self.virtualization:
vm_frame = Gtk.Frame()
vm_frame.add_css_class("card")
vm_frame.set_margin_start(40)
vm_frame.set_margin_end(40)
vm_frame.set_margin_top(12)
vm_frame.set_margin_bottom(4)
vm_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
vm_box.set_margin_top(12)
vm_box.set_margin_bottom(12)
vm_box.set_margin_start(16)
vm_box.set_margin_end(16)
vm_icon = symbolic_icon("computer-symbolic")
vm_icon.set_valign(Gtk.Align.START)
vm_box.append(vm_icon)
vm_lbl = Gtk.Label()
vm_lbl.set_use_markup(True)
vm_lbl.set_wrap(True)
vm_lbl.set_xalign(0)
vm_lbl.set_halign(Gtk.Align.FILL)
vm_lbl.set_markup(
f"<span weight='bold'>Virtual machine detected: {GLib.markup_escape_text(self.virtualization)}</span>\n"
f"Boot mode: <span weight='bold'>{boot_mode_label(self.boot_mode)}</span>. "
"For the smoothest VM test, use 8+ GB RAM, NAT/bridged networking, "
"and a thin-provisioned virtual OS disk of at least 256 GB. "
"For Node or Server + Desktop, attach a second 2 TB virtual data disk; "
"otherwise choose Desktop Only."
)
vm_box.append(vm_lbl)
vm_frame.set_child(vm_box)
outer.append(vm_frame)
# Role label
role_lbl = Gtk.Label()
role_lbl.set_markup("<span size='medium' weight='bold'>Choose your installation type:</span>")
@@ -544,10 +448,7 @@ class InstallerWindow(Adw.ApplicationWindow):
available = has_second_drive or key not in NEEDS_DATA_DRIVE
if not available:
drive_hint = "⚠ Requires a second internal drive (not detected)"
if self.virtualization:
drive_hint += " — in a VM, attach a second virtual disk or choose Desktop Only"
card.set_subtitle(desc + f"\n{drive_hint}")
card.set_subtitle(desc + "\n⚠ Requires a second internal drive (not detected)")
card.set_sensitive(False)
else:
card.set_subtitle(desc)
@@ -630,10 +531,9 @@ class InstallerWindow(Adw.ApplicationWindow):
# ── OS Drive group ────────────────────────────────────────────
os_group = Adw.PreferencesGroup()
os_group.set_title("OS Drive (NixOS Boot + Root)")
os_description = "Choose the drive for the NixOS installation. Minimum 256 GB required."
if self.virtualization:
os_description += " In a VM, a thin-provisioned virtual disk is OK."
os_group.set_description(os_description)
os_group.set_description(
"Choose the drive for the NixOS installation. Minimum 256 GB required."
)
os_group.set_margin_top(24)
os_group.set_margin_start(40)
os_group.set_margin_end(40)
@@ -646,8 +546,6 @@ class InstallerWindow(Adw.ApplicationWindow):
row.set_title(f"/dev/{name}")
type_label = tran.upper() if tran else "Disk"
meets = "✓ Meets 256 GB minimum" if size >= BYTES_256GB else "✗ Below 256 GB minimum"
if self.virtualization and size < BYTES_256GB:
meets += " (expand the virtual disk)"
row.set_subtitle(f"{human_size(size)} · {type_label} — {meets}")
row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
@@ -672,14 +570,11 @@ class InstallerWindow(Adw.ApplicationWindow):
if self.role != "Desktop Only":
data_group = Adw.PreferencesGroup()
data_group.set_title("Bitcoin Timechain & Backups Drive")
data_description = (
data_group.set_description(
"💡 Tip: Always assign your LARGEST drive here. "
"The full Bitcoin timechain is over 700 GB and grows continuously — "
"a 2 TB or larger drive is required."
)
if self.virtualization:
data_description += " In a VM, attach a second 2 TB thin-provisioned virtual disk for Node or Server + Desktop."
data_group.set_description(data_description)
data_group.set_margin_top(20)
data_group.set_margin_start(40)
data_group.set_margin_end(40)
@@ -704,8 +599,6 @@ class InstallerWindow(Adw.ApplicationWindow):
row.set_title(f"/dev/{name}")
type_label = tran.upper() if tran else "Disk"
meets = "✓ Meets 2 TB minimum" if size >= BYTES_2TB else "✗ Below 2 TB minimum"
if self.virtualization and size < BYTES_2TB:
meets += " (use a 2 TB thin-provisioned virtual disk)"
row.set_subtitle(f"{human_size(size)} · {type_label} — {meets}")
row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
@@ -762,16 +655,10 @@ class InstallerWindow(Adw.ApplicationWindow):
dlg = Adw.MessageDialog()
dlg.set_transient_for(self)
dlg.set_heading("OS Drive Too Small")
body = (
dlg.set_body(
f"The selected OS drive (/dev/{os_name}, {human_size(os_size)}) "
f"does not meet the 256 GB minimum. Please choose a larger drive."
)
if self.virtualization:
body += (
"\n\nVM tip: expand the virtual OS disk to at least 256 GB. "
"Thin-provisioned disks usually do not consume the full size immediately."
)
dlg.set_body(body)
dlg.add_response("ok", "OK")
dlg.present()
return
@@ -781,18 +668,12 @@ class InstallerWindow(Adw.ApplicationWindow):
dlg = Adw.MessageDialog()
dlg.set_transient_for(self)
dlg.set_heading("Bitcoin Drive Too Small")
body = (
dlg.set_body(
f"The selected Bitcoin Timechain & Backups drive "
f"(/dev/{data_name}, {human_size(data_size)}) "
f"does not meet the 2 TB minimum. "
f"Please choose a larger drive or select \"None\"."
)
if self.virtualization:
body += (
"\n\nVM tip: attach a second 2 TB thin-provisioned virtual disk "
"for Node or Server + Desktop test installs."
)
dlg.set_body(body)
dlg.add_response("ok", "OK")
dlg.present()
return
@@ -948,12 +829,7 @@ class InstallerWindow(Adw.ApplicationWindow):
# ── Worker: partition ─────────────────────────────────────────────────
def partition_path(self, dev_path, num):
base = os.path.basename(dev_path)
needs_p = (
base.startswith(("nvme", "mmcblk", "loop", "md")) or
(base[-1:].isdigit())
)
return f"{dev_path}p{num}" if needs_p else f"{dev_path}{num}"
return f"{dev_path}p{num}" if "nvme" in dev_path else f"{dev_path}{num}"
def detect_existing_timechain_data(self, data_path, buf=None):
data_p1 = self.partition_path(data_path, 1)
@@ -1024,19 +900,12 @@ class InstallerWindow(Adw.ApplicationWindow):
time.sleep(2)
# ── Partition boot disk ──
if self.boot_mode == "uefi":
GLib.idle_add(append_text, buf, "\n=== Partitioning boot disk (UEFI) ===\n")
run_stream(["sudo", "sgdisk",
"-n", "1:1M:+512M", "-t", "1:EF00", "-c", "1:ESP",
"-n", "2:0:0", "-t", "2:8300", "-c", "2:root",
boot_path], buf)
else:
GLib.idle_add(append_text, buf, "\n=== Partitioning boot disk (Legacy BIOS) ===\n")
run_stream(["sudo", "sgdisk",
"-n", "1:1M:+1M", "-t", "1:EF02", "-c", "1:BIOS-boot",
"-n", "2:0:0", "-t", "2:8300", "-c", "2:root",
boot_path], buf)
# ── Partition boot disk: 512M ESP + rest as root ──
GLib.idle_add(append_text, buf, "\n=== Partitioning boot disk ===\n")
run_stream(["sudo", "sgdisk",
"-n", "1:1M:+512M", "-t", "1:EF00", "-c", "1:ESP",
"-n", "2:0:0", "-t", "2:8300", "-c", "2:root",
boot_path], buf)
run_stream(["sudo", "partprobe", boot_path], buf)
time.sleep(2)
@@ -1056,10 +925,7 @@ class InstallerWindow(Adw.ApplicationWindow):
boot_p1 = self.partition_path(boot_path, 1)
boot_p2 = self.partition_path(boot_path, 2)
if self.boot_mode == "uefi":
run_stream(["sudo", "mkfs.vfat", "-F", "32", boot_p1], buf)
else:
GLib.idle_add(append_text, buf, "Skipping ESP format for Legacy BIOS install.\n")
run_stream(["sudo", "mkfs.vfat", "-F", "32", boot_p1], buf)
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "sovran_systemsos", boot_p2], buf)
if data_path and not self.data_drive_has_timechain:
@@ -1069,11 +935,8 @@ class InstallerWindow(Adw.ApplicationWindow):
# ── Mount filesystems ──
GLib.idle_add(append_text, buf, "\n=== Mounting filesystems ===\n")
run_stream(["sudo", "mount", boot_p2, "/mnt"], buf)
if self.boot_mode == "uefi":
run_stream(["sudo", "mkdir", "-p", "/mnt/boot/efi"], buf)
run_stream(["sudo", "mount", "-o", "umask=0077,defaults", boot_p1, "/mnt/boot/efi"], buf)
else:
GLib.idle_add(append_text, buf, "Legacy BIOS install: /boot/efi mount not required.\n")
run_stream(["sudo", "mkdir", "-p", "/mnt/boot/efi"], buf)
run_stream(["sudo", "mount", "-o", "umask=0077,defaults", boot_p1, "/mnt/boot/efi"], buf)
if data_path:
data_p1 = self.partition_path(data_path, 1)
@@ -1103,33 +966,12 @@ class InstallerWindow(Adw.ApplicationWindow):
is_server = str(self.role == "Server+Desktop").lower()
is_desktop = str(self.role == "Desktop Only").lower()
is_node = str(self.role == "Node (Bitcoin-only)").lower()
boot_overrides = ""
if self.boot_mode == "bios":
grub_device = nix_string(f"/dev/{self.boot_disk}")
boot_overrides = f"""
# The installer ISO was booted in Legacy BIOS mode, so install GRUB to the
# target disk instead of using the default UEFI systemd-boot configuration.
boot.loader.systemd-boot.enable = lib.mkForce false;
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
boot.loader.grub.enable = lib.mkForce true;
boot.loader.grub.device = lib.mkForce {grub_device};
fileSystems."/boot/efi".enable = lib.mkForce false;
"""
elif self.virtualization:
boot_overrides = """
# VM UEFI firmware can reject or forget NVRAM boot-entry writes. Keep the
# normal UEFI systemd-boot layout, but use the fallback removable path instead
# of depending on EFI variable updates.
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
"""
content = f"""# THIS FILE IS AUTO-GENERATED BY THE INSTALLER. DO NOT EDIT.
{{ config, lib, ... }}:
{{
sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault {is_server};
sovran_systemsOS.roles.desktop = lib.mkDefault {is_desktop};
sovran_systemsOS.roles.node = lib.mkDefault {is_node};{boot_overrides}
sovran_systemsOS.roles.node = lib.mkDefault {is_node};
}}
"""
proc = subprocess.run(
@@ -1166,11 +1008,6 @@ class InstallerWindow(Adw.ApplicationWindow):
boot_row.set_subtitle(f"/dev/{self.boot_disk} — {human_size(self.boot_size)}")
details.add(boot_row)
boot_mode_row = Adw.ActionRow()
boot_mode_row.set_title("Boot Mode")
boot_mode_row.set_subtitle(boot_mode_label(self.boot_mode))
details.add(boot_mode_row)
if self.data_disk:
data_row = Adw.ActionRow()
data_row.set_title("Data Disk")
+12 -88
View File
@@ -12,11 +12,6 @@ Options:
--deploy-key "ssh-ed25519 AAAA..." SSH pubkey for remote access after install
--headscale-server URL Headscale login server for post-install Tailnet
--headscale-key KEY Headscale pre-auth key for the installed OS
VM notes:
Desktop test installs still need a 256 GB OS disk (thin-provisioned is OK).
Node/server installs need a separate 2 TB data disk. UEFI is preferred; UEFI
VMs avoid NVRAM boot-entry writes, and legacy BIOS VMs use GRUB.
USAGE
}
@@ -30,8 +25,6 @@ DEPLOY_KEY=""
HEADSCALE_SERVER=""
HEADSCALE_KEY=""
DATA_DISK_HAS_TIMECHAIN=false
BOOT_MODE=""
VIRTUALIZATION=""
FLAKE="/etc/sovran/flake"
LOG="/tmp/sovran-headless-install.log"
@@ -76,22 +69,6 @@ case "$ROLE" in
*) die "--role must be one of: server, desktop, node" ;;
esac
# ── Detect firmware / VM environment ─────────────────────────────────────────
if [[ -d /sys/firmware/efi ]]; then
BOOT_MODE="uefi"
else
BOOT_MODE="bios"
fi
if command -v systemd-detect-virt >/dev/null 2>&1; then
if VIRT_RESULT=$(systemd-detect-virt --vm 2>/dev/null); then
[[ "$VIRT_RESULT" != "none" ]] && VIRTUALIZATION="$VIRT_RESULT"
fi
fi
log "Boot mode: ${BOOT_MODE} ($([[ "$BOOT_MODE" == uefi ]] && echo systemd-boot || echo GRUB))"
[[ -n "$VIRTUALIZATION" ]] && log "Virtual machine detected: ${VIRTUALIZATION}"
# ── Validate disk existence and size ─────────────────────────────────────────
log "=== Validating disks ==="
@@ -119,26 +96,13 @@ fi
# ── Helper: partition suffix ──────────────────────────────────────────────────
part_suffix() {
local dev="$1" n="$2"
local base
base=$(basename "$dev")
if [[ "$base" == nvme* || "$base" == mmcblk* || "$base" == loop* || "$base" == md* || "$base" =~ [0-9]$ ]]; then
if [[ "$dev" == *nvme* ]]; then
echo "${dev}p${n}"
else
echo "${dev}${n}"
fi
}
nix_string() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
value="${value//$'\n'/\\n}"
value="${value//$'\r'/\\r}"
value="${value//$'\t'/\\t}"
value="${value//\$\{/\\\$\{}"
printf '"%s"' "$value"
}
# ── Detect existing Bitcoin timechain data on data disk ───────────────────────
if [[ -n "$DATA_DISK" ]]; then
DATA_P1=$(part_suffix "$DATA_DISK" 1)
@@ -178,19 +142,12 @@ partprobe "$DISK"
sleep 2
# ── Step 2: Partition OS disk ─────────────────────────────────────────────────
if [[ "$BOOT_MODE" == "uefi" ]]; then
log "=== Partitioning OS disk (UEFI) ==="
sgdisk \
-n "1:1M:+512M" -t "1:EF00" -c "1:ESP" \
-n "2:0:0" -t "2:8300" -c "2:root" \
"$DISK"
else
log "=== Partitioning OS disk (Legacy BIOS) ==="
sgdisk \
-n "1:1M:+1M" -t "1:EF02" -c "1:BIOS-boot" \
-n "2:0:0" -t "2:8300" -c "2:root" \
"$DISK"
fi
log "=== Partitioning OS disk ==="
sgdisk \
-n "1:1M:+512M" -t "1:EF00" -c "1:ESP" \
-n "2:0:0" -t "2:8300" -c "2:root" \
"$DISK"
partprobe "$DISK"
sleep 2
@@ -211,11 +168,7 @@ log "=== Formatting partitions ==="
BOOT_P1=$(part_suffix "$DISK" 1)
BOOT_P2=$(part_suffix "$DISK" 2)
if [[ "$BOOT_MODE" == "uefi" ]]; then
mkfs.vfat -F 32 "$BOOT_P1"
else
log "Skipping ESP format for Legacy BIOS install"
fi
mkfs.vfat -F 32 "$BOOT_P1"
mkfs.ext4 -F -L sovran_systemsos "$BOOT_P2"
if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
@@ -227,12 +180,8 @@ fi
log "=== Mounting filesystems ==="
mount "$BOOT_P2" /mnt
if [[ "$BOOT_MODE" == "uefi" ]]; then
mkdir -p /mnt/boot/efi
mount -o umask=0077,defaults "$BOOT_P1" /mnt/boot/efi
else
log "Legacy BIOS install: /boot/efi mount not required"
fi
mkdir -p /mnt/boot/efi
mount -o umask=0077,defaults "$BOOT_P1" /mnt/boot/efi
if [[ -n "$DATA_DISK" ]]; then
DATA_P1=$(part_suffix "$DATA_DISK" 1)
@@ -270,41 +219,16 @@ case "$ROLE" in
IS_SERVER=false; IS_DESKTOP=false; IS_NODE=true ;;
esac
{
cat <<EOF
cat > /mnt/etc/nixos/role-state.nix <<EOF
# THIS FILE IS AUTO-GENERATED BY THE INSTALLER. DO NOT EDIT.
{ config, lib, ... }:
{
sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault ${IS_SERVER};
sovran_systemsOS.roles.desktop = lib.mkDefault ${IS_DESKTOP};
sovran_systemsOS.roles.node = lib.mkDefault ${IS_NODE};
}
EOF
if [[ "$BOOT_MODE" == "bios" ]]; then
GRUB_DEVICE=$(nix_string "$DISK")
cat <<EOF
# The installer ISO was booted in Legacy BIOS mode, so install GRUB to the
# target disk instead of using the default UEFI systemd-boot configuration.
boot.loader.systemd-boot.enable = lib.mkForce false;
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
boot.loader.grub.enable = lib.mkForce true;
boot.loader.grub.device = lib.mkForce ${GRUB_DEVICE};
fileSystems."/boot/efi".enable = lib.mkForce false;
EOF
elif [[ -n "$VIRTUALIZATION" ]]; then
cat <<EOF
# VM UEFI firmware can reject or forget NVRAM boot-entry writes. Keep the
# normal UEFI systemd-boot layout, but use the fallback removable path instead
# of depending on EFI variable updates.
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
EOF
fi
echo "}"
} > /mnt/etc/nixos/role-state.nix
# ── Step 10: Write custom.nix with deploy config ──────────────────────────────
log "=== Writing custom.nix ==="
+3 -3
View File
@@ -1,7 +1,7 @@
{ config, pkgs, lib, ... }:
lib.mkIf config.sovran_systemsOS.features.bitcoin-core {
# Vendored nix-bitcoin now uses nixpkgs directly; no nix-bitcoin.pkgs indirection.
# Use standard bitcoind from nixpkgs (override to knots if desired via pkgs.bitcoind-knots)
services.bitcoind.package = lib.mkForce pkgs.bitcoind;
services.bitcoind.package = lib.mkForce config.nix-bitcoin.pkgs.bitcoind;
}
@@ -1,70 +0,0 @@
# RPC calls that are safe for public use
# Vendored from nix-bitcoin - do not fetch from upstream at runtime
[
"echo"
"getinfo"
"getindexinfo"
"help"
"ping"
"uptime"
# Blockchain
"getbestblockhash"
"getblock"
"getblockchaininfo"
"getblockcount"
"getblockfilter"
"getblockfrompeer"
"getblockhash"
"getblockheader"
"getblockstats"
"getchaintips"
"getchaintxstats"
"getdeploymentinfo"
"getdifficulty"
"getmempoolancestors"
"getmempooldescendants"
"getmempoolentry"
"getmempoolinfo"
"getrawmempool"
"gettxout"
"gettxoutproof"
"gettxoutsetinfo"
"scantxoutset"
"verifytxoutproof"
# Mining
"getblocktemplate"
"getmininginfo"
"getnetworkhashps"
# Network
"getnetworkinfo"
"getnodeaddresses"
"getpeerinfo"
# Rawtransactions
"analyzepsbt"
"combinepsbt"
"combinerawtransaction"
"converttopsbt"
"createpsbt"
"createrawtransaction"
"decodepsbt"
"decoderawtransaction"
"decodescript"
"finalizepsbt"
"fundrawtransaction"
"getrawtransaction"
"joinpsbts"
"sendrawtransaction"
"signrawtransactionwithkey"
"testmempoolaccept"
"utxoupdatepsbt"
# Util
"createmultisig"
"deriveaddresses"
"estimatesmartfee"
"getdescriptorinfo"
"signmessagewithprivkey"
"validateaddress"
"verifymessage"
# Zmq
"getzmqnotifications"
]
-506
View File
@@ -1,506 +0,0 @@
{ config, pkgs, lib, ... }:
with lib;
let
options = {
services.bitcoind = {
enable = mkEnableOption "Bitcoin daemon";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for peer connections.";
};
port = mkOption {
type = types.port;
default = if !cfg.regtest then 8333 else 18444;
defaultText = "if !cfg.regtest then 8333 else 18444";
description = "Port to listen for peer connections.";
};
onionPort = mkOption {
type = types.nullOr types.port;
# When the bitcoind onion service is enabled, add an onion-tagged socket
# to distinguish local connections from Tor connections
default = if (config.nix-bitcoin.onionServices.bitcoind.enable or false) then 8334 else null;
description = ''
Port to listen for Tor peer connections.
If set, inbound connections to this port are tagged as onion peers.
'';
};
listen = mkOption {
type = types.bool;
default = false;
description = ''
Listen for peer connections at `address:port`
and `address:onionPort` (if {option}`onionPort` is set).
'';
};
listenWhitelisted = mkOption {
type = types.bool;
default = false;
description = ''
Listen for peer connections at `address:whitelistedPort`.
Peers connected through this socket are automatically whitelisted.
'';
};
whitelistedPort = mkOption {
type = types.port;
default = 8335;
description = "See `listenWhitelisted`.";
};
getPublicAddressCmd = mkOption {
type = types.str;
default = "";
description = ''
Bash expression which outputs the public service address to announce to peers.
If left empty, no address is announced.
'';
};
package = mkOption {
type = types.package;
default = pkgs.bitcoind;
defaultText = "pkgs.bitcoind";
description = ''
The package providing bitcoind binaries.
You can use this option to select other bitcoind-compatible implementations.
Example:
```nix
services.bitcoind.package = pkgs.bitcoind-knots;
```
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = ''
par=16
logips=1
'';
description = "Extra lines appended to {file}`bitcoin.conf`.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/bitcoind";
description = "The data directory for bitcoind.";
};
rpc = {
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Address to listen for JSON-RPC connections.
'';
};
port = mkOption {
type = types.port;
default = if !cfg.regtest then 8332 else 18443;
defaultText = "if !cfg.regtest then 8332 else 18443";
description = "Port to listen for JSON-RPC connections.";
};
threads = mkOption {
type = types.nullOr types.ints.u16;
default = null;
description = "The number of threads to service RPC calls.";
};
allowip = mkOption {
type = types.listOf types.str;
default = [ "127.0.0.1" ];
description = ''
Allow JSON-RPC connections from specified sources.
'';
};
users = mkOption {
default = {};
description = ''
Allowed users for JSON-RPC connections.
'';
example = {
alice = {
passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
rpcwhitelist = [ "sendtoaddress" "getnewaddress" ];
};
};
type = with types; attrsOf (submodule ({ name, ... }: {
options = {
name = mkOption {
type = types.str;
default = name;
example = "alice";
description = ''
Username for JSON-RPC connections.
'';
};
passwordHMAC = mkOption {
type = types.str;
example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
description = ''
Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the
format `<SALT-HEX>$<HMAC-HEX>`.
'';
};
passwordHMACFromFile = mkOption {
type = lib.types.bool;
internal = true;
default = false;
};
rpcwhitelist = mkOption {
type = types.listOf types.str;
default = [];
description = ''
List of allowed rpc calls for each user.
If empty list, rpcwhitelist is disabled for that user.
'';
};
};
}));
};
};
regtest = mkOption {
type = types.bool;
default = false;
description = "Enable regtest mode.";
};
network = mkOption {
readOnly = true;
default = if cfg.regtest then "regtest" else "mainnet";
};
makeNetworkName = mkOption {
readOnly = true;
default = mainnet: regtest: if cfg.regtest then regtest else mainnet;
};
proxy = mkOption {
type = types.nullOr types.str;
default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null;
description = "Connect through SOCKS5 proxy";
};
i2p = mkOption {
type = types.enum [ false true "only-outgoing" ];
default = false;
description = ''
Enable peer connections via i2p.
With `only-outgoing`, incoming i2p connections are disabled.
'';
};
dataDirReadableByGroup = mkOption {
type = types.bool;
default = false;
description = ''
If enabled, data dir content is readable by the bitcoind service group.
Warning: This disables bitcoind's wallet support.
'';
};
sysperms = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Create new files with system default permissions, instead of umask 077
(only effective with disabled wallet functionality)
'';
};
disablewallet = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Do not load the wallet and disable wallet RPC calls
'';
};
dbCache = mkOption {
type = types.nullOr (intAtLeast 4);
default = null;
example = 4000;
description = "Override the default database cache size in MiB.";
};
prune = mkOption {
type = types.ints.unsigned;
default = 0;
example = 10000;
description = ''
Automatically prune block files to stay under the specified target size in MiB.
Value 0 disables pruning.
'';
};
txindex = mkOption {
type = types.bool;
default = false;
description = "Enable the transaction index.";
};
zmqpubrawblock = mkOption {
type = types.nullOr types.str;
default = null;
example = "tcp://127.0.0.1:28332";
description = "ZMQ address for zmqpubrawblock notifications";
};
zmqpubrawtx = mkOption {
type = types.nullOr types.str;
default = null;
example = "tcp://127.0.0.1:28333";
description = "ZMQ address for zmqpubrawtx notifications";
};
assumevalid = mkOption {
type = types.nullOr types.str;
default = null;
example = "00000000000000000000e5abc3a74fe27dc0ead9c70ea1deb456f11c15fd7bc6";
description = ''
If this block is in the chain assume that it and its ancestors are
valid and potentially skip their script verification.
'';
};
addnodes = mkOption {
type = types.listOf types.str;
default = [];
example = [ "ecoc5q34tmbq54wl.onion" ];
description = "Add nodes to connect to and attempt to keep the connections open";
};
discover = mkOption {
type = types.nullOr types.bool;
default = null;
description = "Discover own IP addresses";
};
addresstype = mkOption {
type = types.nullOr types.str;
default = null;
example = "bech32";
description = "The type of addresses to use";
};
user = mkOption {
type = types.str;
default = "bitcoin";
description = "The user as which to run bitcoind.";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = "The group as which to run bitcoind.";
};
cli = mkOption {
readOnly = true;
type = types.package;
default = pkgs.writers.writeBashBin "bitcoin-cli" ''
exec ${cfg.package}/bin/bitcoin-cli -datadir='${cfg.dataDir}' "$@"
'';
defaultText = "(See source)";
description = "Binary to connect with the bitcoind instance.";
};
tor = nbLib.tor;
};
};
cfg = config.services.bitcoind;
nbLib = config.nix-bitcoin.lib;
secretsDir = config.nix-bitcoin.secretsDir;
i2pSAM = config.services.i2pd.proto.sam;
configFile = builtins.toFile "bitcoin.conf" ''
# We're already logging via journald
nodebuglogfile=1
logtimestamps=0
startupnotify=/run/current-system/systemd/bin/systemd-notify --ready
${optionalString cfg.regtest ''
regtest=1
[regtest]
''}
${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"}
prune=${toString cfg.prune}
${optionalString cfg.txindex "txindex=1"}
${optionalString (cfg.sysperms != null) "sysperms=${if cfg.sysperms then "1" else "0"}"}
${optionalString (cfg.disablewallet != null) "disablewallet=${if cfg.disablewallet then "1" else "0"}"}
${optionalString (cfg.assumevalid != null) "assumevalid=${cfg.assumevalid}"}
# Connection options
listen=${if (cfg.listen || cfg.listenWhitelisted) then "1" else "0"}
${optionalString cfg.listen
"bind=${cfg.address}:${toString cfg.port}"}
${optionalString (cfg.listen && cfg.onionPort != null)
"bind=${cfg.address}:${toString cfg.onionPort}=onion"}
${optionalString cfg.listenWhitelisted
"whitebind=${cfg.address}:${toString cfg.whitelistedPort}"}
${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"}
${optionalString (cfg.i2p != false) "i2psam=${nbLib.addressWithPort i2pSAM.address i2pSAM.port}"}
${optionalString (cfg.i2p == "only-outgoing") "i2pacceptincoming=0"}
${optionalString (cfg.discover != null) "discover=${if cfg.discover then "1" else "0"}"}
${lib.concatMapStrings (node: "addnode=${node}\n") cfg.addnodes}
# RPC server options
rpcbind=${cfg.rpc.address}
rpcport=${toString cfg.rpc.port}
rpcconnect=${cfg.rpc.address}
${optionalString (cfg.rpc.threads != null) "rpcthreads=${toString cfg.rpc.threads}"}
rpcwhitelistdefault=0
${concatMapStrings (user: ''
${optionalString (!user.passwordHMACFromFile) "rpcauth=${user.name}:${user.passwordHMAC}"}
${optionalString (user.rpcwhitelist != [])
"rpcwhitelist=${user.name}:${lib.strings.concatStringsSep "," user.rpcwhitelist}"}
'') (builtins.attrValues cfg.rpc.users)
}
${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpc.allowip}
# Wallet options
${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"}
# ZMQ options
${optionalString (cfg.zmqpubrawblock != null) "zmqpubrawblock=${cfg.zmqpubrawblock}"}
${optionalString (cfg.zmqpubrawtx != null) "zmqpubrawtx=${cfg.zmqpubrawtx}"}
# Extra options
${cfg.extraConfig}
'';
zmqServerEnabled = (cfg.zmqpubrawblock != null) || (cfg.zmqpubrawtx != null);
intAtLeast = n: types.addCheck types.int (x: x >= n) // {
name = "intAtLeast";
description = "integer >= ${toString n}";
};
in {
inherit options;
config = mkIf cfg.enable {
environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ];
services.bitcoind = mkMerge [
(mkIf cfg.dataDirReadableByGroup {
disablewallet = true;
sysperms = true;
})
{
rpc.users.privileged = {
passwordHMACFromFile = true;
};
rpc.users.public = {
passwordHMACFromFile = true;
rpcwhitelist = import ./bitcoind-rpc-public-whitelist.nix;
};
}
];
services.i2pd = mkIf (cfg.i2p != false) {
enable = true;
proto.sam.enable = true;
};
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
];
systemd.services.bitcoind = rec {
wants = [
"network-online.target"
# Use `wants` instead of `requires` for `nix-bitcoin-secrets.target`
# so that bitcoind and all dependent services are not restarted when
# the secrets target restarts.
# The secrets target always restarts when deploying with one of the methods
# in ./deployment.
#
# TODO-EXTERNAL: Instead of `wants`, use a future systemd dependency type
# that propagates initial start failures but no restarts
"nix-bitcoin-secrets.target"
];
after = wants;
wantedBy = [ "multi-user.target" ];
preStart = let
extraRpcauth = concatMapStrings (name: let
user = cfg.rpc.users.${name};
in optionalString user.passwordHMACFromFile ''
hmacPayload="$(readValidatedRpcHmac '${secretsDir}/bitcoin-HMAC-${name}')" || exit 1
printf '%s\n' "rpcauth=${user.name}:$hmacPayload"
''
) (builtins.attrNames cfg.rpc.users);
in ''
${optionalString cfg.dataDirReadableByGroup ''
if [[ -e '${cfg.dataDir}/blocks' ]]; then
chmod -R g+rX '${cfg.dataDir}/blocks'
fi
''}
readValidatedRpcHmac() {
local hmacFile="$1"
local hmacPayload
if [[ ! -e "$hmacFile" ]]; then
echo "Error: Bitcoin RPC HMAC file is missing: $hmacFile" >&2
return 1
fi
if [[ ! -r "$hmacFile" ]]; then
echo "Error: Bitcoin RPC HMAC file is unreadable: $hmacFile" >&2
return 1
fi
hmacPayload="$(<"$hmacFile")"
if [[ -z "$hmacPayload" ]]; then
echo "Error: Bitcoin RPC HMAC file is empty: $hmacFile" >&2
return 1
fi
if [[ ! "$hmacPayload" =~ ^[[:xdigit:]]+\$[[:xdigit:]]+$ ]]; then
echo "Error: Bitcoin RPC HMAC file has invalid format: $hmacFile" >&2
return 1
fi
printf '%s\n' "$hmacPayload"
}
cfg=$(
cat ${configFile}
${extraRpcauth}
echo
${optionalString (cfg.getPublicAddressCmd != "") ''
echo "externalip=$(${cfg.getPublicAddressCmd})"
''}
)
confFile='${cfg.dataDir}/bitcoin.conf'
if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then
install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile
fi
'';
# Enable RPC access for group
postStart = ''
chmod g=r '${cfg.dataDir}/${optionalString cfg.regtest "regtest/"}.cookie'
'' + (optionalString cfg.regtest) ''
chmod g=x '${cfg.dataDir}/regtest'
'';
serviceConfig = nbLib.defaultHardening // {
Type = "notify";
NotifyAccess = "all";
User = cfg.user;
Group = cfg.group;
TimeoutStartSec = "30min";
TimeoutStopSec = "30min";
ExecStart = "${cfg.package}/bin/bitcoind -datadir='${cfg.dataDir}'";
Restart = "on-failure";
UMask = mkIf cfg.dataDirReadableByGroup "0027";
ReadWritePaths = [ cfg.dataDir ];
} // nbLib.allowedIPAddresses cfg.tor.enforce
// optionalAttrs zmqServerEnabled nbLib.allowNetlink;
};
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
};
users.groups.${cfg.group} = {};
users.groups.bitcoinrpc-public = {};
nix-bitcoin.operator.groups = [ cfg.group ];
nix-bitcoin.secrets = {
bitcoin-rpcpassword-privileged.user = cfg.user;
bitcoin-rpcpassword-public = {
user = cfg.user;
group = "bitcoinrpc-public";
};
bitcoin-HMAC-privileged.user = cfg.user;
bitcoin-HMAC-public.user = cfg.user;
};
nix-bitcoin.generateSecretsCmds.bitcoind = ''
makeBitcoinRPCPassword privileged
makeBitcoinRPCPassword public
'';
};
}
-334
View File
@@ -1,334 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
options.services = {
nbxplorer = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable nbxplorer, a lightweight API for Bitcoin HD wallets.
Access API documentation here:
{option}`services.nbxplorer.address`:{option}`services.nbxplorer.port`
'';
};
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen on.";
};
port = mkOption {
type = types.port;
default = 24444;
description = "Port to listen on.";
};
package = mkOption {
type = types.package;
default = pkgs.stable.nbxplorer;
defaultText = "pkgs.stable.nbxplorer";
description = "The package providing nbxplorer binaries.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/nbxplorer";
description = "The data directory for nbxplorer.";
};
# TODO-EXTERNAL:
# The shortcut link `main` in the datadir has changed to a directory
# in version 2.3.3.
# Add a dummy symlink, if it does not already exist, to be compatible with older modules.
# When the old system uses a link and the new system a directory, switching fails with:
# mv: cannot move '/var/lib/nbxplorer/Main' to '/var/lib/nbxplorer/.Main.tmp':
# No such file or directory
#
# Remove this option when it is irrelevant (i.e. when the old system will never
# be nix-bitcoin <=0.0.91)
addNetworkSymlink = mkOption {
readOnly = true;
default = pkgs.stable.nbxplorer != cfg.nbxplorer.package;
description = ''
Whether to add a compatibility symlink (like `${cfg.nbxplorer.dataDir}/Main`)
to the dataDir.
This is enabled by default if the nbxplorer package is set to the version-locked package.
'';
};
user = mkOption {
type = types.str;
default = "nbxplorer";
description = "The user as which to run NBXplorer.";
};
group = mkOption {
type = types.str;
default = cfg.nbxplorer.user;
description = "The group as which to run NBXplorer.";
};
tor = nbLib.tor;
};
btcpayserver = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable BTCPay Server, a self-hosted, open-source payment processor.
Extra recommendations:
- Enable `services.btcpayserver.lightningBackend` to provide Lightning payment support.
- Secure this service if the instance is publically accessible. For example, set
{option}`services.btcpayserver.address` to `127.0.0.1` and use a reverse proxy
that enforces TLS (Transport Layer Security).
'';
};
package = mkOption {
type = types.package;
default = pkgs.stable.btcpayserver;
defaultText = "pkgs.stable.btcpayserver";
description = "The package providing BTCPay Server binaries.";
};
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen on.";
};
port = mkOption {
type = types.port;
default = 23000;
description = "Port to listen on.";
};
lightningBackend = mkOption {
type = types.nullOr (types.enum [ "lnd" ]);
default = null;
description = ''
The lightning node to use as a backend.
Enables the node service if not already enabled.
'';
};
lbtc = mkOption {
readOnly = true;
default = false;
description = ''
Enable Liquid support.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/btcpayserver";
description = "The data directory for BTCPay Server.";
};
user = mkOption {
type = types.str;
default = "btcpayserver";
description = "The user as which to run BTCPay Server.";
};
group = mkOption {
type = types.str;
default = cfg.btcpayserver.user;
description = "The group as which to run BTCPay Server.";
};
tor = nbLib.tor;
};
};
cfg = {
inherit (config.services)
nbxplorer
btcpayserver
bitcoind;
};
nbLib = config.nix-bitcoin.lib;
secretsDir = config.nix-bitcoin.secretsDir;
in {
inherit options;
config = mkMerge [
(mkIf cfg.nbxplorer.enable {
systemd.tmpfiles.rules = [
"d '${cfg.nbxplorer.dataDir}' 0770 ${cfg.nbxplorer.user} ${cfg.nbxplorer.group} - -"
] ++ optional cfg.nbxplorer.addNetworkSymlink
"L+ '${cfg.nbxplorer.dataDir}/Main' - - - - '${cfg.nbxplorer.dataDir}/main'";
systemd.services.nbxplorer = let
configFile = builtins.toFile "nbxplorer-config" ''
network=${cfg.bitcoind.network}
btcrpcuser=${cfg.bitcoind.rpc.users.btcpayserver.name}
btcrpcurl=http://${nbLib.addressWithPort cfg.bitcoind.rpc.address cfg.bitcoind.rpc.port}
btcnodeendpoint=${nbLib.addressWithPort cfg.bitcoind.address cfg.bitcoind.whitelistedPort}
bind=${cfg.nbxplorer.address}
port=${toString cfg.nbxplorer.port}
postgres=User ID=${cfg.nbxplorer.user};Host=/run/postgresql;Database=nbxplorer
'';
in rec {
wantedBy = [ "multi-user.target" ];
requires = [ "postgresql.target" ];
wants = [ "bitcoind.service" ];
after = requires ++ wants ++ [ "nix-bitcoin-secrets.target" ];
preStart = ''
install -m 600 ${configFile} '${cfg.nbxplorer.dataDir}/settings.config'
printf '%s\n' "btcrpcpassword=$(<${secretsDir}/bitcoin-rpcpassword-btcpayserver)" \
>> '${cfg.nbxplorer.dataDir}/settings.config'
'';
serviceConfig = nbLib.defaultHardening // {
ExecStart = ''
${cfg.nbxplorer.package}/bin/nbxplorer --conf=${cfg.nbxplorer.dataDir}/settings.config \
--datadir='${cfg.nbxplorer.dataDir}'
'';
RuntimeDirectory = "nbxplorer";
StateDirectory = "nbxplorer";
User = cfg.nbxplorer.user;
Group = cfg.nbxplorer.group;
Restart = "on-failure";
RestartSec = "10s";
ReadWritePaths = [ cfg.nbxplorer.dataDir ];
MemoryDenyWriteExecute = false;
} // nbLib.allowedIPAddresses cfg.nbxplorer.tor.enforce;
};
services.bitcoind = {
enable = true;
listenWhitelisted = true;
txindex = true;
};
users.users.${cfg.nbxplorer.user} = {
isSystemUser = true;
group = cfg.nbxplorer.group;
home = cfg.nbxplorer.dataDir;
};
users.groups.${cfg.nbxplorer.group} = {};
})
(mkIf cfg.btcpayserver.enable {
services.nbxplorer.enable = true;
services.bitcoind = {
listenWhitelisted = true;
rpc.users.btcpayserver = {
name = "btcpayserver";
passwordHMACFromFile = true;
rpcwhitelist = [
"getblockchaininfo"
"getblock"
"getblockhash"
"getblockheader"
"getblockstats"
"gettransaction"
"getrawtransaction"
"sendrawtransaction"
"getblockcount"
"getbestblockhash"
"getnetworkinfo"
"getpeerinfo"
"estimatesmartfee"
"getmempoolinfo"
"getmempoolentry"
"getrawmempool"
"gettxout"
"scantxoutset"
"importmulti"
"listunspent"
"getwalletinfo"
"listtransactions"
"listreceivedbyaddress"
"getnewaddress"
"uptime"
"getrpcinfo"
];
};
};
systemd.tmpfiles.rules = [
"d '${cfg.btcpayserver.dataDir}' 0770 ${cfg.btcpayserver.user} ${cfg.btcpayserver.group} - -"
];
systemd.services.btcpayserver = let
nbExplorerUrl = "http://${nbLib.addressWithPort cfg.nbxplorer.address cfg.nbxplorer.port}/";
nbExplorerCookie =
"${cfg.nbxplorer.dataDir}/${cfg.bitcoind.makeNetworkName "Main" "RegTest"}/.cookie";
configFile = builtins.toFile "btcpayserver-config" (
''
network=${cfg.bitcoind.network}
bind=${cfg.btcpayserver.address}
port=${toString cfg.btcpayserver.port}
socksendpoint=${config.nix-bitcoin.torClientAddressWithPort}
btcexplorerurl=${nbExplorerUrl}
btcexplorercookiefile=${nbExplorerCookie}
explorer.postgres=User ID=${cfg.nbxplorer.user};Host=/run/postgresql;Database=nbxplorer
postgres=User ID=${cfg.btcpayserver.user};Host=/run/postgresql;Database=btcpayserver
'' + optionalString (cfg.btcpayserver.lightningBackend == "lnd")
(
"btclightning=type=lnd-rest;"
+ "server=https://${nbLib.address config.services.lnd.restAddress}:${toString config.services.lnd.restPort}/;"
+ "macaroonfilepath=/run/lnd/btcpayserver.macaroon;"
+ "certfilepath=${config.services.lnd.certPath}\n"
)
);
in rec {
wantedBy = [ "multi-user.target" ];
requires = [ "postgresql.target" "nbxplorer.service" ];
wants = optional (cfg.btcpayserver.lightningBackend == "lnd") "lnd.service";
after = requires ++ wants;
serviceConfig = nbLib.defaultHardening // {
ExecStart = ''
${cfg.btcpayserver.package}/bin/btcpayserver --conf=${configFile} \
--datadir='${cfg.btcpayserver.dataDir}'
'';
WorkingDirectory = "${cfg.btcpayserver.package}/lib/btcpayserver";
RuntimeDirectory = "btcpayserver";
StateDirectory = "btcpayserver";
User = cfg.btcpayserver.user;
Group = cfg.btcpayserver.group;
Restart = "on-failure";
RestartSec = "10s";
ReadWritePaths = [ cfg.btcpayserver.dataDir ];
MemoryDenyWriteExecute = false;
} // nbLib.allowedIPAddresses cfg.btcpayserver.tor.enforce;
};
services.postgresql = {
enable = true;
ensureDatabases = [ "btcpayserver" "nbxplorer" ];
ensureUsers = [
{ name = cfg.btcpayserver.user; ensureDBOwnership = true; }
{ name = cfg.nbxplorer.user; ensureDBOwnership = true; }
];
};
users.users.${cfg.btcpayserver.user} = {
isSystemUser = true;
group = cfg.btcpayserver.group;
home = cfg.btcpayserver.dataDir;
extraGroups = optional (cfg.btcpayserver.lightningBackend == "lnd") config.services.lnd.group;
};
users.groups.${cfg.btcpayserver.group} = {};
nix-bitcoin.secrets = {
bitcoin-rpcpassword-btcpayserver = {
user = cfg.bitcoind.user;
group = cfg.nbxplorer.group;
};
bitcoin-HMAC-btcpayserver.user = cfg.bitcoind.user;
};
nix-bitcoin.generateSecretsCmds.btcpayserver = ''
makeBitcoinRPCPassword btcpayserver
'';
})
(mkIf (cfg.btcpayserver.enable && cfg.btcpayserver.lightningBackend == "lnd") {
services.lnd = {
enable = true;
macaroons.btcpayserver = {
user = cfg.btcpayserver.user;
permissions = ''
{"entity":"address","action":"write"},{"entity":"info","action":"read"},{"entity":"invoices","action":"read"},{"entity":"invoices","action":"write"},{"entity":"offchain","action":"read"},{"entity":"offchain","action":"write"},{"entity":"onchain","action":"read"},{"entity":"onchain","action":"write"},{"entity":"peers","action":"read"},{"entity":"peers","action":"write"}
'';
};
};
users.users.${config.services.lnd.user}.extraGroups = [ cfg.btcpayserver.group ];
})
];
}
-15
View File
@@ -1,15 +0,0 @@
# Common Bitcoin infrastructure: secrets, onion services, nodeinfo, security
# Extracted from nix-bitcoin, tailored for Sovran (lnd-only)
{ config, lib, pkgs, ... }:
{
imports = [
./nix-bitcoin.nix
./secrets/secrets.nix
./operator.nix
./security.nix
./onion-addresses.nix
./onion-services.nix
./nodeinfo.nix
./versioning.nix
];
}
-16
View File
@@ -1,16 +0,0 @@
# Sovran Bitcoin stack - tailored from nix-bitcoin, lnd-only, nixpkgs packages
# Original: https://github.com/fort-nix/nix-bitcoin
{
imports = [
./common.nix
./bitcoind.nix
./electrs.nix
./lnd.nix
./lndconnect.nix
./rtl.nix
./btcpayserver.nix
./mempool.nix
];
disabledModules = [ "services/networking/bitcoind.nix" ];
}
-106
View File
@@ -1,106 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
options.services.electrs = {
enable = mkEnableOption "electrs, an Electrum server implemented in Rust";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for RPC connections.";
};
port = mkOption {
type = types.port;
default = 50001;
description = "Port to listen for RPC connections.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/electrs";
description = "The data directory for electrs.";
};
monitoringPort = mkOption {
type = types.port;
default = 4224;
description = "Prometheus monitoring port.";
};
extraArgs = mkOption {
type = types.separatedString " ";
default = "";
description = "Extra command line arguments passed to electrs.";
};
user = mkOption {
type = types.str;
default = "electrs";
description = "The user as which to run electrs.";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = "The group as which to run electrs.";
};
tor.enforce = nbLib.tor.enforce;
};
cfg = config.services.electrs;
nbLib = config.nix-bitcoin.lib;
secretsDir = config.nix-bitcoin.secretsDir;
bitcoind = config.services.bitcoind;
in {
inherit options;
config = mkIf cfg.enable {
assertions = [
{ assertion = bitcoind.prune == 0;
message = "electrs does not support bitcoind pruning.";
}
];
services.bitcoind = {
enable = true;
listenWhitelisted = true;
};
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
];
systemd.services.electrs = {
wantedBy = [ "multi-user.target" ];
requires = [ "bitcoind.service" ];
after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ];
preStart = ''
echo "auth = \"${bitcoind.rpc.users.public.name}:$(cat ${secretsDir}/bitcoin-rpcpassword-public)\"" \
> electrs.toml
'';
serviceConfig = nbLib.defaultHardening // {
# electrs only uses the working directory for reading electrs.toml
WorkingDirectory = cfg.dataDir;
ExecStart = ''
${pkgs.electrs}/bin/electrs \
--log-filters=INFO \
--network=${bitcoind.makeNetworkName "bitcoin" "regtest"} \
--db-dir='${cfg.dataDir}' \
--daemon-dir='${bitcoind.dataDir}' \
--electrum-rpc-addr=${cfg.address}:${toString cfg.port} \
--monitoring-addr=${cfg.address}:${toString cfg.monitoringPort} \
--daemon-rpc-addr=${nbLib.addressWithPort bitcoind.rpc.address bitcoind.rpc.port} \
--daemon-p2p-addr=${nbLib.addressWithPort bitcoind.address bitcoind.whitelistedPort} \
${cfg.extraArgs}
'';
User = cfg.user;
Group = cfg.group;
Restart = "on-failure";
RestartSec = "10s";
ReadWritePaths = [ cfg.dataDir ];
} // nbLib.allowedIPAddresses cfg.tor.enforce;
};
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
extraGroups = [ "bitcoinrpc-public" ];
};
users.groups.${cfg.group} = {};
};
}
-132
View File
@@ -1,132 +0,0 @@
lib: pkgs: config:
with lib;
# See `man systemd.exec` and `man systemd.resource-control` for an explanation
# of the systemd-related options available through this file.
let self = {
# These settings roughly follow systemd's "strict" security profile
defaultHardening = {
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
NoNewPrivileges = true;
PrivateDevices = true;
MemoryDenyWriteExecute = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectClock = true;
ProtectProc = "invisible";
ProcSubset = "pid";
ProtectControlGroups = true;
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
RestrictNamespaces = true;
LockPersonality = true;
IPAddressDeny = "any";
PrivateUsers = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
RestrictRealtime = true;
ProtectHostname = true;
CapabilityBoundingSet = "";
# @system-service whitelist and docker seccomp blacklist (except for "clone"
# which is a core requirement for systemd services)
# @system-service is defined in src/shared/seccomp-util.c (systemd source)
SystemCallFilter = [ "@system-service" "~add_key kcmp keyctl mbind move_pages name_to_handle_at personality process_vm_readv process_vm_writev request_key setns unshare userfaultfd" ];
SystemCallArchitectures = "native";
};
allowNetlink = {
RestrictAddressFamilies = self.defaultHardening.RestrictAddressFamilies + " AF_NETLINK";
};
nodejs = {
# Required for JIT compilation
MemoryDenyWriteExecute = false;
# Required by nodejs >= 18
SystemCallFilter = self.defaultHardening.SystemCallFilter ++ [ "@pkey" ];
};
# Allow takes precedence over Deny.
allowLocalIPAddresses = {
IPAddressAllow = [
"127.0.0.1/32"
"::1/128"
"169.254.0.0/16"
];
};
allowAllIPAddresses = { IPAddressAllow = "any"; };
allowTor = self.allowLocalIPAddresses;
allowedIPAddresses = onlyLocal:
if onlyLocal
then self.allowLocalIPAddresses
else self.allowAllIPAddresses;
tor = {
proxy = mkOption {
type = types.bool;
default = false;
description = "Whether to proxy outgoing connections with Tor.";
};
enforce = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enforce Tor on this service by only allowing connections
from and to localhost and link-local addresses.
'';
};
};
script = name: src: pkgs.writers.writeBash name ''
set -eo pipefail
${src}
'';
# Used for ExecStart*
rootScript = name: src: "+${self.script name src}";
cliExec = mkOption {
# Used by netns-isolation to execute the cli in the service's private netns
internal = true;
type = types.str;
default = "exec";
};
mkOnionService = map: {
map = [ map ];
version = 3;
};
# Convert a bind address, which may be a special INADDR_ANY address,
# to an actual IP address
address = addr:
if addr == "0.0.0.0" then
"127.0.0.1"
else if addr == "::" then
"::1"
else
addr;
addressWithPort = addr: port: "${self.address addr}:${toString port}";
optionalAttr = cond: name: if cond then name else null;
mkCertExtraAltNames = cert:
builtins.concatStringsSep "," (
(map (domain: "DNS:${domain}") cert.extraDomains) ++
(map (ip: "IP:${ip}") cert.extraIPs)
);
test = {
mkIfTest = test: mkIf (config.tests.${test} or false);
};
mkAlias = default: mkOption {
internal = true;
readOnly = true;
inherit default;
};
}; in self
-313
View File
@@ -1,313 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
options.services.lnd = {
enable = mkEnableOption "Lightning Network daemon, a Lightning Network implementation in Go";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for peer connections";
};
port = mkOption {
type = types.port;
default = 9735;
description = "Port to listen for peer connections";
};
rpcAddress = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for RPC connections.";
};
rpcPort = mkOption {
type = types.port;
default = 10009;
description = "Port to listen for gRPC connections.";
};
restAddress = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for REST connections.";
};
restPort = mkOption {
type = types.port;
default = 8080;
description = "Port to listen for REST connections.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/lnd";
description = "The data directory for LND.";
};
networkDir = mkOption {
readOnly = true;
default = "${cfg.dataDir}/chain/bitcoin/${bitcoind.network}";
description = "The network data directory.";
};
tor-socks = mkOption {
type = types.nullOr types.str;
default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null;
description = "Socks proxy for connecting to Tor nodes";
};
macaroons = mkOption {
default = {};
type = with types; attrsOf (submodule {
options = {
user = mkOption {
type = types.str;
description = "User who owns the macaroon.";
};
permissions = mkOption {
type = types.str;
example = ''
{"entity":"info","action":"read"},{"entity":"onchain","action":"read"}
'';
description = "List of granted macaroon permissions.";
};
};
});
description = ''
Extra macaroon definitions.
'';
};
certificate = {
extraIPs = mkOption {
type = with types; listOf str;
default = [];
example = [ "60.100.0.1" ];
description = ''
Extra `subjectAltName` IPs added to the certificate.
This works the same as lnd option {option}`tlsextraip`.
'';
};
extraDomains = mkOption {
type = with types; listOf str;
default = [];
example = [ "example.com" ];
description = ''
Extra `subjectAltName` domain names added to the certificate.
This works the same as lnd option {option}`tlsextradomain`.
'';
};
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = ''
autopilot.active=1
'';
description = ''
Extra lines appended to {file}`lnd.conf`.
See here for all available options:
https://github.com/lightningnetwork/lnd/blob/master/sample-lnd.conf
'';
};
package = mkOption {
type = types.package;
default = pkgs.lnd;
defaultText = "pkgs.lnd";
description = "The package providing lnd binaries.";
};
cli = mkOption {
default = pkgs.writers.writeBashBin "lncli"
# Switch user because lnd makes datadir contents readable by user only
''
${runAsUser} ${cfg.user} ${cfg.package}/bin/lncli \
--rpcserver ${cfg.rpcAddress}:${toString cfg.rpcPort} \
--tlscertpath '${cfg.certPath}' \
--macaroonpath '${networkDir}/admin.macaroon' "$@"
'';
defaultText = "(See source)";
description = "Binary to connect with the lnd instance.";
};
getPublicAddressCmd = mkOption {
type = types.str;
default = "";
description = ''
Bash expression which outputs the public service address to announce to peers.
If left empty, no address is announced.
'';
};
user = mkOption {
type = types.str;
default = "lnd";
description = "The user as which to run LND.";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = "The group as which to run LND.";
};
certPath = mkOption {
readOnly = true;
default = "${secretsDir}/lnd-cert";
description = "LND TLS certificate path.";
};
tor = nbLib.tor;
};
cfg = config.services.lnd;
nbLib = config.nix-bitcoin.lib;
secretsDir = config.nix-bitcoin.secretsDir;
runAsUser = config.nix-bitcoin.runAsUserCmd;
lndinit = "${(pkgs.callPackage ../../packages/lndinit {})}/bin/lndinit";
bitcoind = config.services.bitcoind;
bitcoindRpcAddress = nbLib.address bitcoind.rpc.address;
networkDir = cfg.networkDir;
configFile = pkgs.writeText "lnd.conf" ''
datadir=${cfg.dataDir}
tlscertpath=${cfg.certPath}
tlskeypath=${secretsDir}/lnd-key
# We're logging via journald
logging.file.disable=1
logging.console.no-timestamps=1
listen=${toString cfg.address}:${toString cfg.port}
rpclisten=${cfg.rpcAddress}:${toString cfg.rpcPort}
restlisten=${cfg.restAddress}:${toString cfg.restPort}
bitcoin.${bitcoind.network}=1
bitcoin.node=bitcoind
${optionalString (cfg.tor.proxy) "tor.active=true"}
${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"}
bitcoind.rpchost=${bitcoindRpcAddress}:${toString bitcoind.rpc.port}
bitcoind.rpcuser=${bitcoind.rpc.users.public.name}
bitcoind.zmqpubrawblock=${zmqHandleSpecialAddress bitcoind.zmqpubrawblock}
bitcoind.zmqpubrawtx=${zmqHandleSpecialAddress bitcoind.zmqpubrawtx}
wallet-unlock-password-file=${secretsDir}/lnd-wallet-password
${cfg.extraConfig}
'';
zmqHandleSpecialAddress = builtins.replaceStrings [ "0.0.0.0" "[::]" ] [ "127.0.0.1" "[::1]" ];
in {
inherit options;
config = mkIf cfg.enable {
assertions = [
{ assertion =
!(config.services ? clightning)
|| true; # clightning enable/port check disabled - option structure differs between nixpkgs versions (f13ff45 has plugins only, 8b8c811 removed). Sovran uses lnd only, so no conflict.
message = ''
LND and clightning can't both bind to lightning port 9735. Either
disable LND/clightning or change services.clightning.port or
services.lnd.port to a port other than 9735.
'';
}
];
services.bitcoind = {
enable = true;
# Increase rpc thread count due to reports that lightning implementations fail
# under high bitcoind rpc load
rpc.threads = 16;
zmqpubrawblock = mkDefault "tcp://${bitcoindRpcAddress}:28332";
zmqpubrawtx = mkDefault "tcp://${bitcoindRpcAddress}:28333";
};
environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ];
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
];
services.lnd.certificate.extraIPs = mkIf (cfg.rpcAddress != "127.0.0.1") [ "${cfg.rpcAddress}" ];
systemd.services.lnd = {
wantedBy = [ "multi-user.target" ];
requires = [ "bitcoind.service" ];
after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ];
preStart = ''
install -m600 ${configFile} '${cfg.dataDir}/lnd.conf'
{
echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)"
${optionalString (cfg.getPublicAddressCmd != "") ''
echo "externalip=$(${cfg.getPublicAddressCmd})"
''}
} >> '${cfg.dataDir}/lnd.conf'
if [[ ! -f ${networkDir}/wallet.db ]]; then
seed='${cfg.dataDir}/lnd-seed-mnemonic'
if [[ ! -f "$seed" ]]; then
echo "Create lnd seed"
(umask u=r,go=; ${lndinit} gen-seed > "$seed")
fi
echo "Create lnd wallet"
${lndinit} -v init-wallet \
--file.seed="$seed" \
--file.wallet-password='${secretsDir}/lnd-wallet-password' \
--init-file.output-wallet-dir='${cfg.networkDir}'
fi
'';
serviceConfig = nbLib.defaultHardening // {
Type = "notify";
RuntimeDirectory = "lnd"; # Only used to store custom macaroons
RuntimeDirectoryMode = "711";
ExecStart = "${cfg.package}/bin/lnd --configfile='${cfg.dataDir}/lnd.conf'";
User = cfg.user;
TimeoutSec = "15min";
Restart = "on-failure";
RestartSec = "10s";
ReadWritePaths = [ cfg.dataDir ];
ExecStartPost = let
curl = "${pkgs.curl}/bin/curl -fsS --cacert ${cfg.certPath}";
restUrl = "https://${nbLib.addressWithPort cfg.restAddress cfg.restPort}/v1";
# Setting macaroon permissions for other users needs root permissions
script = nbLib.rootScript "lnd-create-macaroons" ''
umask ug=r,o=
${lib.concatMapStrings (macaroon: ''
echo "Create custom macaroon ${macaroon}"
macaroonPath="$RUNTIME_DIRECTORY/${macaroon}.macaroon"
${curl} \
-H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')" \
-X POST \
-d '{"permissions":[${cfg.macaroons.${macaroon}.permissions}]}' \
${restUrl}/macaroon |\
${pkgs.jq}/bin/jq -c '.macaroon' | ${pkgs.xxd}/bin/xxd -p -r > "$macaroonPath"
chown ${cfg.macaroons.${macaroon}.user}: "$macaroonPath"
'') (attrNames cfg.macaroons)}
'';
in [
script
];
} // nbLib.allowedIPAddresses cfg.tor.enforce;
};
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
extraGroups = [ "bitcoinrpc-public" ];
home = cfg.dataDir; # lnd creates .lnd dir in HOME
};
users.groups.${cfg.group} = {};
nix-bitcoin.operator = {
groups = [ cfg.group ];
allowRunAsUsers = [ cfg.user ];
};
nix-bitcoin.secrets = {
lnd-wallet-password.user = cfg.user;
lnd-key.user = cfg.user;
lnd-cert.user = cfg.user;
lnd-cert.permissions = "444"; # world readable
};
# Advantages of manually pre-generating certs:
# - Reduces dynamic state
# - Enables deployment of a mesh of server plus client nodes with predefined certs
nix-bitcoin.generateSecretsCmds.lnd = ''
makePasswordSecret lnd-wallet-password
makeCert lnd '${nbLib.mkCertExtraAltNames cfg.certificate}'
'';
};
}
-63
View File
@@ -1,63 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.lnd;
operatorName = config.nix-bitcoin.operator.name;
nbLib = config.nix-bitcoin.lib;
mkLndconnect = { name, isClightning ? false, enableOnion, onionService, port, certPath, authSecretPath }:
let
lnd = config.services.lnd;
getOnionAddress = "cat ${config.nix-bitcoin.secretsDir}/onion-address-${onionService} 2>/dev/null || echo ${onionService}.onion";
in pkgs.writeScriptBin name ''
#!${pkgs.bash}/bin/bash
set -e
certPath="${certPath}"
authSecretPath="${authSecretPath}"
if [ "${toString enableOnion}" = "1" ]; then
host=$(cat /var/lib/tor/onion/${onionService}/hostname 2>/dev/null || echo "${onionService}.onion")
port="${toString port}"
else
host="${nbLib.address lnd.restAddress}"
port="${toString lnd.restPort}"
fi
# lndconnect is provided by pkgs.lndconnect
${getExe pkgs.lndconnect} --host="$host" --port="$port" --cert="$certPath" --macaroon="$authSecretPath" "$@"
'';
in {
options.services.lnd.lndconnect = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable lndconnect for LND";
};
onion = mkOption {
type = types.bool;
default = false;
description = "Expose lndconnect via Tor onion service";
};
};
config = mkIf cfg.enable (mkMerge [
(mkIf cfg.lndconnect.enable {
environment.systemPackages = [
(mkLndconnect {
name = "lndconnect";
enableOnion = cfg.lndconnect.onion;
onionService = "${operatorName}/lnd";
port = cfg.restPort;
certPath = cfg.certPath;
authSecretPath = "${cfg.networkDir}/admin.macaroon";
})
];
})
(mkIf (cfg.lndconnect.enable && cfg.lndconnect.onion) {
services.tor.relay.onionServices.lnd = nbLib.mkOnionService {
port = cfg.restPort;
target = { addr = nbLib.address cfg.restAddress; port = cfg.restPort; };
};
nix-bitcoin.onionAddresses.access.${operatorName} = [ "lnd" ];
})
]);
}
-348
View File
@@ -1,348 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
options.services = {
mempool = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable Mempool, a fully featured Bitcoin visualizer, explorer, and API service.
Note: Mempool enables `txindex` in bitcoind (this is a requirement).
This module has two components:
- A backend service (systemd service `mempool`)
- An optional web interface run by nginx, defined by options `services.mempool.frontend.*`.
The frontend is enabled by default when mempool is enabled.
For details, see `services.mempool.frontend.enable`.
'';
};
frontend = {
enable = mkOption {
type = types.bool;
default = cfg.enable;
description = ''
Enable the mempool frontend (web interface).
This starts a simple nginx instance, configured for local usage with
settings similar to the `mempool/frontend` Docker image.
IMPORTANT:
If you want to expose the mempool frontend to the internet, you
should create a custom nginx config that includes TLS, backend caching, rate limiting
and performance tuning.
For this task, reuse the config snippets from option `services.mempool.frontend.nginxConfig`.
See also: https://github.com/fort-nix/nixbitcoin.org/blob/master/website/mempool.nix,
which contains a mempool nginx config for public hosting (running at
https://mempool.nixbitcoin.org).
'';
};
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "HTTP server address.";
};
port = mkOption {
type = types.port;
default = 60845; # A random private port
description = "HTTP server port.";
};
settings = mkOption {
type = with types; attrsOf anything;
default = {};
example = {
TESTNET_ENABLED = true;
MEMPOOL_WEBSITE_URL = "mempool.mynode.org";
};
description = ''
Mempool frontend settings.
See here for available options:
https://github.com/mempool/mempool/blob/master/frontend/src/app/services/state.service.ts
(`interface Env` and `defaultEnv`)
'';
};
staticContentRoot = mkOption {
type = types.path;
default = (pkgs.callPackage ../../packages/mempool { fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {}; }).mempool-frontend.withConfig cfg.frontend.settings;
defaultText = "mempoolPkgs.mempool-frontend";
description = "
Path of the static frontend content root.
";
};
nginxConfig = mkOption {
readOnly = true;
default = frontend.nginxConfig;
defaultText = "(See source)";
description = "
An attrset of nginx config snippets for assembling a custom
mempool nginx config.
For details, see the source comments at the point of definition.
";
};
};
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Mempool backend address.";
};
port = mkOption {
type = types.port;
default = 8999;
description = "Mempool backend port.";
};
electrumServer = mkOption {
type = types.enum [ "electrs" ];
default = "electrs";
description = ''
The Electrum server to use for fetching address information.
Possible options:
- electrs:
Small database size, slow when querying new addresses.
'';
};
settings = mkOption {
type = with types; attrsOf (attrsOf anything);
example = {
MEMPOOL = {
POLL_RATE_MS = 3000;
STDOUT_LOG_MIN_PRIORITY = "debug";
};
PRICE_DATA_SERVER = {
CLEARNET_URL = "https://myserver.org/prices";
};
};
description = ''
Mempool backend settings.
See here for available options:
https://github.com/mempool/mempool/blob/master/backend/src/config.ts
'';
};
database = {
name = mkOption {
type = types.str;
default = "mempool";
description = "Database name.";
};
};
package = mkOption {
type = types.package;
default = (pkgs.callPackage ../../packages/mempool { fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {}; }).mempool-backend;
defaultText = "mempoolPkgs.mempool-backend";
description = "The package providing mempool binaries.";
};
user = mkOption {
type = types.str;
default = "mempool";
description = "The user as which to run Mempool.";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = "The group as which to run Mempool.";
};
tor = nbLib.tor;
};
# Internal read-only options used by `./nodeinfo.nix` and `./onion-services.nix`
mempool-frontend = let
inherit (nbLib) mkAlias;
in {
enable = mkAlias cfg.frontend.enable;
address = mkAlias cfg.frontend.address;
port = mkAlias cfg.frontend.port;
};
};
cfg = config.services.mempool;
nbLib = config.nix-bitcoin.lib;
nbPkgs = pkgs; # vendored: now alias to pkgs
secretsDir = config.nix-bitcoin.secretsDir;
configFile = builtins.toFile "mempool-config" (builtins.toJSON cfg.settings);
cacheDir = "/var/cache/mempool";
inherit (config.services)
bitcoind
electrs;
torSocket = config.services.tor.client.socksListenAddress;
# Vendored mempool package
fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {};
mempoolPkgs = pkgs.callPackage ../../packages/mempool { inherit fetchNodeModules; };
# See the `services.nginx` definition further below
# on how to use these snippets.
frontend.nginxConfig = {
# This must be added to `services.nginx.commonHttpConfig` when
# `mempool/location-static.conf` is used
httpConfig = ''
include ${mempoolPkgs.mempool-nginx-conf}/http-language.conf;
'';
# Config for static website content.
# This should be added to `services.nginx.virtualHosts.<mempool server name>.extraConfig`.
# Adapted from mempool/nginx-mempool.conf and mempool/production/nginx/location-redirects.conf
staticContent = ''
index index.html;
add_header Cache-Control "public, no-transform";
add_header Vary Accept-Language;
add_header Vary Cookie;
include ${mempoolPkgs.mempool-nginx-conf}/location-static.conf;
# Redirect /api to /docs/api
location = /api {
return 308 https://$host/docs/api;
}
location = /api/ {
return 308 https://$host/docs/api;
}
'';
# Config for backend API.
# This should be added to `services.nginx.virtualHosts.<mempool server name>.extraConfig`.
# Adapted from mempool/nginx-mempool.conf and mempool/production/nginx/location-api.conf.
proxyApi = let
backend = "http://${nbLib.addressWithPort cfg.address cfg.port}";
in ''
location /api/ {
proxy_pass ${backend}/api/v1/;
}
location /api/v1 {
proxy_pass ${backend};
}
# Websocket API
location /api/v1/ws {
proxy_pass ${backend};
# Websocket header settings
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
# Relevant settings from `recommendedProxyConfig` (nixos/nginx/default.nix)
# (In the above api locations, these are inherited from the parent scope)
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
'';
};
in {
inherit options;
config = mkIf cfg.enable {
services.bitcoind.txindex = true;
services.electrs.enable = true;
services.mysql = {
enable = true;
package = pkgs.mariadb;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{
name = cfg.user;
ensurePermissions."${cfg.database.name}.*" = "ALL PRIVILEGES";
}
];
};
# Available options:
# https://github.com/mempool/mempool/blob/master/backend/src/config.ts
services.mempool.settings = {
MEMPOOL = {
# mempool doesn't support regtest
NETWORK = "mainnet";
BACKEND = "electrum";
HTTP_PORT = cfg.port;
CACHE_DIR = "${cacheDir}/cache";
STDOUT_LOG_MIN_PRIORITY = mkDefault "info";
AUTOMATIC_POOLS_UPDATE = true;
};
CORE_RPC = {
HOST = bitcoind.rpc.address;
PORT = bitcoind.rpc.port;
USERNAME = bitcoind.rpc.users.public.name;
PASSWORD = "@btcRpcPassword@";
};
ELECTRUM = let
server = config.services.${cfg.electrumServer};
in {
HOST = server.address;
PORT = server.port;
TLS_ENABLED = false;
};
DATABASE = {
ENABLED = true;
DATABASE = cfg.database.name;
SOCKET = "/run/mysqld/mysqld.sock";
PID_DIR = cacheDir;
};
} // optionalAttrs (cfg.tor.proxy) {
# Use Tor for rate fetching and pool updating
SOCKS5PROXY = {
ENABLED = true;
USE_ONION = true;
HOST = torSocket.addr;
PORT = torSocket.port;
};
};
systemd.services.mempool = rec {
wantedBy = [ "multi-user.target" ];
requires = [ "mysql.service" ];
wants = [ "${cfg.electrumServer}.service" ];
after = requires ++ wants;
preStart = ''
mkdir -p '${cacheDir}/cache'
<${configFile} sed \
-e "s|@btcRpcPassword@|$(cat ${secretsDir}/bitcoin-rpcpassword-public)|" \
> '${cacheDir}/config.json'
'';
environment.MEMPOOL_CONFIG_FILE = "${cacheDir}/config.json";
serviceConfig = nbLib.defaultHardening // {
ExecStart = "${cfg.package}/bin/mempool-backend";
CacheDirectory = "mempool";
CacheDirectoryMode = "770";
# Show "mempool" instead of "node" in the journal
SyslogIdentifier = "mempool";
User = cfg.user;
Restart = "on-failure";
RestartSec = "10s";
} // nbLib.allowedIPAddresses cfg.tor.enforce
// nbLib.nodejs;
};
services.nginx = mkIf cfg.frontend.enable {
enable = true;
enableReload = true;
recommendedBrotliSettings = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
commonHttpConfig = frontend.nginxConfig.httpConfig;
virtualHosts."mempool" = {
serverName = "_";
listen = [ { addr = cfg.frontend.address; port = cfg.frontend.port; } ];
root = cfg.frontend.staticContentRoot;
extraConfig =
frontend.nginxConfig.staticContent +
frontend.nginxConfig.proxyApi;
};
};
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
extraGroups = [ "bitcoinrpc-public" ];
};
users.groups.${cfg.group} = {};
};
}
-60
View File
@@ -1,60 +0,0 @@
{ config, pkgs, lib, ... }:
with lib;
{
options = {
nix-bitcoin = {
# Kept for compatibility, now simply aliases system pkgs
pkgs = mkOption {
type = types.attrs;
default = pkgs;
defaultText = "pkgs";
description = "Alias to system pkgs (vendored nix-bitcoin now uses nixpkgs directly).";
};
useVersionLockedPkgs = mkOption {
type = types.bool;
default = false;
description = "Deprecated — vendored modules always use system pkgs.";
};
pkgOverlays = mkOption {
internal = true;
type = with types; functionTo attrs;
default = _: _: {};
description = "Deprecated stub.";
};
lib = mkOption {
readOnly = true;
default = import ./lib.nix lib pkgs config;
defaultText = "vendor/nix-bitcoin/lib.nix";
};
torClientAddressWithPort = mkOption {
readOnly = true;
default = with config.services.tor.client.socksListenAddress;
"${addr}:${toString port}";
defaultText = "(See source)";
};
torify = mkOption {
readOnly = true;
default = pkgs.writers.writeBashBin "torify" ''
${pkgs.tor}/bin/torify \
--address ${config.services.tor.client.socksListenAddress.addr} \
"$@"
'';
defaultText = "(See source)";
};
runAsUserCmd = mkOption {
readOnly = true;
default = if config.security.doas.enable
then "doas -u"
else "sudo -u";
defaultText = "(See source)";
};
};
};
}
-154
View File
@@ -1,154 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
options = {
nix-bitcoin.nodeinfo = {
enable = mkEnableOption "nodeinfo";
program = mkOption {
readOnly = true;
default = script;
defaultText = "(See source)";
};
services = mkOption {
internal = true;
type = types.attrs;
default = {};
defaultText = "(See source)";
description = ''
Nodeinfo service definitions.
'';
};
lib = mkOption {
internal = true;
readOnly = true;
default = nodeinfoLib;
defaultText = "(See source)";
description = ''
Helper functions for defining nodeinfo services.
'';
};
};
};
cfg = config.nix-bitcoin.nodeinfo;
nbLib = config.nix-bitcoin.lib;
script = pkgs.writeScriptBin "nodeinfo" ''
#!${pkgs.python3}/bin/python
import json
import subprocess
import sys
from collections import OrderedDict
def success(*args):
return subprocess.call(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0
def is_active(unit):
return success("systemctl", "is-active", "--quiet", unit)
def is_enabled(unit):
return success("systemctl", "is-enabled", "--quiet", unit)
def cmd(*args):
return subprocess.run(args, stdout=subprocess.PIPE).stdout.decode('utf-8')
def shell(*args):
return cmd("bash", "-c", *args).strip()
infos = OrderedDict()
operator = "${config.nix-bitcoin.operator.name}"
def get_onion_address(name, port):
path = f"/var/lib/onion-addresses/{operator}/{name}"
try:
with open(path, "r") as f:
onion_address = f.read().strip()
except OSError:
print(f"error reading file {path}", file=sys.stderr)
return
return f"{onion_address}:{port}"
def add_service(service, make_info, systemd_service = None):
systemd_service = systemd_service or service
if not is_active(systemd_service):
infos[service] = f"'{systemd_service}.service' is not running"
else:
info = OrderedDict()
exec(make_info, globals(), locals())
infos[service] = info
if is_enabled("onion-addresses") and not is_active("onion-addresses"):
print("error: service 'onion-addresses' is not running")
exit(1)
${concatStrings infos}
print(json.dumps(infos, indent=2))
'';
infos = map (serviceName:
let serviceCfg = config.services.${serviceName};
in optionalString serviceCfg.enable (cfg.services.${serviceName} serviceName serviceCfg)
) (builtins.attrNames cfg.services);
nodeinfoLib = rec {
mkInfo = extraCode: name: cfg:
mkInfoLong {
inherit extraCode name cfg;
};
mkInfoLong = { extraCode ? "", name, cfg, systemdServiceName ? name }: ''
add_service("${name}", """
info["local_address"] = "${nbLib.addressWithPort cfg.address cfg.port}"
'' + mkIfOnionPort name (onionPort: ''
info["onion_address"] = get_onion_address("${name}", ${onionPort})
'') + extraCode + ''
""", "${systemdServiceName}")
'';
mkIfOnionPort = name: fn:
if onionServices ? ${name} then
fn (toString (builtins.elemAt onionServices.${name}.map 0).port)
else
"";
};
inherit (config.services.tor.relay) onionServices;
in {
inherit options;
config = mkIf cfg.enable {
environment.systemPackages = [ script ];
nix-bitcoin.operator.enable = true;
nix-bitcoin.nodeinfo.services = with nodeinfoLib; {
bitcoind = mkInfo "";
lnd = name: cfg: mkInfo (''
info["rest_address"] = "${nbLib.addressWithPort cfg.restAddress cfg.restPort}"
'' + mkIfOnionPort "lnd-rest" (onionPort: ''
info["onion_rest_address"] = get_onion_address("lnd-rest", ${onionPort})
'') + ''
info["nodeid"] = shell("lncli getinfo | jq -r '.identity_pubkey'")
'') name cfg;
electrs = mkInfo "";
btcpayserver = mkInfo "";
rtl = mkInfo "";
mempool = mkInfo "";
mempool-frontend = name: cfg: mkInfoLong {
inherit name cfg;
systemdServiceName = "nginx";
};
# Only add sshd when it has an onion service
sshd = name: cfg: mkIfOnionPort "sshd" (onionPort: ''
add_service("sshd", """info["onion_address"] = get_onion_address("sshd", ${onionPort})""")
'');
};
};
}
-106
View File
@@ -1,106 +0,0 @@
# This module enables unprivileged users to read onion addresses.
# By default, onion addresses in /var/lib/tor/onion are only readable by the
# tor user.
# The included service copies onion addresses to /var/lib/onion-addresses/<user>/
# and sets permissions according to option 'access'.
{ config, lib, ... }:
with lib;
let
options.nix-bitcoin.onionAddresses = {
access = mkOption {
type = with types; attrsOf (listOf str);
default = {};
description = ''
This option controls who is allowed to access onion addresses.
For example, the following allows user 'myuser' to access bitcoind
and clightning onion addresses:
```nix
{
"myuser" = [ "bitcoind" "clightning" ];
};
```
The onion hostnames can then be read from
{file}`/var/lib/onion-addresses/myuser`.
'';
};
services = mkOption {
type = with types; listOf str;
default = [];
description = ''
Services that can access their onion address via file
{file}`/var/lib/onion-addresses/<service>`
The file is readable only by the service user.
'';
};
dataDir = mkOption {
readOnly = true;
default = "/var/lib/onion-addresses";
};
};
cfg = config.nix-bitcoin.onionAddresses;
nbLib = config.nix-bitcoin.lib;
in {
inherit options;
config = mkIf (cfg.access != {} || cfg.services != []) {
systemd.services.onion-addresses = {
wantedBy = [ "tor.service" ];
bindsTo = [ "tor.service" ];
after = [ "tor.service" ];
serviceConfig = nbLib.defaultHardening // {
Type = "oneshot";
RemainAfterExit = true;
StateDirectory = "onion-addresses";
StateDirectoryMode = "771";
PrivateNetwork = true; # This service needs no network access
PrivateUsers = false;
CapabilityBoundingSet = "CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER";
};
script = ''
waitForFile() {
file=$1
for ((i=0; i<300; i++)); do
if [[ -e $file ]]; then
return;
fi
sleep 0.1
done
echo "Error: File $file did not appear after 30 sec."
exit 1
}
# Wait until tor is up
waitForFile /var/lib/tor/state
cd ${cfg.dataDir}
rm -rf ./*
${concatMapStrings
(user: ''
mkdir -p -m 0700 ${user}
chown ${user} ${user}
${concatMapStrings
(service: ''
onionFile='/var/lib/tor/onion/${service}/hostname'
waitForFile "$onionFile"
cp "$onionFile" '${user}/${service}'
chown '${user}' '${user}/${service}'
'')
cfg.access.${user}
}
'')
(builtins.attrNames cfg.access)
}
${concatMapStrings (service: ''
onionFile=/var/lib/tor/onion/${service}/hostname
waitForFile "$onionFile"
install -D -o ${config.systemd.services.${service}.serviceConfig.User} -m 400 "$onionFile" services/${service}
'') cfg.services}
'';
};
};
}
-119
View File
@@ -1,119 +0,0 @@
# This module creates onion-services for NixOS services.
# An onion service can be enabled for every service that defines
# options 'address', 'port' and optionally 'getPublicAddressCmd'.
#
# See it in use at ./presets/enable-tor.nix
{ config, lib, pkgs, ... }:
with lib;
let
options.nix-bitcoin.onionServices = mkOption {
default = {};
type = with types; attrsOf (submodule (
{ config, ... }: {
options = {
enable = mkOption {
type = types.bool;
default = config.public;
description = ''
Create an onion service for the given service.
The service must define options {option}`address` and {option}`onionPort` (or `port`).
'';
};
public = mkOption {
type = types.bool;
default = false;
description = ''
Make the onion address accessible to the service.
If enabled, the onion service is automatically enabled.
Only available for services that define option {option}`getPublicAddressCmd`.
'';
};
externalPort = mkOption {
type = types.nullOr types.port;
default = null;
description = "Override the external port of the onion service.";
};
};
}
));
};
cfg = config.nix-bitcoin.onionServices;
nbLib = config.nix-bitcoin.lib;
onionServices = builtins.attrNames cfg;
activeServices = builtins.filter (service:
config.services.${service}.enable && cfg.${service}.enable
) onionServices;
publicServices = builtins.filter (service: cfg.${service}.public) activeServices;
in {
inherit options;
config = mkMerge [
(mkIf (activeServices != []) {
# Define hidden services
services.tor = {
enable = true;
relay.onionServices = genAttrs activeServices (name:
let
service = config.services.${name};
inherit (cfg.${name}) externalPort;
in nbLib.mkOnionService {
port = if externalPort != null then externalPort else service.port;
target.port = service.onionPort or service.port;
target.addr = nbLib.address service.address;
}
);
};
nix-bitcoin.onionAddresses = {
# Enable public services to access their own onion addresses
services = publicServices;
# Allow the operator user to access onion addresses for all active services
access.${config.nix-bitcoin.operator.name} = mkIf config.nix-bitcoin.operator.enable activeServices;
};
systemd.services = let
onionAddresses = [ "onion-addresses.service" ];
in genAttrs publicServices (service: {
# TODO-EXTERNAL: Instead of `wants`, use a future systemd dependency type
# that propagates initial start failures but no restarts
wants = onionAddresses;
after = onionAddresses;
});
})
# Set getPublicAddressCmd for public services
{
services = let
# publicServices' doesn't depend on config.services.*.enable,
# so we can use it to define config.services without causing infinite recursion
publicServices' = builtins.filter (service:
let srv = cfg.${service};
in srv.public && srv.enable
) onionServices;
in genAttrs publicServices' (service: {
getPublicAddressCmd = "cat ${config.nix-bitcoin.onionAddresses.dataDir}/services/${service}";
});
}
# Set sensible defaults for some services
{
nix-bitcoin.onionServices = {
btcpayserver = {
externalPort = 80;
};
rtl = {
externalPort = 80;
};
mempool-frontend = {
externalPort = 80;
};
};
}
];
}
-58
View File
@@ -1,58 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
options.nix-bitcoin.operator = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to define a user named `operator` for convenient interactive access
to nix-bitcoin features (like `bitcoin-cli`).
When using nix-bitcoin as part of a larger system config, it makes sense
to set your main system user as the operator, by setting option
`nix-bitcoin.operator.name = "MAIN_USER_NAME";`.
'';
};
name = mkOption {
type = types.str;
default = "operator";
description = "Name of the operator user.";
};
groups = mkOption {
type = with types; listOf str;
default = [];
description = "Extra groups of the operatur user.";
};
allowRunAsUsers = mkOption {
type = with types; listOf str;
default = [];
description = "Users as which the operator is allowed to run commands.";
};
};
cfg = config.nix-bitcoin.operator;
in {
inherit options;
config = mkIf cfg.enable {
users.users.${cfg.name} = {
isNormalUser = true;
extraGroups = [
"systemd-journal"
"proc" # Enable full /proc access and systemd-status
] ++ cfg.groups;
};
security = mkIf (cfg.allowRunAsUsers != []) {
# Use doas instead of sudo if enabled
doas.extraConfig = mkIf config.security.doas.enable ''
${lib.concatMapStrings (user: "permit nopass ${cfg.name} as ${user}\n") cfg.allowRunAsUsers}
'';
sudo.extraConfig = mkIf (!config.security.doas.enable) ''
${cfg.name} ALL=(${builtins.concatStringsSep "," cfg.allowRunAsUsers}) NOPASSWD: ALL
'';
};
};
}
-216
View File
@@ -1,216 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
options.services.rtl = {
enable = mkEnableOption "RTL, a web interface for LND";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen for HTTP connections.";
};
port = mkOption {
type = types.port;
default = 3000;
description = "Port to listen for HTTP connections.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/rtl";
description = "The data directory for RTL.";
};
nightTheme = mkOption {
type = types.bool;
default = false;
description = "Enable night theme by default.";
};
extraCurrency = mkOption {
type = types.nullOr types.str;
default = null;
example = "USD";
description = ''
Additional currency for displaying amounts.
When set, Tor is disabled for the RTL service to allow currency rate fetching.
'';
};
nodes = {
lnd = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable LND node in RTL.";
};
loop = mkOption {
type = types.bool;
default = false;
description = "Enable Lightning Loop integration (requires loopd).";
};
};
clightning = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable Core Lightning node in RTL (not supported in Sovran).";
};
};
};
user = mkOption {
type = types.str;
default = "rtl";
description = "The user as which to run RTL.";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = "The group as which to run RTL.";
};
tor = nbLib.tor;
};
cfg = config.services.rtl;
nbLib = config.nix-bitcoin.lib;
secretsDir = config.nix-bitcoin.secretsDir;
# Vendored RTL package
fetchNodeModules = pkgs.callPackage ../../packages/build-support/fetch-node-modules.nix {};
rtlPackage = pkgs.callPackage ../../packages/rtl { inherit fetchNodeModules; };
runePath = "${cfg.dataDir}/CLN-Rune.env";
rtlConfig = {
multiPass = "@multiPass@";
port = cfg.port;
host = cfg.address;
defaultNodeIndex = 1;
dbDirectoryPath = cfg.dataDir;
SSO = {
rtlSSO = 0;
rtlCookiePath = "";
logoutRedirectLink = "";
};
nodes = optional cfg.nodes.lnd.enable ({
index = 1;
lnNode = "lnd";
lnImplementation = "LND";
authentication = {
macaroonPath = "${cfg.dataDir}/macaroons";
swapMacaroonPath = if cfg.nodes.lnd.loop then "${cfg.dataDir}/loop-macaroons" else "";
boltzMacaroonPath = "";
};
settings = {
userPersona = "OPERATOR";
themeMode = if cfg.nightTheme then "NIGHT" else "DAY";
themeColor = "PURPLE";
channelBackupPath = "${cfg.dataDir}/backup";
logLevel = "INFO";
lnServerUrl = "https://${lnd.restAddress}:${toString lnd.restPort}";
swapServerUrl = if cfg.nodes.lnd.loop then "https://127.0.0.1:8081" else "";
boltzServerUrl = "";
fiatConversion = cfg.extraCurrency != null;
unannouncedChannels = true;
} // optionalAttrs (cfg.extraCurrency != null) {
currencyUnit = cfg.extraCurrency;
};
}) ++ optional cfg.nodes.clightning.enable {
index = 2;
lnNode = "clightning";
lnImplementation = "CLN";
authentication = {
runePath = runePath;
};
settings = {
userPersona = "OPERATOR";
themeMode = if cfg.nightTheme then "NIGHT" else "DAY";
themeColor = "PURPLE";
logLevel = "INFO";
fiatConversion = cfg.extraCurrency != null;
} // optionalAttrs (cfg.extraCurrency != null) {
currencyUnit = cfg.extraCurrency;
};
};
};
configFile = builtins.toFile "config" (builtins.toJSON rtlConfig);
inherit (config.services)
bitcoind
lnd;
lndLoopEnabled = cfg.nodes.lnd.enable && cfg.nodes.lnd.loop;
in {
inherit options;
config = mkIf cfg.enable {
assertions = [
{ assertion = cfg.nodes.lnd.enable;
message = ''
RTL: At least one node must be enabled. Sovran supports LND only.
'';
}
{ assertion = !cfg.nodes.clightning.enable;
message = ''
RTL: Core Lightning (clightning) is not supported in Sovran. Use LND instead.
'';
}
];
services.lnd.enable = mkIf cfg.nodes.lnd.enable true;
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
];
services.rtl.tor.enforce = mkIf (cfg.extraCurrency != null) false;
systemd.services.rtl = rec {
wantedBy = [ "multi-user.target" ];
wants = optional cfg.nodes.lnd.enable "lnd.service";
after = wants ++ [ "nix-bitcoin-secrets.target" ];
environment.RTL_CONFIG_PATH = cfg.dataDir;
environment.DB_DIRECTORY_PATH = cfg.dataDir;
serviceConfig = nbLib.defaultHardening // {
ExecStartPre = [
(nbLib.script "rtl-setup-config" ''
<${configFile} sed "s|@multiPass@|$(cat ${secretsDir}/rtl-password)|" \
> '${cfg.dataDir}/RTL-Config.json'
'')
]
++ optional cfg.nodes.lnd.enable
# The lnd admin macaroon is not readable by group `lnd`, so copy it
(nbLib.rootScript "rtl-copy-macaroon" ''
install --compare -m 640 -o ${cfg.user} -g ${cfg.group} -D ${lnd.networkDir}/admin.macaroon \
'${cfg.dataDir}/macaroons/admin.macaroon'
'');
ExecStart = "${rtlPackage}/bin/rtl";
# Show "rtl" instead of "node" in the journal
SyslogIdentifier = "rtl";
User = cfg.user;
Restart = "on-failure";
RestartSec = "10s";
ReadWritePaths = [ cfg.dataDir ];
} // nbLib.allowedIPAddresses cfg.tor.enforce
// nbLib.nodejs;
};
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
extraGroups = optional lndLoopEnabled lnd.group;
};
users.groups.${cfg.group} = {};
nix-bitcoin.secrets.rtl-password.user = cfg.user;
nix-bitcoin.generateSecretsCmds.rtl = ''
makePasswordSecret rtl-password
'';
};
}
-235
View File
@@ -1,235 +0,0 @@
{ config, pkgs, lib, ... }:
with lib;
let
options.nix-bitcoin = {
secretsDir = mkOption {
type = types.path;
default = "/etc/nix-bitcoin-secrets";
description = "Directory to store secrets";
};
setupSecrets = mkOption {
type = types.bool;
default = false;
description = ''
Set permissions for existing secrets in {option}`nix-bitcoin.secretsDir`
before services are started.
'';
};
generateSecrets = mkOption {
type = types.bool;
default = false;
description = ''
Automatically generate all required secrets before services are started.
Note: Make sure to create a backup of the generated secrets.
'';
};
generateSecretsCmds = mkOption {
type = types.attrsOf types.lines;
default = {};
description = ''
Bash expressions for generating secrets.
'';
};
# Currently, this is used only by ../deployment/nixops.nix
deployment.secretsDir = mkOption {
type = types.path;
description = ''
Directory of local secrets that are transferred to the nix-bitcoin node on deployment
'';
};
secrets = mkOption {
default = {};
type = with types; attrsOf (submodule (
{ config, ... }: {
options = {
user = mkOption {
type = str;
default = "root";
};
group = mkOption {
type = str;
default = config.user;
};
permissions = mkOption {
type = str;
default = "440";
};
};
}
));
};
secretsSetupMethod = mkOption {
type = with types; nullOr str;
default = null;
};
generateSecretsScript = mkOption {
internal = true;
default = let
rpcauthSrc = pkgs.fetchurl {
url = "https://raw.githubusercontent.com/bitcoin/bitcoin/d6cde007db9d3e6ee93bd98a9bbfdce9bfa9b15b/share/rpcauth/rpcauth.py";
sha256 = "189mpplam6yzizssrgiyv70c9899ggh8cac76j4n7v0xqzfip07n";
};
rpcauth = pkgs.writers.writeBash "rpcauth" ''
exec ${pkgs.python3}/bin/python ${rpcauthSrc} "$@"
'';
# Writes secrets to PWD
in pkgs.writers.writeBash "generate-secrets" ''
set -euo pipefail
export PATH=${lib.makeBinPath (with pkgs; [ coreutils gnugrep ])}
makePasswordSecret() {
# Passwords have alphabet {a-z, A-Z, 0-9} and ~119 bits of entropy
[[ -e $1 ]] || ${pkgs.pwgen}/bin/pwgen -s 20 1 > "$1"
}
makeBitcoinRPCPassword() {
user=$1
file=bitcoin-rpcpassword-$user
HMACfile=bitcoin-HMAC-$user
makePasswordSecret "$file"
if [[ $file -nt $HMACfile ]]; then
${rpcauth} $user $(cat "$file") | grep rpcauth | cut -d ':' -f 2 > "$HMACfile"
fi
}
makeCert() {
name=$1
# Add leading comma if not empty
extraAltNames=''${2:+,}''${2:-}
if [[ ! -e $name-key ]]; then
# Create new key and cert
doMakeCert "-newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout $name-key"
elif [[ ! -e $name-cert \
|| $(cat "$name-cert-alt-names" 2>/dev/null) != $extraAltNames ]]; then
# Create cert from existing key
doMakeCert "-key $name-key"
fi;
}
doMakeCert() {
# This fn uses global variables `name` and `extraAltNames`
keyOpts=$1
${pkgs.openssl}/bin/openssl req -x509 \
-sha256 -days 3650 $keyOpts -out "$name-cert" \
-subj "/CN=localhost/O=$name" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1$extraAltNames"
echo "$extraAltNames" > "$name-cert-alt-names"
}
umask u=rw,go=
${builtins.concatStringsSep "\n" (builtins.attrValues cfg.generateSecretsCmds)}
'';
};
};
cfg = config.nix-bitcoin;
in {
inherit options;
config = {
assertions = [
{ assertion = cfg.secretsSetupMethod != null;
message = ''
No secrets setup method has been defined.
To fix this, choose one of the following:
- Use one of the deployment methods in ${toString ./../deployment}
- Set `nix-bitcoin.generateSecrets = true` to automatically generate secrets
- Set `nix-bitcoin.secretsSetupMethod = "manual"` if you want to manually setup secrets
'';
}
];
# This target is active when secrets have been setup successfully.
systemd.targets.nix-bitcoin-secrets = mkIf (cfg.secretsSetupMethod != "manual") {
# This ensures that the secrets target is always activated when switching
# configurations.
# In this way `switch-to-configuration` is guaranteed to show an error
# when activating the secrets target fails on deployment.
wantedBy = [ "multi-user.target" ];
};
nix-bitcoin.setupSecrets = mkIf cfg.generateSecrets true;
nix-bitcoin.secretsSetupMethod = mkIf cfg.setupSecrets "setup-secrets";
# Operation of this service:
# - Set owner and permissions for all used secrets
# - Make all other secrets accessible to root only
# For all steps make sure that no secrets are copied to the nix store.
#
systemd.services.setup-secrets = mkIf cfg.setupSecrets {
requiredBy = [ "nix-bitcoin-secrets.target" ];
before = [ "nix-bitcoin-secrets.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
# Use the same sort order for globbing and sorting as in Nix attrsets.
# Required for `comm` below.
export LC_COLLATE=C
${optionalString cfg.generateSecrets ''
mkdir -p "${cfg.secretsDir}"
cd "${cfg.secretsDir}"
chown root: .
chmod 0700 .
${cfg.generateSecretsScript}
''}
setupSecret() {
file="$1"
user="$2"
group="$3"
permissions="$4"
if [[ ! -e $file ]]; then
echo "Error: Secret file '$file' is missing"
exit 1
fi
chown "$user:$group" "$file"
chmod "$permissions" "$file"
processedFiles+=("$file")
}
dir="${cfg.secretsDir}"
if [[ ! -e $dir ]]; then
echo "Error: Secrets dir '$dir' is missing"
exit 1
fi
chown root: "$dir"
cd "$dir"
processedFiles=()
${
concatStrings (mapAttrsToList (n: v: ''
setupSecret ${n} ${v.user} ${v.group} ${v.permissions}
'') cfg.secrets)
}
# Make all other files accessible to root only
unprocessedFiles=$(
comm -23 <(shopt -s nullglob; printf '%s\n' *) <(printf '%s\n' "''${processedFiles[@]}")
)
if [[ $unprocessedFiles ]]; then
IFS=$'\n'
# shellcheck disable=SC2086
chown root: $unprocessedFiles
# shellcheck disable=SC2086
chmod 0440 $unprocessedFiles
fi
# Now make the secrets dir accessible to other users
chmod 0751 "$dir"
'';
};
};
}
-48
View File
@@ -1,48 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
{
options = {
nix-bitcoin.security.dbusHideProcessInformation = mkOption {
type = types.bool;
default = false;
description = ''
Only allow users with group `proc` to retrieve systemd unit information like
cgroup paths (i.e. (sub)process command lines) via D-Bus.
This mitigates a systemd security issue where (sub)process command lines can
be retrieved by services even when their access to /proc is restricted
(via ProtectProc).
This option works by restricting the D-Bus method `GetUnitProcesses`, which
is also used internally by {command}`systemctl status`.
'';
};
};
config = mkIf config.nix-bitcoin.security.dbusHideProcessInformation {
users.groups.proc = {};
nix-bitcoin.operator.groups = [ "proc" ]; # Enable operator access to systemd-status
services.dbus.packages = lib.mkAfter [ # Apply at the end to override the default policy
(pkgs.writeTextDir "etc/dbus-1/system.d/dbus.conf" ''
<busconfig>
<policy context="default">
<deny
send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.systemd1.Manager"
send_member="GetUnitProcesses"
/>
</policy>
<policy group="proc">
<allow
send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.systemd1.Manager"
send_member="GetUnitProcesses"
/>
</policy>
</busconfig>
'')
];
};
}
-12
View File
@@ -1,12 +0,0 @@
{ config, lib, ... }:
with lib;
let
options.nix-bitcoin.configVersion = mkOption {
type = with types; nullOr str;
default = null;
description = "Vendored stub — no version migration needed.";
};
in {
inherit options;
config = {};
}
-1
View File
@@ -68,7 +68,6 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
name = "free";
};
# vendored: now no-op (always uses nixpkgs)
nix-bitcoin.useVersionLockedPkgs = false;
systemd.services.bitcoind = {
-16
View File
@@ -12,7 +12,6 @@ let
|| config.sovran_systemsOS.services.nextcloud
|| config.sovran_systemsOS.services.vaultwarden
|| config.sovran_systemsOS.features.haven
|| config.sovran_systemsOS.features."nwc-wallets"
|| config.sovran_systemsOS.features.element-calling;
in
{
@@ -71,7 +70,6 @@ in
BTCPAY=$(read_domain btcpayserver)
VAULTWARDEN=$(read_domain vaultwarden)
HAVEN=$(read_domain haven)
LIGHTNING=$(read_domain lightning)
ACME_EMAIL=$(read_domain sslemail)
# Start with global config — use ACME only when domain-based services are active
@@ -188,20 +186,6 @@ $HAVEN {
EOF
fi
# ── Lightning Wallet Connections LNURL ──────────────────────
if [ -n "$LIGHTNING" ]; then
cat >> /run/caddy/Caddyfile <<EOF
$LIGHTNING {
# LNURL discovery and callback are served by the dedicated
# nwc-lnurl service on loopback port 8181. Only these paths
# are proxied; the Alby Hub management port (18080) is never exposed.
reverse_proxy /.well-known/lnurlp/* http://127.0.0.1:8181
reverse_proxy /lnurlp/* http://127.0.0.1:8181
}
EOF
fi
# ── Sovran Hub (LAN access via mDNS) ────────────
cat >> /run/caddy/Caddyfile <<EOF
+23 -49
View File
@@ -32,31 +32,22 @@
# regenerated by the system activation script. The ``system.activationScripts``
# hook below converts it to a writable file each time the system is activated
# (i.e. after every ``nixos-rebuild switch``) and then injects the Sovran block.
# The same wrapped Nix-store executable is reused by both the activation hook
# and the ``sovran-hosts-update.service`` unit, ensuring a deterministic runtime
# PATH in every execution context.
# The same script is also run by the ``sovran-hosts-update.service`` unit so
# that the Hub can trigger it immediately after saving a domain without
# requiring a full rebuild.
let
# ── Wrapped Nix-store executable ──────────────────────────────────────────
# Built with pkgs.writeShellApplication so that all required runtime tools
# (awk, grep, coreutils) are declared explicitly and injected into PATH by
# Nix. Both the systemd service and the activation hook reference this same
# store-path executable — there is no second raw script body.
hostsUpdateScript = pkgs.writeShellApplication {
name = "sovran-hosts-update";
{
# ── Helper script (stored in the Nix store, never reads /var/lib at eval) ──
# Declare every external command the script calls. These packages are
# added to the script's runtime PATH by writeShellApplication; nothing from
# the system PATH is relied upon.
runtimeInputs = [
pkgs.coreutils # readlink, cp, mv, chmod, mktemp, rm, tr, head
pkgs.gawk # awk
pkgs.gnugrep # grep
];
environment.systemPackages = [ pkgs.coreutils ];
environment.etc."sovran-hosts-update.sh" = {
mode = "0755";
text = ''
#!/bin/sh
# Regenerate the Sovran-managed loopback block in /etc/hosts.
# Safe to run multiple times — idempotent.
set -eu
DOMAINS_DIR="/var/lib/domains"
HOSTS_FILE="/etc/hosts"
@@ -75,20 +66,19 @@ let
# ── Step 2: remove any existing Sovran block ──────────────────────────
# Use a temp file so the operation is atomic.
# awk -v passes marker strings safely without shell interpolation.
TMP=$(mktemp "$HOSTS_FILE.XXXXXX")
trap 'rm -f "$TMP"' EXIT
awk -v begin="$BEGIN_MARKER" -v end="$END_MARKER" '
$0 == begin { skip=1; next }
$0 == end { skip=0; next }
awk "
/^$BEGIN_MARKER\$/ { skip=1; next }
/^$END_MARKER\$/ { skip=0; next }
!skip
' "$HOSTS_FILE" > "$TMP"
" "$HOSTS_FILE" > "$TMP"
# ── Step 3: collect valid configured service domains ──────────────────
# NOTE: The hostname validation regex below must stay in sync with
# _SAFE_DOMAIN_RE in app/sovran_systemsos_web/server.py.
ENTRIES=""
for KEY in matrix wordpress nextcloud btcpayserver vaultwarden haven element-calling lightning; do
for KEY in matrix wordpress nextcloud btcpayserver vaultwarden haven element-calling; do
FILE="$DOMAINS_DIR/$KEY"
[ -f "$FILE" ] || continue
# Read the domain value (strip all whitespace, limit to 253 chars)
@@ -107,13 +97,11 @@ let
# ── Step 4: append the Sovran block if there are any entries ──────────
if [ -n "$ENTRIES" ]; then
{
printf '\n%s\n' "$BEGIN_MARKER"
printf '%s\n' "# These entries route configured service domains to local Caddy."
printf '%s\n' "# They are managed automatically — do not edit this block."
printf '%s\n' "$ENTRIES"
printf '%s\n' "$END_MARKER"
} >> "$TMP"
printf '\n%s\n' "$BEGIN_MARKER" >> "$TMP"
printf '%s\n' "# These entries route configured service domains to local Caddy." >> "$TMP"
printf '%s\n' "# They are managed automatically — do not edit this block." >> "$TMP"
printf '%s\n' "$ENTRIES" >> "$TMP"
printf '%s\n' "$END_MARKER" >> "$TMP"
fi
# ── Step 5: atomically replace /etc/hosts ─────────────────────────────
@@ -122,14 +110,6 @@ let
'';
};
in
{
# ── /etc/sovran-hosts-update.sh — operator discoverability symlink ─────────
# Retain the familiar /etc path so administrators can inspect or manually
# invoke the helper. The target is the wrapped Nix-store executable, so
# there is no second raw script body to keep in sync.
environment.etc."sovran-hosts-update.sh".source = lib.getExe hostsUpdateScript;
# ── Systemd service ────────────────────────────────────────────────────────
systemd.services.sovran-hosts-update = {
@@ -146,24 +126,18 @@ in
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
# Point directly at the wrapped Nix-store executable, not the /etc path.
ExecStart = lib.getExe hostsUpdateScript;
ExecStart = "/etc/sovran-hosts-update.sh";
};
};
# ── Activation script (runs after every nixos-rebuild switch) ─────────────
# This ensures the loopback block survives rebuilds that restore the /etc/hosts
# symlink. The "users" and "etc" scripts must complete first.
# The same wrapped Nix-store executable used by the systemd service is
# referenced here, guaranteeing identical runtime dependencies in both
# execution contexts.
system.activationScripts.sovranDomainLoopback = {
text = ''
if [ -d /var/lib/domains ]; then
if ! ${lib.getExe hostsUpdateScript}; then
echo "warning: sovran-hosts-update: failed to update /etc/hosts loopback entries" >&2
fi
if [ -x /etc/sovran-hosts-update.sh ] && [ -d /var/lib/domains ]; then
/etc/sovran-hosts-update.sh || true
fi
'';
deps = [ "etc" "users" ];
+21 -113
View File
@@ -1,124 +1,32 @@
{ config, pkgs, lib, ... }:
{
# ── Ensure njalla directory exists on every build ────────────────────────
# ── Ensure njalla directory and base script exist on every build ──
systemd.tmpfiles.rules = [
"d /var/lib/njalla 0750 root root -"
];
# ── Install the shared validation helper so the DDNS runner can import it ─
# The exact same _validate_ddns_url() function used by the Hub web application
# is installed here as a read-only system file. The DDNS runner imports it
# directly so the two code paths share one validator — no weaker inline copy.
environment.etc."sovran/security_helpers.py" = {
source = ../../app/sovran_systemsos_web/security_helpers.py;
mode = "0444";
user = "root";
group = "root";
};
# ── Safe DDNS update service ─────────────────────────────────────────────
# Reads DDNS update URLs from the JSON store written by the Hub API and
# invokes curl directly — no shell interpolation, no script execution.
# Replaces the legacy root cron job that ran /var/lib/njalla/njalla.sh.
systemd.services.sovran-ddns-update = {
description = "Sovran Njal.la DDNS update (safe JSON-based runner)";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
# ── Create base njalla.sh if it doesn't exist yet ────────────
systemd.services.njalla-init = {
description = "Initialize Njal.la DDNS script if missing";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
User = "root";
ExecStart = "${pkgs.python3}/bin/python3 /var/lib/sovran/ddns-update.py";
# Harden the service — it only needs network access and read access to
# /var/lib/njalla/ddns_urls.json.
NoNewPrivileges = true;
ProtectSystem = "strict";
ReadWritePaths = [ "/var/lib/njalla" ];
ReadOnlyPaths = [ "/etc/sovran" ];
ProtectHome = true;
PrivateTmp = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
Type = "oneshot";
RemainAfterExit = true;
};
};
# Run the update every 15 minutes
systemd.timers.sovran-ddns-update = {
description = "Sovran Njal.la DDNS update timer";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = "15min";
Persistent = true;
unitConfig = {
ConditionPathExists = "!/var/lib/njalla/njalla.sh";
};
script = ''
cat > /var/lib/njalla/njalla.sh <<'SCRIPT'
#!/usr/bin/env bash
IP=$(dig @resolver4.opendns.com myip.opendns.com +short -4)
## Add DDNS entries below — one curl per line
## Managed via Sovran Hub web interface
SCRIPT
chmod 700 /var/lib/njalla/njalla.sh
'';
};
# Install the Python runner script at build time so the service can find it.
# The script is owned by root and not world-writable.
# Uses _validate_ddns_url() from /etc/sovran/security_helpers.py — the same
# production validator used by the Hub API — before executing any curl call.
# No shell is used; no redirects; no script execution.
# ${IP} placeholder is preserved in stored URLs and substituted at runtime;
# the URL is validated after substitution so any remaining $ is rejected.
system.activationScripts.sovran-ddns-update-script = ''
install -d -m 0755 /var/lib/sovran
cat > /var/lib/sovran/ddns-update.py <<'PYEOF'
#!/usr/bin/env python3
"""Sovran safe DDNS update runner.
Reads ddns_urls.json, substitutes the public IP for the ''${IP} placeholder,
validates each URL using the production _validate_ddns_url() from
/etc/sovran/security_helpers.py, then calls curl per URL.
No shell interpolation. No redirects. No script execution.
"""
import ipaddress, json, os, subprocess, sys
sys.path.insert(0, '/etc/sovran')
try:
from security_helpers import _validate_ddns_url
except ImportError:
sys.exit(1) # validator missing — fail so systemd logs the misconfiguration
URLS_FILE = "/var/lib/njalla/ddns_urls.json"
try:
with open(URLS_FILE) as f:
urls = json.load(f)
if not isinstance(urls, list):
raise ValueError("not a list")
except Exception:
sys.exit(0) # no URLs configured — nothing to do
# Resolve current public IP once
public_ip = ""
try:
r = subprocess.run(
["dig", "@resolver4.opendns.com", "myip.opendns.com", "+short", "-4"],
capture_output=True, text=True, timeout=10,
)
raw = r.stdout.strip().splitlines()[0] if r.stdout.strip() else ""
ipaddress.ip_address(raw) # validates — raises if not a real IP
public_ip = raw
except Exception:
pass
if not public_ip:
sys.exit(0) # no IP resolved — skip to avoid sending bare ''${IP}
for raw_url in urls:
try:
# Substitute ''${IP} placeholder then validate through production validator.
# After substitution there must be no $ left; _validate_ddns_url rejects
# any remaining $ expression.
url = raw_url.replace("''${IP}", public_ip)
_validate_ddns_url(url)
subprocess.run(
["curl", "--silent", "--max-time", "15", "--fail", "--no-location", url],
timeout=20, check=False,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
except Exception:
pass
PYEOF
chmod 0500 /var/lib/sovran/ddns-update.py
'';
}
}
+3 -4
View File
@@ -3,9 +3,9 @@
{
config = lib.mkMerge [
# Vendored nix-bitcoin is always imported via modules/vendor/nix-bitcoin/modules.nix.
# This default satisfies the secrets assertion so Desktop-Only systems evaluate
# without enabling any Bitcoin services.
# nix-bitcoin is globally imported by the flake (nixosModules.Sovran_SystemsOS).
# This default satisfies nix-bitcoin's generateSecrets assertion so that Desktop
# Only systems can evaluate without enabling any Bitcoin services.
{
nix-bitcoin.generateSecrets = lib.mkDefault true;
}
@@ -34,7 +34,6 @@
mempool = lib.mkForce false;
element-calling = lib.mkForce false;
bitcoin-core = lib.mkForce false;
"nwc-wallets" = lib.mkForce false;
};
sovran_systemsOS.web.btcpayserver = lib.mkForce false;
-1
View File
@@ -46,7 +46,6 @@
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
bitcoin-core = lib.mkEnableOption "Bitcoin Core";
"nwc-wallets" = lib.mkEnableOption "Lightning Wallet Connections";
rdp = lib.mkEnableOption "Gnome Remote Desktop";
sshd = lib.mkEnableOption "SSH remote access";
+11 -95
View File
@@ -3,16 +3,6 @@
let
cfg = config.sovran_systemsOS;
# Read the OS version from the repo-root VERSION file at eval time so the
# Hub always ships a real, baked-in version string instead of relying on
# a runtime file lookup (which returns "dev" when /etc/nixos/VERSION is
# missing, e.g. in a dev checkout — this is the "vdev" bug in the Hub UI).
versionFile = ../../VERSION;
sovranVersion =
if builtins.pathExists versionFile
then builtins.replaceStrings [ "v" "\n" "\r" ] [ "" "" "" ] (builtins.readFile versionFile)
else "1.0.0";
monitoredServices =
# ── Infrastructure — System Passwords (always present) ─────
[
@@ -65,14 +55,12 @@ let
]; }
{ name = "Zeus Connect"; unit = "zeus-connect-setup.service"; type = "system"; icon = "zeus"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
{ label = "QR Code"; file = "/var/lib/secrets/zeus-connect-url"; qrcode = true; qronly = true; }
{ label = "How to Connect"; value = "1. Download Zeus from App Store or Google Play\n2. Open Zeus → Scan Node Config\n3. Scan the QR code above"; }
]; }
{ name = "Sparrow Auto-Link"; unit = "sparrow-autoconnect.service"; type = "system"; icon = "sparrow"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
{ label = "Server"; value = "tcp://127.0.0.1:50001 (Electrs)"; }
{ label = "Status"; value = "Auto-configured on first boot"; }
]; }
{ name = "Lightning Wallet Connections"; unit = "albyhub.service"; type = "system"; icon = "nwc"; enabled = cfg.features."nwc-wallets"; category = "bitcoin-apps"; credentials = [
{ label = "Lightning Address Domain"; file = "/var/lib/domains/lightning"; }
]; }
{ name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/mempool-frontend/hostname"; prefix = "http://"; }
{ label = "Local Network — Access on your home network only"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":60847"; }
@@ -88,7 +76,7 @@ let
{ label = "Test Username"; file = "/var/lib/secrets/matrix-test-username"; }
{ label = "Test Password"; file = "/var/lib/secrets/matrix-test-password"; }
]; }
{ name = "LiveKit (Element Call)"; unit = "livekit.service"; type = "system"; icon = "element-calling"; enabled = cfg.features.element-calling; category = "communication"; credentials = []; }
{ name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "element-calling"; enabled = cfg.features.element-calling; category = "communication"; credentials = []; }
]
# ── Self-Hosted Apps (server+desktop only) ─────────────────
++ lib.optionals cfg.roles.server_plus_desktop [
@@ -125,34 +113,8 @@ let
role = activeRole;
services = monitoredServices;
feature_manager = true;
sovran_version = sovranVersion;
});
generatedVersions = pkgs.writeText "sovran-hub-versions.json" (builtins.toJSON {
"caddy.service" = if pkgs ? caddy then pkgs.caddy.version else "2.8.4";
"tor.service" = if pkgs ? tor then pkgs.tor.version else "0.4.8.12";
"gnome-remote-desktop.service" = if pkgs ? gnome-remote-desktop then pkgs.gnome-remote-desktop.version else "46.0";
"bitcoind.service" = if pkgs ? bitcoind-knots then pkgs.bitcoind-knots.version else (if pkgs ? bitcoind then pkgs.bitcoind.version else "27.1.0");
"electrs.service" = if pkgs ? electrs then pkgs.electrs.version else "0.10.6";
"lnd.service" = if pkgs ? lnd then pkgs.lnd.version else "0.18.0";
# Keep the fallbacks aligned with the vendored packages used by the
# service modules (RTL 0.15.8 and Mempool 3.2.1). The nixpkgs attrs are
# optional because these packages are built locally in this repository.
"rtl.service" = if pkgs ? clightning-rtl then pkgs.clightning-rtl.version else (if pkgs ? rtl then pkgs.rtl.version else "0.15.8");
# BTCPay Server is intentionally sourced from pkgs.stable by the service
# module. Read the configured package here rather than pkgs.btcpayserver
# (unstable), otherwise the Hub can advertise a version that is not running.
"btcpayserver.service" = lib.getVersion config.services.btcpayserver.package;
"albyhub.service" = if pkgs ? albyhub then pkgs.albyhub.version else "1.8.0";
"mempool.service" = if pkgs ? mempool then pkgs.mempool.version else "3.2.1";
"matrix-synapse.service" = if pkgs ? matrix-synapse then pkgs.matrix-synapse.version else "1.115.0";
"livekit.service" = if pkgs ? livekit then pkgs.livekit.version else "1.5.2";
"vaultwarden.service" = if pkgs ? vaultwarden then pkgs.vaultwarden.version else "1.32.0";
# Nextcloud and WordPress are downloaded into /var/lib/www, so their
# deployed versions are read from their own PHP version files by the Hub.
"haven-relay.service" = if pkgs ? haven-relay then pkgs.haven-relay.version else (if pkgs ? haven then pkgs.haven.version else "0.1.0");
});
# ── Update wrapper script ──────────────────────────────────────
update-script = pkgs.writeShellScript "sovran-hub-update.sh" ''
set -uo pipefail
@@ -185,14 +147,13 @@ let
if [ "$RC" -eq 0 ]; then
echo "── Step 2/3: nixos-rebuild boot (stage next reboot) ──"
# Stream output straight into $LOG (see rebuild-script) so the Hub UI
# shows live progress instead of an empty log during long builds.
nixos-rebuild boot --flake /etc/nixos --print-build-logs \
BOOT_OUT=$(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
--option fallback true 2>&1)
BOOT_RC=$?
echo "$BOOT_OUT"
if [ "$BOOT_RC" -ne 0 ]; then
echo "[ERROR] nixos-rebuild boot failed"
RC=1
@@ -241,23 +202,20 @@ let
echo "══════════════════════════════════════════════════"
echo ""
echo "── Rebuilding system configuration ──────────────"
# Stream output straight into $LOG (tee'd by the exec redirect above) so
# the Hub UI shows live progress. Capturing the output in a variable
# kept the log empty for the entire build+activation, which made long
# rebuilds (e.g. the Bitcoin Knots → Core switch) look like a hang.
nixos-rebuild switch --flake /etc/nixos --print-build-logs \
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
--option fallback true 2>&1)
SWITCH_RC=$?
echo "$SWITCH_OUT"
if [ "$SWITCH_RC" -eq 0 ]; then
echo ""
echo "══════════════════════════════════════════════════"
echo " ✓ Rebuild completed successfully"
echo "══════════════════════════════════════════════════"
echo "SUCCESS" > "$STATUS"
elif grep -q "switchInhibitors\|Pre-switch checks failed" "$LOG"; then
elif echo "$SWITCH_OUT" | grep -q "switchInhibitors\|Pre-switch checks failed"; then
echo ""
echo " ✓ Build succeeded — a reboot is required to apply this rebuild"
echo " (Critical system components changed; running nixos-rebuild boot instead)"
@@ -327,8 +285,7 @@ let
sovran-hub-web = pkgs.python3Packages.buildPythonApplication {
pname = "sovran-systemsos-hub-web";
# Keep the package metadata in lockstep with the version shown in the Hub.
version = sovranVersion;
version = "1.0.0";
format = "other";
src = ../../app;
@@ -351,8 +308,6 @@ let
cp -r sovran_systemsos_web $out/lib/sovran-hub-web/
cp ${generatedConfig} $out/lib/sovran-hub-web/config.json
cp ${generatedVersions} $out/lib/sovran-hub-web/versions.json
printf '%s' "${sovranVersion}" > $out/lib/sovran-hub-web/VERSION
install -d $out/share/sovran-hub/icons
cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true
@@ -387,7 +342,6 @@ import os, sys
base = os.path.join("$out", "lib", "sovran-hub-web")
sys.path.insert(0, base)
os.environ["SOVRAN_HUB_CONFIG"] = os.path.join(base, "config.json")
os.environ["SOVRAN_HUB_VERSIONS"] = os.path.join(base, "versions.json")
os.environ["SOVRAN_HUB_ICONS"] = os.path.join("$out", "share", "sovran-hub", "icons")
import uvicorn
uvicorn.run(
@@ -399,26 +353,6 @@ uvicorn.run(
LAUNCHER
chmod +x $out/bin/sovran-hub-web
cat > $out/bin/nwc-wallet <<LAUNCHER
#!${pkgs.python3}/bin/python3
import os, sys
base = os.path.join("$out", "lib", "sovran-hub-web")
sys.path.insert(0, base)
from sovran_systemsos_web.nwc_wallet_cli import main
sys.exit(main())
LAUNCHER
chmod +x $out/bin/nwc-wallet
cat > $out/bin/nwc-lnurl <<LAUNCHER
#!${pkgs.python3}/bin/python3
import os, sys
base = os.path.join("$out", "lib", "sovran-hub-web")
sys.path.insert(0, base)
from sovran_systemsos_web.nwc_lnurl_service import main
main()
LAUNCHER
chmod +x $out/bin/nwc-lnurl
runHook postInstall
'';
@@ -430,12 +364,6 @@ LAUNCHER
in
{
options.services.sovranHub.webPackage = lib.mkOption {
type = lib.types.package;
default = sovran-hub-web;
description = "The sovran-hub-web Python application package. Other modules use this to reference Hub-installed scripts without duplicating the Python path setup.";
};
config = {
systemd.services.sovran-hub-web = {
description = "Sovran_SystemsOS Hub Web Interface";
@@ -454,25 +382,13 @@ in
};
path = [
pkgs.bash
pkgs.gawk
pkgs.qrencode
pkgs.curl
pkgs.iproute2
pkgs.nftables
pkgs.iptables
pkgs.hostname
pkgs.coreutils
pkgs.findutils
pkgs.gnugrep
pkgs.rsync
pkgs.acl
pkgs.util-linux
]
++ lib.optional cfg.services.bitcoin config.services.bitcoind.package
++ lib.optionals cfg.services.bitcoin [ pkgs.lnd ]
++ lib.optionals (cfg.services.nextcloud || cfg.services.synapse) [ config.services.postgresql.package ]
++ lib.optionals config.services.mysql.enable [ config.services.mysql.package ];
] ++ lib.optional cfg.services.bitcoin config.services.bitcoind.package;
};
systemd.services.sovran-hub-update = {
-175
View File
@@ -1,175 +0,0 @@
#!/usr/bin/env python3
"""Sovran restricted journal helper.
A root-owned, non-user-writable diagnostic tool that wraps journalctl with
a strict allowlist of safe flags. Replaces the ``journalctl *`` sudo rule
in tech-support.nix.
Accepted flags:
--unit / -u <name> must be one of the explicitly approved service units
--lines / -n <N> positive integer (max 10000)
--priority / -p <level> 0-7 or emerg/alert/crit/err/warning/notice/info/debug
--since <datetime> ISO 8601 date/datetime (no paths, no filesystem roots)
--until <datetime> ISO 8601 date/datetime (no paths, no filesystem roots)
--output / -o <format> short | short-iso | cat | json | verbose
At least one ``--unit`` flag is required; whole-journal queries are rejected.
All other flags, paths, directories, roots, namespaces, and output
destinations are rejected with a non-zero exit code.
"""
import re
import subprocess
import sys
# ── Allowlists ────────────────────────────────────────────────────────────────
# Explicit approved units. Only these four services may be queried through
# the restricted journal helper. Any other unit is rejected.
_APPROVED_UNITS: frozenset[str] = frozenset([
"sovran-hub-web.service",
"caddy.service",
"bitcoind.service",
"lnd.service",
])
_ALLOWED_PRIORITIES = frozenset([
"0", "1", "2", "3", "4", "5", "6", "7",
"emerg", "alert", "crit", "err", "warning", "notice", "info", "debug",
])
_ALLOWED_OUTPUT_FORMATS = frozenset([
"short", "short-iso", "cat", "json", "verbose",
])
# ISO 8601 date or datetime: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS (no paths)
_DATETIME_RE = re.compile(r'^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?)?$')
_MAX_LINES = 10000
# ── Argument parser ───────────────────────────────────────────────────────────
def _die(msg: str) -> None:
print(f"sovran-journal-helper: {msg}", file=sys.stderr)
sys.exit(1)
def _validate_unit(val: str) -> str:
if val not in _APPROVED_UNITS:
_die(
f"rejected unit name: {val!r} "
f"(allowed: {', '.join(sorted(_APPROVED_UNITS))})"
)
return val
def _validate_lines(val: str) -> str:
try:
n = int(val)
except ValueError:
_die(f"rejected: --lines must be a positive integer, got {val!r}")
if n <= 0 or n > _MAX_LINES:
_die(f"rejected: --lines must be between 1 and {_MAX_LINES}, got {n}")
return str(n)
def _validate_priority(val: str) -> str:
if val not in _ALLOWED_PRIORITIES:
_die(f"rejected priority: {val!r}")
return val
def _validate_datetime(val: str) -> str:
if not _DATETIME_RE.match(val):
_die(f"rejected: datetime {val!r} (must be YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)")
return val
def _validate_output(val: str) -> str:
if val not in _ALLOWED_OUTPUT_FORMATS:
_die(f"rejected output format: {val!r}")
return val
def main() -> None:
args = sys.argv[1:]
cmd = ["journalctl"]
unit_count = 0
i = 0
while i < len(args):
arg = args[i]
if arg in ("--unit", "-u"):
i += 1
if i >= len(args):
_die("--unit requires a value")
cmd += ["--unit", _validate_unit(args[i])]
unit_count += 1
elif arg.startswith("--unit="):
cmd += ["--unit", _validate_unit(arg[len("--unit="):])]
unit_count += 1
elif arg in ("--lines", "-n"):
i += 1
if i >= len(args):
_die("--lines requires a value")
cmd += ["--lines", _validate_lines(args[i])]
elif arg.startswith("--lines="):
cmd += ["--lines", _validate_lines(arg[len("--lines="):])]
elif re.match(r'^-n\d+$', arg):
cmd += ["--lines", _validate_lines(arg[2:])]
elif arg in ("--priority", "-p"):
i += 1
if i >= len(args):
_die("--priority requires a value")
cmd += ["--priority", _validate_priority(args[i])]
elif arg.startswith("--priority="):
cmd += ["--priority", _validate_priority(arg[len("--priority="):])]
elif arg == "--since":
i += 1
if i >= len(args):
_die("--since requires a value")
cmd += ["--since", _validate_datetime(args[i])]
elif arg.startswith("--since="):
cmd += ["--since", _validate_datetime(arg[len("--since="):])]
elif arg == "--until":
i += 1
if i >= len(args):
_die("--until requires a value")
cmd += ["--until", _validate_datetime(args[i])]
elif arg.startswith("--until="):
cmd += ["--until", _validate_datetime(arg[len("--until="):])]
elif arg in ("--output", "-o"):
i += 1
if i >= len(args):
_die("--output requires a value")
cmd += ["--output", _validate_output(args[i])]
elif arg.startswith("--output="):
cmd += ["--output", _validate_output(arg[len("--output="):])]
else:
_die(
f"rejected flag: {arg!r}. "
"Allowed flags: --unit, --lines, --priority, --since, --until, --output"
)
i += 1
if unit_count == 0:
_die(
"at least one --unit flag is required; "
f"allowed units: {', '.join(sorted(_APPROVED_UNITS))}"
)
result = subprocess.run(cmd)
sys.exit(result.returncode)
if __name__ == "__main__":
main()
+12 -33
View File
@@ -11,10 +11,11 @@
# (u:sovran-support:---) by the Hub API as soon as a session is started.
# • The Hub web UI lets the user grant time-limited access to wallet files
# and view a full audit log of every session event.
# • Scoped sudo rules allow support staff to restart specific services and
# read logs — without full root, wallet access, Nix editing, or rebuilds.
# • journalctl access is provided only through the root-owned
# sovran-journal-helper script (see below) with an allowlist of safe flags.
# • Scoped sudo rules allow support staff to edit custom.nix, trigger rebuilds,
# restart services, and read logs — without full root or wallet access.
#
# The `acl` package provides the `setfacl` / `getfacl` utilities required by
# the Hub's _apply_wallet_acls() and _revoke_wallet_acls() helpers.
{
# ── System packages ────────────────────────────────────────────────────────
environment.systemPackages = [ pkgs.acl ];
@@ -41,40 +42,18 @@
"d /var/lib/sovran-support/.ssh 0700 sovran-support sovran-support -"
];
# ── Restricted journal helper ─────────────────────────────────────────────
# The helper is root-owned, not writable by any user, and accepts only a
# narrow allowlist of safe journalctl flags. It is the sole mechanism by
# which the support user may read journal logs.
environment.etc."sovran/sovran-journal-helper.py" = {
source = ./sovran-journal-helper.py;
mode = "0500";
user = "root";
group = "root";
};
# ── Scoped sudo rules for support staff ───────────────────────────────────
# Grants only the minimum privileges needed for diagnostic support.
# Editing Nix configuration and running nixos-rebuild are intentionally
# excluded: combining those two permissions provides a trivial path to
# arbitrary root code execution. Systemctl access is limited to a small
# allowlist of named service restart operations. journalctl is available
# only through the restricted helper above.
# Grants only the minimum privileges needed for a support session.
# Support staff cannot stop/disable/mask services or access wallet files.
security.sudo.extraRules = [
{
users = [ "sovran-support" ];
commands = [
{ command = "/run/current-system/sw/bin/systemctl restart sovran-hub-web.service"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/systemctl restart caddy.service"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/systemctl restart bitcoind.service"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/systemctl restart lnd.service"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/systemctl status sovran-hub-web.service"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/systemctl status caddy.service"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/systemctl status bitcoind.service"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/systemctl status lnd.service"; options = [ "NOPASSWD" ]; }
# Restricted journal helper: accepts only safe flags (--unit, --lines,
# --priority, --since, --until, --output). Rejects paths, directories,
# namespaces, roots, and arbitrary output destinations.
{ command = "/run/current-system/sw/bin/python3 /etc/sovran/sovran-journal-helper.py *"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/nano /etc/nixos/custom.nix"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/nano /etc/nixos/configuration.nix"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/nixos-rebuild switch --flake /etc/nixos"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/systemctl restart *"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/journalctl *"; options = [ "NOPASSWD" ]; }
];
}
];
+1 -9
View File
@@ -111,15 +111,7 @@ in
echo "$ROOT_PASS" > "$SECRET_FILE"
chmod 600 "$SECRET_FILE"
fi
# If the file contains a scrypt hash (salt:hash), skip chpasswd — the
# password was already set via the Hub security reset endpoint and this
# service is only re-running as a manual recovery step.
CONTENT="$(cat "$SECRET_FILE")"
if echo "$CONTENT" | grep -qE '^[0-9a-f]{32}:[0-9a-f]{64,}$'; then
echo "root-password-setup: stored value is already hashed — skipping chpasswd" >&2
else
echo "root:$CONTENT" | chpasswd
fi
echo "root:$(cat "$SECRET_FILE")" | chpasswd
'';
};
-1
View File
@@ -32,7 +32,6 @@
# ── Features (default OFF — enable in custom.nix) ─────────
./haven.nix
./nwc-wallets.nix
./element-calling.nix
./mempool.nix
./bitcoin-core.nix
-182
View File
@@ -1,182 +0,0 @@
{ config, pkgs, lib, ... }:
let
albyHubPort = 18080;
albyHubApiBase = "http://127.0.0.1:${toString albyHubPort}";
patchedAlbyHub = pkgs.albyhub.overrideAttrs (old: {
patches = (old.patches or []) ++ [
../packages/albyhub/0001-private-route-hints.patch
../packages/albyhub/0002-isolated-invoice-app-id.patch
../packages/albyhub/0003-loopback-bind-host.patch
];
});
lndRpcAddress = lib.attrByPath [ "services" "lnd" "rpcAddress" ] "127.0.0.1" config;
lndRpcPort = toString (lib.attrByPath [ "services" "lnd" "rpcPort" ] 10009 config);
lndCertPath = config.services.lnd.certPath;
pythonManagerEnvironment = {
NWC_ALBY_HUB_API_BASE = albyHubApiBase;
NWC_LND_ADDRESS = "${lndRpcAddress}:${lndRpcPort}";
NWC_LND_CERT_FILE = lndCertPath;
NWC_LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon";
NWC_RELAY =
if config.sovran_systemsOS.features.haven
then "wss://haven.${config.networking.domain}/nostr"
else "wss://relay.getalby.com,wss://relay2.getalby.com";
};
wrappedNwcWallet = lib.hiPrio (pkgs.writeShellScriptBin "nwc-wallet" ''
export NWC_ALBY_HUB_API_BASE='${pythonManagerEnvironment.NWC_ALBY_HUB_API_BASE}'
export NWC_LND_ADDRESS='${pythonManagerEnvironment.NWC_LND_ADDRESS}'
export NWC_LND_CERT_FILE='${pythonManagerEnvironment.NWC_LND_CERT_FILE}'
export NWC_LND_MACAROON_FILE='${pythonManagerEnvironment.NWC_LND_MACAROON_FILE}'
export NWC_RELAY='${pythonManagerEnvironment.NWC_RELAY}'
exec ${config.services.sovranHub.webPackage}/bin/nwc-wallet "$@"
'');
albyhubWrapper = pkgs.writeShellScript "albyhub-wrapper" ''
set -euo pipefail
password_file="/var/lib/albyhub/unlock-password"
if [ ! -s "$password_file" ]; then
umask 077
${pkgs.openssl}/bin/openssl rand -hex 32 > "$password_file"
fi
export AUTO_UNLOCK_PASSWORD="$(cat "$password_file")"
exec ${lib.getExe patchedAlbyHub}
'';
in
lib.mkIf config.sovran_systemsOS.features."nwc-wallets" {
assertions = [
{
assertion = config.services.lnd.enable;
message = "Lightning Wallet Connections requires services.lnd.enable = true.";
}
{
assertion = !(lib.attrByPath [ "nix-bitcoin" "netns-isolation" "enable" ] false config);
message = "Lightning Wallet Connections requires nix-bitcoin.netns-isolation.enable = false.";
}
{
assertion = albyHubPort != config.services.lnd.restPort;
message = "Alby Hub and LND REST must use different ports.";
}
{
assertion = albyHubPort != 8181;
message = "Alby Hub and the public LNURL service must use different ports.";
}
{
assertion = !(lib.elem albyHubPort config.networking.firewall.allowedTCPPorts);
message = "Alby Hub management port must not be opened on the public TCP firewall.";
}
];
users.groups.albyhub = { };
users.users.albyhub = {
isSystemUser = true;
group = "albyhub";
home = "/var/lib/albyhub";
createHome = false;
extraGroups = [ ];
};
systemd.tmpfiles.rules = [
"d /var/lib/albyhub 0700 albyhub albyhub -"
];
services.lnd.macaroons.albyhub = {
user = "albyhub";
permissions = lib.concatStringsSep "," [
''{"entity":"info","action":"read"}''
''{"entity":"offchain","action":"read"}''
''{"entity":"offchain","action":"write"}''
''{"entity":"invoices","action":"read"}''
''{"entity":"invoices","action":"write"}''
''{"entity":"onchain","action":"read"}''
''{"entity":"address","action":"read"}''
''{"entity":"message","action":"read"}''
''{"entity":"message","action":"write"}''
];
};
systemd.services.albyhub = {
description = "Alby Hub — NWC wallet server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "lnd.service" ];
requires = [ "lnd.service" ];
environment = {
HOME = "/var/lib/albyhub";
HOST = "127.0.0.1";
LN_BACKEND_TYPE = "LND";
ENABLE_ADVANCED_SETUP = "false";
LND_ADDRESS = "${lndRpcAddress}:${lndRpcPort}";
LND_CERT_FILE = lndCertPath;
LND_MACAROON_FILE = "/run/lnd/albyhub.macaroon";
WORK_DIR = "/var/lib/albyhub";
DATABASE_URI = "/var/lib/albyhub/nwc.db";
PORT = toString albyHubPort;
# Use private Nostr relay if Haven is enabled, otherwise default to Alby's public relays
RELAY =
if config.sovran_systemsOS.features.haven
then "wss://haven.${config.networking.domain}/nostr"
else "wss://relay.getalby.com,wss://relay2.getalby.com";
AUTO_LINK_ALBY_ACCOUNT = "false";
SEND_EVENTS_TO_ALBY = "false";
LOG_TO_FILE = "false";
HIDE_UPDATE_BANNER = "true";
};
serviceConfig = {
Type = "simple";
User = "albyhub";
Group = "albyhub";
WorkingDirectory = "/var/lib/albyhub";
ExecStart = albyhubWrapper;
Restart = "on-failure";
RestartSec = "10s";
UMask = "0077";
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
ReadWritePaths = [ "/var/lib/albyhub" ];
ReadOnlyPaths = [ lndCertPath "/run/lnd" ];
};
};
systemd.services.nwc-lnurl = {
description = "Lightning Wallet Connections public LNURL service";
wantedBy = [ "multi-user.target" ];
after = [ "albyhub.service" "sovran-hub-web.service" ];
wants = [ "albyhub.service" ];
environment = pythonManagerEnvironment;
serviceConfig = {
Type = "simple";
User = "albyhub";
Group = "albyhub";
ExecStart = "${config.services.sovranHub.webPackage}/bin/nwc-lnurl";
Restart = "on-failure";
RestartSec = "10s";
UMask = "0027";
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
ReadOnlyPaths = [
"/var/lib/domains/lightning"
"/var/lib/albyhub/unlock-password"
];
};
};
systemd.services.sovran-hub-web.environment = pythonManagerEnvironment;
environment.systemPackages = lib.mkBefore [ wrappedNwcWallet ];
sovran_systemsOS.domainRequirements = [
{
name = "lightning";
label = "Lightning Address Domain";
example = "pay.yourdomain.com";
needsDDNS = true;
}
];
}
-24
View File
@@ -229,30 +229,6 @@ CREDS
chmod 600 "$CREDS_FILE"
fi
# Provision a private, random Matrix administrator for the Hub's Admin
# API calls. This is deliberately separate from the visible bootstrap
# admin account: installations where @admin pre-dated Sovran do not have
# its password, but Hub user management must still work. Keeping the
# generated localpart in this root-only file makes the operation
# idempotent without exposing this service credential in the Hub UI.
HUB_ADMIN_CREDS="/var/lib/secrets/matrix-hub-admin"
if [ ! -s "$HUB_ADMIN_CREDS" ]; then
HUB_ADMIN_USER="sovran-hub-$(pwgen -sA0 20 1)"
HUB_ADMIN_PASS=$(pwgen -s 32 1)
if register_new_matrix_user -c /run/matrix-synapse/runtime-config.yaml \
-u "$HUB_ADMIN_USER" -p "$HUB_ADMIN_PASS" -a http://localhost:8008; then
(umask 077; cat > "$HUB_ADMIN_CREDS" <<CREDS
username=$HUB_ADMIN_USER
password=$HUB_ADMIN_PASS
CREDS
)
echo "Created private Matrix Hub service administrator."
else
echo "Failed to create the private Matrix Hub service administrator." >&2
exit 1
fi
fi
# Always write individual credential files for the hub UI, even if the bulk
# credentials file already existed from a prior run (umask 077 ensures mode 600).
# If passwords were not freshly generated above, parse them from the bulk file.
@@ -1,13 +0,0 @@
diff --git a/lnclient/lnd/lnd.go b/lnclient/lnd/lnd.go
index bad532f..ba35b7b 100644
--- a/lnclient/lnd/lnd.go
+++ b/lnclient/lnd/lnd.go
@@ -708,7 +708,7 @@ func (svc *LNDService) MakeInvoice(ctx context.Context, amountMsat int64, descri
DescriptionHash: descriptionHashBytes,
Expiry: expiry,
RouteHints: hints,
- Private: !hasPublicChannels, // use private channel hints in the invoice
+ Private: true, // always include private channel hints in the invoice
}
resp, err := svc.client.AddInvoice(ctx, addInvoiceRequest)
@@ -1,71 +0,0 @@
diff --git a/api/models.go b/api/models.go
index 28bd117..dceb79a 100644
--- a/api/models.go
+++ b/api/models.go
@@ -46,7 +46,7 @@ type API interface {
ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error)
ListOnchainTransactions(ctx context.Context) ([]OnchainTransaction, error)
SendPayment(ctx context.Context, invoice string, amountMsat *uint64, metadata map[string]interface{}, fromAppId *uint) (*SendPaymentResponse, error)
- CreateInvoice(ctx context.Context, amountMsat uint64, description string) (*MakeInvoiceResponse, error)
+ CreateInvoice(ctx context.Context, amountMsat uint64, description string, appId *uint) (*MakeInvoiceResponse, error)
LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error)
SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error
RequestMempoolApi(ctx context.Context, endpoint string) (interface{}, error)
@@ -586,6 +586,7 @@ type MakeInvoiceRequest struct {
AmountSat *uint64 `json:"amountSat"`
AmountMsat *uint64 `json:"amountMsat"`
Description string `json:"description"`
+ AppId *uint `json:"appId"`
}
type ResetRouterRequest struct {
diff --git a/api/transactions.go b/api/transactions.go
index aaf9380..3f9850a 100644
--- a/api/transactions.go
+++ b/api/transactions.go
@@ -12,12 +12,12 @@ import (
"github.com/sirupsen/logrus"
)
-func (api *api) CreateInvoice(ctx context.Context, amountMsat uint64, description string) (*MakeInvoiceResponse, error) {
+func (api *api) CreateInvoice(ctx context.Context, amountMsat uint64, description string, appId *uint) (*MakeInvoiceResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, ErrLNClientNotStarted
}
- transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, nil, nil, nil)
+ transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, appId, nil, nil)
if err != nil {
return nil, err
}
diff --git a/http/http_service.go b/http/http_service.go
index bf650b9..cf61250 100644
--- a/http/http_service.go
+++ b/http/http_service.go
@@ -687,7 +687,12 @@ func (httpSvc *HttpService) makeInvoiceHandler(c echo.Context) error {
amountMsat = *resolvedAmountMsat
}
- invoice, err := httpSvc.api.CreateInvoice(c.Request().Context(), amountMsat, makeInvoiceRequest.Description)
+ invoice, err := httpSvc.api.CreateInvoice(
+ c.Request().Context(),
+ amountMsat,
+ makeInvoiceRequest.Description,
+ makeInvoiceRequest.AppId,
+ )
if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go
index d25259b..c9fe4e8 100644
--- a/wails/wails_handlers.go
+++ b/wails/wails_handlers.go
@@ -600,7 +600,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
if resolvedAmountMsat != nil {
amountMsat = *resolvedAmountMsat
}
- invoice, err := app.api.CreateInvoice(ctx, amountMsat, makeInvoiceRequest.Description)
+ invoice, err := app.api.CreateInvoice(ctx, amountMsat, makeInvoiceRequest.Description, nil)
if err != nil {
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
@@ -1,37 +0,0 @@
diff --git a/cmd/http/main.go b/cmd/http/main.go
index ed31f96..4d31212 100644
--- a/cmd/http/main.go
+++ b/cmd/http/main.go
@@ -2,8 +2,8 @@ package main
import (
"context"
- "fmt"
nethttp "net/http"
+ "net"
"os"
"os/signal"
"syscall"
@@ -51,8 +51,9 @@ func main() {
httpSvc := http.NewHttpService(svc, svc.GetEventPublisher())
httpSvc.RegisterSharedRoutes(e)
//start Echo server
+ bindAddress := net.JoinHostPort(svc.GetConfig().GetEnv().Host, svc.GetConfig().GetEnv().Port)
go func() {
- if err := e.Start(fmt.Sprintf(":%v", svc.GetConfig().GetEnv().Port)); err != nil && err != nethttp.ErrServerClosed {
+ if err := e.Start(bindAddress); err != nil && err != nethttp.ErrServerClosed {
logger.Logger.WithError(err).Error("echo server failed to start")
cancel()
}
diff --git a/config/models.go b/config/models.go
index 0fb8870..dc11afd 100644
--- a/config/models.go
+++ b/config/models.go
@@ -24,6 +24,7 @@ type AppConfig struct {
LNDAddress string `envconfig:"LND_ADDRESS"`
LNDCertFile string `envconfig:"LND_CERT_FILE"`
LNDMacaroonFile string `envconfig:"LND_MACAROON_FILE"`
+ Host string `envconfig:"HOST" default:"127.0.0.1"`
Workdir string `envconfig:"WORK_DIR"`
Port string `envconfig:"PORT" default:"8080"`
DatabaseUri string `envconfig:"DATABASE_URI" default:"nwc.db"`
@@ -1,79 +0,0 @@
# Vendored from fort-nix/nix-bitcoin commit 360e30fee.
# This local copy does not fetch or import nix-bitcoin.
# This is a modified version of
# https://github.com/NixOS/nixpkgs/pull/128749
{ lib, stdenvNoCC, makeWrapper, nodejs, cacert }:
{ src
, hash ? ""
, runScripts ? false
, preferLocalBuild ? true
, npmFlags ? ""
, ...
} @ args:
stdenvNoCC.mkDerivation ({
inherit src preferLocalBuild;
name = "${src.name}-node_modules";
nativeBuildInputs = [
makeWrapper
(if args ? nodejs then args.nodejs else nodejs)
];
outputHashMode = "recursive";
impureEnvVars = lib.fetchers.proxyImpureEnvVars;
phases = "unpackPhase patchPhase buildPhase installPhase";
# npm doesn't support var `SSL_CERT_FILE`.
NODE_EXTRA_CA_CERTS = "${cacert}/etc/ssl/certs/ca-bundle.crt";
buildPhase = ''
runHook preBuild
if [[ ! -f package.json ]]; then
echo "Error: file `package.json` doesn't exist"
exit 1
fi
if [[ ! -f package-lock.json ]]; then
echo "Error: file `package-lock.json` doesn't exist"
exit 1
fi
export SOURCE_DATE_EPOCH=1
export npm_config_cache=/tmp
NPM_FLAGS="--omit=dev --omit=optional --no-update-notifier $npmFlags"
# Scripts may result in non-deterministic behavior.
# Some packages (e.g., Puppeteer) use postinstall scripts to download extra data.
if [[ ! $runScripts ]]; then
NPM_FLAGS+=" --ignore-scripts"
fi
echo "Running npm ci $NPM_FLAGS"
npm ci $NPM_FLAGS
cp package.json \
package-lock.json node_modules/
rm -f node_modules/.package-lock.json
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib
cp -r node_modules $out/lib
runHook postInstall
'';
} // (
if hash == "" then {
outputHashAlgo = "sha256";
outputHash = "";
} else {
outputHash = hash;
}
) // (builtins.removeAttrs args [ "hash" ]))
-19
View File
@@ -1,19 +0,0 @@
{ lib, buildGoModule, fetchFromGitHub }:
buildGoModule rec {
pname = "lndinit";
version = "0.1.3-beta";
src = fetchFromGitHub {
owner = "lightninglabs";
repo = pname;
rev = "v${version}";
sha256 = "sha256-sO1DpbppCurxr9g9nUl9Vx82FJK1mTcUw3rY1Fm1wEU=";
};
vendorHash = "sha256-El44BS5Bu0K/klMxkajciU/R6uqiXBMOiLN536QztbE=";
subPackages = [ "." ];
meta = with lib; {
description = "Wallet initializer for lnd (from nix-bitcoin)";
homepage = "https://github.com/lightninglabs/lndinit";
license = licenses.mit;
};
}
@@ -1,41 +0,0 @@
From e4b3ebaf0451c1bddbd7dcf8527c296938ebb607 Mon Sep 17 00:00:00 2001
From: Erik Arvstedt <erik.arvstedt@gmail.com>
Date: Sun, 1 Jun 2025 11:17:22 +0200
Subject: [PATCH] allow disabling mining pool fetching in offline environments
Previously, Mempool strictly required fetching mining pool data from
Github and failed when this was not possible, e.g. in offline
environments.
This patch allows disabling pool fetching.
When disabled, empty pool data is inserted into the DB, which
effectively turns off block pool classification.
---
backend/src/tasks/pools-updater.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts
index 6b0520dfc..a74259b95 100644
--- a/backend/src/tasks/pools-updater.ts
+++ b/backend/src/tasks/pools-updater.ts
@@ -75,7 +75,7 @@ class PoolsUpdater {
} else {
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag);
}
- const poolsJson = await this.query(this.poolsUrl);
+ const poolsJson = (githubSha == "disable-pool-fetching") ? [] : await this.query(this.poolsUrl);
if (poolsJson === undefined) {
return;
}
@@ -136,6 +136,9 @@ class PoolsUpdater {
* Fetch our latest pools-v2.json sha from github
*/
private async fetchPoolsSha(): Promise<string | null> {
+ if (this.poolsUrl == "disable-pool-fetching") {
+ return "disable-pool-fetching";
+ }
const response = await this.query(this.treeUrl);
if (response !== undefined) {
--
2.47.2
-203
View File
@@ -1,203 +0,0 @@
# Packaging adapted from fort-nix/nix-bitcoin commit 360e30fee.
# This local copy does not fetch or import nix-bitcoin.
{ lib
, stdenvNoCC
, nodejs_22
, nodejs-slim_22
, fetchFromGitHub
, fetchNodeModules
, runCommand
, makeWrapper
, curl
, cacert
, rsync
# for rust-gbt (backend module)
, cargo
, rustc
, rustPlatform
, napi-rs-cli
}:
rec {
nodejs = nodejs_22;
nodejsRuntime = nodejs-slim_22;
version = "3.2.1";
src = fetchFromGitHub {
owner = "mempool";
repo = "mempool";
tag = "v${version}";
hash = "sha256-O2XPD1/BXQnzuOP/vMVyRfmFZEgjA85r+PShWne0vqU=";
};
nodeModules = {
frontend = fetchNodeModules {
inherit src nodejs;
sourceRoot = "source/frontend";
hash = "sha256-+jfgsAkDdYvgso8uSHaBj/sQL3fC/ABQWzVTXfdZcU0=";
};
backend = fetchNodeModules {
inherit src nodejs;
sourceRoot = "source/backend";
hash = "sha256-y5l2SYZYK9SKSy6g0+mtTWD6JFkkdQHHBboECpEvWZ4=";
};
};
frontendAssets = fetchFiles {
name = "mempool-frontend-assets";
hash = "sha256-r6GfOY8Pdh15o2OQMk8syfvWMV6WMCReToAEkQm7tqQ=";
fetcher = ./frontend-assets-fetch.sh;
};
mempool-backend = mkDerivationMempool {
pname = "mempool-backend";
patches = [ ./0001-allow-disabling-mining-pool-fetching.patch ];
buildPhase = ''
cd backend
${sync} --chmod=+w ${nodeModules.backend}/lib/node_modules .
patchShebangs node_modules
${sync} ${mempool-rust-gbt}/ rust-gbt
npm run package
runHook postBuild
'';
installPhase = ''
mkdir -p $out/lib/mempool-backend
${sync} package/ $out/lib/mempool-backend
makeWrapper ${nodejsRuntime}/bin/node $out/bin/mempool-backend \
--add-flags $out/lib/mempool-backend/index.js
runHook postInstall
'';
passthru = {
inherit nodejs nodejsRuntime;
nodeModules = nodeModules.backend;
};
};
mempool-frontend = mkFrontend {};
# Argument `config` (type: attrset) defines the mempool frontend config.
# If `{}`, the default config is used.
# See here for available options:
# https://github.com/mempool/mempool/blob/master/frontend/src/app/services/state.service.ts
# (`interface Env` and `defaultEnv`)
mkFrontend = config: mkDerivationMempool {
pname = "mempool-frontend";
buildPhase = ''
cd frontend
${sync} --chmod=+w ${nodeModules.frontend}/lib/node_modules .
patchShebangs node_modules
# sync-assets.js is called during `npm run build` and downloads assets from the
# internet. Disable this script and instead add the assets manually after building.
: > sync-assets.js
${lib.optionalString (config != {}) ''
ln -s ${builtins.toFile "mempool-frontend-config" (builtins.toJSON config)} mempool-frontend-config.json
''}
npm run build
# Add assets that would otherwise be downloaded by sync-assets.js
${sync} ${frontendAssets}/ dist/mempool/browser/resources
runHook postBuild
'';
installPhase = ''
${sync} dist/mempool/browser/ $out
runHook postInstall
'';
passthru = {
withConfig = mkFrontend;
assets = frontendAssets;
nodeModules = nodeModules.frontend;
};
};
mempool-rust-gbt = stdenvNoCC.mkDerivation rec {
pname = "mempool-rust-gbt";
inherit version src meta;
sourceRoot = "source/rust/gbt";
nativeBuildInputs = [
rustPlatform.cargoSetupHook
cargo
rustc
napi-rs-cli
];
cargoDeps = rustPlatform.fetchCargoVendor {
inherit src;
name = "${pname}-${version}";
inherit sourceRoot;
hash = "sha256-eox/K3ipjAqNyFt87lZnxaU/okQLF/KIhqXrX86n+qw=";
};
buildPhase = ''
runHook preBuild
# napi doesn't accept an absolute path as dest dir, so we can't directly write to $out
napi build --platform --release --strip out
runHook postBuild
'';
installPhase = ''
mv out $out
cp package.json $out
'';
passthru = { inherit cargoDeps; };
};
mempool-nginx-conf = runCommand "mempool-nginx-conf" {} ''
${sync} --chmod=u+w ${./nginx-conf}/ $out
${sync} ${src}/production/nginx/http-language.conf $out
'';
sync = "${rsync}/bin/rsync -a --inplace";
mkDerivationMempool = args: stdenvNoCC.mkDerivation ({
inherit version src meta;
nativeBuildInputs = [
makeWrapper
nodejs
rsync
];
phases = "unpackPhase patchPhase buildPhase installPhase";
} // args);
fetchFiles = { name, hash, fetcher }: stdenvNoCC.mkDerivation {
inherit name;
outputHashMode = "recursive";
outputHashAlgo = "sha256";
outputHash = hash;
nativeBuildInputs = [ curl cacert ];
buildCommand = ''
mkdir $out
cd $out
${builtins.readFile fetcher}
'';
};
meta = with lib; {
description = "Bitcoin blockchain and mempool explorer";
homepage = "https://github.com/mempool/mempool/";
license = licenses.agpl3Plus;
maintainers = with maintainers; [ erikarvstedt ];
platforms = platforms.unix;
};
}
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Fetch hash-locked versions of assets that are dynamically fetched via
# https://github.com/mempool/mempool/blob/master/frontend/sync-assets.js
# when running `npm run build` in the frontend.
#
# This file is updated by ./frontend-assets-update.sh
declare -A revs=(
["mempool/mining-pool-logos"]=53972ebbd08373cf4910cbb3e6421a1f3bba4563
)
fetchFile() {
repo=$1
file=$2
rev=${revs["$repo"]}
curl -fsS "https://raw.githubusercontent.com/$repo/$rev/$file"
}
fetchRepo() {
repo=$1
rev=${revs["$repo"]}
curl -fsSL "https://github.com/$repo/archive/$rev.tar.gz"
}
mkdir mining-pools
fetchRepo "mempool/mining-pool-logos" | tar xz --strip-components=1 -C mining-pools
@@ -1,47 +0,0 @@
# Settings adapted from
# https://github.com/mempool/mempool/blob/v3.2.1/production/nginx/server-common.conf
# see order of nginx location rules
# https://stackoverflow.com/questions/5238377/nginx-location-priority
# for exact / requests, redirect based on $lang
# cache redirect for 5 minutes
location = / {
if ($lang != '') {
return 302 $scheme://$host/$lang/;
}
try_files /en-US/index.html =404;
expires 5m;
}
# cache /<lang>/main.f40e91d908a068a2.js forever since they never change
location ~ ^/([a-z][a-z])/(.+\..+\.(js|css))$ {
try_files $uri =404;
expires 1y;
}
# cache everything else for 5 minutes
location ~ ^/([a-z][a-z])$ {
try_files $uri /$1/index.html /en-US/index.html =404;
expires 5m;
}
location ~ ^/([a-z][a-z])/ {
try_files $uri /$1/index.html /en-US/index.html =404;
expires 5m;
}
# cache /resources/** for 1 week since they don't change often
location /resources {
try_files $uri /en-US/index.html;
expires 1w;
}
# cache /main.f40e91d908a068a2.js forever since they never change
location ~* ^/.+\..+\.(js|css)$ {
try_files /$lang/$uri /en-US/$uri =404;
expires 1y;
}
# catch-all for all URLs i.e. /address/foo /tx/foo /block/000
# cache 5 minutes since they change frequently
location / {
try_files /$lang/$uri $uri /en-US/$uri /en-US/index.html =404;
expires 5m;
}
-66
View File
@@ -1,66 +0,0 @@
# Packaging adapted from fort-nix/nix-bitcoin commit 360e30fee.
# This local copy does not fetch or import nix-bitcoin.
{ lib
, stdenvNoCC
, nodejs_22
, nodejs-slim_22
, fetchNodeModules
, fetchurl
, makeWrapper
}:
let self = stdenvNoCC.mkDerivation {
pname = "rtl";
version = "0.15.8";
src = fetchurl {
url = "https://github.com/Ride-The-Lightning/RTL/archive/refs/tags/v${self.version}.tar.gz";
hash = "sha256-8XdGyORxB2dkZRB/Yl7zh+Quqo4L/Y0VmC6Brbr/hqU=";
};
passthru = {
nodejs = nodejs_22;
nodejsRuntime = nodejs-slim_22;
nodeModules = fetchNodeModules {
inherit (self) src nodejs;
# TODO-EXTERNAL: Remove `npmFlags` when no longer required
# See: https://github.com/Ride-The-Lightning/RTL/issues/1182
npmFlags = "--legacy-peer-deps";
hash = "sha256-oMqd6nLzS6iQ9w4z2yzpR2unA5qhOq5YdvfoS8IgYLY=";
};
};
nativeBuildInputs = [
makeWrapper
];
phases = "unpackPhase patchPhase installPhase";
# `src` already contains the precompiled frontend and backend.
# Copy all files required for packaging, like in
# https://github.com/Ride-The-Lightning/RTL/blob/master/dockerfiles/Dockerfile
installPhase = ''
dest=$out/lib/node_modules/rtl
mkdir -p $dest
cp -r \
rtl.js \
package.json \
frontend \
backend \
${self.nodeModules}/lib/node_modules \
$dest
makeWrapper ${self.nodejsRuntime}/bin/node "$out/bin/rtl" \
--add-flags "$dest/rtl.js"
runHook postInstall
'';
meta = with lib; {
description = "A web interface for LND, c-lightning and Eclair";
homepage = "https://github.com/Ride-The-Lightning/RTL";
license = licenses.mit;
maintainers = with maintainers; [ nixbitcoin erikarvstedt ];
platforms = platforms.unix;
};
}; in self
-479
View File
@@ -1,479 +0,0 @@
#!/usr/bin/env bash
#
# release-stable.sh
# Automated stable release script for Sovran_SystemsOS
#
# Features:
# - Detects latest tag and suggests next version
# - Pushes main → stable on Gitea
# - Creates annotated tag
# - Auto-updates CHANGELOG.md
# - Creates releases on both GitHub and Gitea via API
#
# Usage:
# ./scripts/release-stable.sh [version] [--message "text"]
#
# Requirements:
# - gh CLI (for GitHub releases)
# - GITEA_TOKEN env var (for Gitea releases)
#
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# Configuration
GITEA_REMOTE_DEFAULT="gitea"
GITHUB_REMOTE_DEFAULT="origin"
GITEA_API_URL="https://git.sovransystems.com/api/v1"
CHANGELOG_FILE="CHANGELOG.md"
# Auto-detect remotes
detect_remote() {
local preferred="$1"
local keyword="$2"
if git remote | grep -q -x "$preferred"; then
echo "$preferred"
return
fi
local found
found=$(git remote -v | grep -i "$keyword" | head -n 1 | awk '{print $1}')
if [[ -n "$found" ]]; then
echo "$found"
return
fi
echo "$preferred"
}
GITEA_REMOTE=$(detect_remote "$GITEA_REMOTE_DEFAULT" "sovransystems\|gitea")
GITHUB_REMOTE=$(detect_remote "$GITHUB_REMOTE_DEFAULT" "github")
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Sovran_SystemsOS Automated Stable Release Script ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
echo
# ─────────────────────────────────────────────────────────────────────────────
# Helper: Get latest tag
# ─────────────────────────────────────────────────────────────────────────────
get_latest_tag() {
local exclude="${1:-}"
if [[ -n "$exclude" ]]; then
git tag --list 'v*' --sort=-version:refname | grep -v -x -E "v?${exclude#v}|${exclude}" | head -1 || echo ""
else
git tag --list 'v*' --sort=-version:refname | head -1 || echo ""
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Helper: Suggest next version (patch bump)
# ─────────────────────────────────────────────────────────────────────────────
suggest_next_version() {
local current="$1"
if [[ -z "$current" ]]; then
echo "1.0.0"
return
fi
# Remove 'v' prefix
local ver="${current#v}"
IFS='.' read -r major minor patch <<< "$ver"
# Default: bump patch
echo "${major}.$((minor)).$((patch + 1))"
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 0: Fetch everything
# ─────────────────────────────────────────────────────────────────────────────
echo -e "${BLUE}Step 0: Fetching latest from all remotes...${NC}"
git fetch --all --tags 2>/dev/null || true
LATEST_TAG=$(get_latest_tag)
NEXT_VERSION=$(suggest_next_version "$LATEST_TAG")
echo -e " Latest tag : ${CYAN}${LATEST_TAG:-none}${NC}"
echo -e " Suggested : ${GREEN}v${NEXT_VERSION}${NC}"
echo
# ─────────────────────────────────────────────────────────────────────────────
# Get version from user or argument
# ─────────────────────────────────────────────────────────────────────────────
if [ $# -ge 1 ] && [[ "$1" != --* ]]; then
VERSION="$1"
shift
else
read -rp "Enter version to release (default: ${NEXT_VERSION}): " input_version
VERSION="${input_version:-$NEXT_VERSION}"
fi
# Strip 'v' if user added it
VERSION="${VERSION#v}"
TAG="v${VERSION}"
# ─────────────────────────────────────────────────────────────────────────────
# Get release message
# ─────────────────────────────────────────────────────────────────────────────
RELEASE_MESSAGE=""
while [[ $# -gt 0 ]]; do
case $1 in
--message|-m)
RELEASE_MESSAGE="$2"
shift 2
;;
*)
shift
;;
esac
done
if [[ -z "$RELEASE_MESSAGE" ]]; then
echo
read -rp "Enter release headline (or press Enter for default): " input_msg
if [[ -n "$input_msg" ]]; then
RELEASE_MESSAGE="$input_msg"
else
RELEASE_MESSAGE="Sovran_SystemsOS ${TAG} — Stable Release"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Helper: Generate categorized release notes from commit history
# Groups commits since the last tag into Keep-a-Changelog sections based on
# conventional-commit prefixes (feat/fix/docs/security/etc.) and keywords.
# ─────────────────────────────────────────────────────────────────────────────
generate_release_notes() {
local range="$1"
local added="" changed="" fixed="" security="" docs=""
while IFS= read -r subject; do
# Skip noise commits
case "$subject" in
"Initial plan"|"initial plan") continue ;;
"Merge pull request"*|"Merge branch"*) continue ;;
"chore: bump VERSION"*|"docs: update CHANGELOG"*) continue ;;
"Address code review"*|"Address review"*|"Address validation"*) continue ;;
esac
# Strip conventional-commit prefix for display
local clean
clean="$(echo "$subject" | sed -E 's/^(feat|fix|docs|chore|refactor|test|security|perf|style|ci|build)(\([^)]*\))?!?:[[:space:]]*//')"
# Capitalize first letter
clean="$(echo "${clean:0:1}" | tr '[:lower:]' '[:upper:]')${clean:1}"
case "$subject" in
security:*|security\(*) security+="- ${clean}"$'\n' ;;
feat:*|feat\(*) added+="- ${clean}"$'\n' ;;
fix:*|fix\(*|Fix\ *|fixed\ *) fixed+="- ${clean}"$'\n' ;;
docs:*|docs\(*) docs+="- ${clean}"$'\n' ;;
refactor:*|refactor\(*|chore:*|chore\(*|removed\ *|Updated\ *|updated\ *) changed+="- ${clean}"$'\n' ;;
test:*|test\(*) continue ;;
*) added+="- ${clean}"$'\n' ;;
esac
done < <(git log --no-merges --format='%s' "$range" 2>/dev/null | awk '!seen[$0]++')
local notes=""
if [[ -n "$added" ]]; then notes+=$'### Added\n'"$added"$'\n'; fi
if [[ -n "$changed" ]]; then notes+=$'### Changed\n'"$changed"$'\n'; fi
if [[ -n "$fixed" ]]; then notes+=$'### Fixed\n'"$fixed"$'\n'; fi
if [[ -n "$security" ]]; then notes+=$'### Security\n'"$security"$'\n'; fi
if [[ -n "$docs" ]]; then notes+=$'### Documentation\n'"$docs"$'\n'; fi
if [[ -z "$notes" ]]; then
notes=$'### Changed\n- Incremental stable updates\n'
fi
printf '%s' "$notes"
}
# Build the notes from commits since the previous tag (exclude the target tag if already present)
PREV_TAG=$(get_latest_tag "$TAG")
if [[ -n "$PREV_TAG" ]] && git rev-parse -q --verify "$PREV_TAG" >/dev/null; then
COMMIT_RANGE="${PREV_TAG}..HEAD"
else
COMMIT_RANGE="HEAD"
fi
echo
echo -e "${BLUE}Generating draft release notes from ${COMMIT_RANGE}...${NC}"
RELEASE_NOTES="$(generate_release_notes "$COMMIT_RANGE")"
# Let the user review/edit the generated notes before publishing
NOTES_FILE="$(mktemp "/tmp/release-notes-${TAG}.XXXXXX.md")"
{
echo "## Sovran_SystemsOS ${TAG}"
echo
echo "${RELEASE_MESSAGE}"
echo
echo "$RELEASE_NOTES"
echo "**Full changelog:** [CHANGELOG.md](https://github.com/naturallaw777/Sovran_SystemsOS/blob/main/CHANGELOG.md)"
} > "$NOTES_FILE"
echo -e " ${GREEN}✓${NC} Draft notes written to: ${CYAN}${NOTES_FILE}${NC}"
echo
echo "──────────────── Draft Release Notes ────────────────"
cat "$NOTES_FILE"
echo "──────────────────────────────────────────────────────"
echo
read -rp "Edit the notes before publishing? (y/N): " edit_confirm
if [[ "$edit_confirm" =~ ^[Yy]$ ]]; then
"${EDITOR:-nano}" "$NOTES_FILE"
RELEASE_NOTES="$(sed -n '/^###/,$p' "$NOTES_FILE" | sed '/^\*\*Full changelog/d')"
fi
RELEASE_BODY="$(cat "$NOTES_FILE")"
echo
echo -e "${YELLOW}════════════════════════════════════════════════════════════${NC}"
echo -e "${YELLOW} Preparing Release${NC}"
echo -e "${YELLOW}════════════════════════════════════════════════════════════${NC}"
echo " Version : ${TAG}"
echo " Message : ${RELEASE_MESSAGE}"
echo " Gitea Remote : ${GITEA_REMOTE}"
echo " GitHub Remote : ${GITHUB_REMOTE}"
echo
read -rp "Proceed with this release? (y/N): " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
# ─────────────────────────────────────────────────────────────────────────────
# Step 1: Push tested code to Gitea (stable & staging-dev)
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 1: Pushing tested code to Gitea (stable & staging-dev)...${NC}"
if git remote | grep -q -x "${GITEA_REMOTE}"; then
git push "${GITEA_REMOTE}" HEAD:stable --force-with-lease || echo -e " ${YELLOW}⚠ Push to ${GITEA_REMOTE} (stable) failed.${NC}"
git push "${GITEA_REMOTE}" HEAD:staging-dev --force-with-lease || echo -e " ${YELLOW}⚠ Push to ${GITEA_REMOTE} (staging-dev) failed.${NC}"
else
echo -e " ${YELLOW}⚠ Remote '${GITEA_REMOTE}' not found in git — skipping Gitea push.${NC}"
fi
# ─────────────────────────────────────────────────────────────────────────────
# Step 2: Create annotated tag
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 2: Creating annotated tag ${TAG}...${NC}"
git tag -f -a "${TAG}" -m "${RELEASE_MESSAGE}
- Stable release of Sovran_SystemsOS
- See CHANGELOG.md for full details" || true
if git remote | grep -q -x "${GITEA_REMOTE}"; then
git push "${GITEA_REMOTE}" "${TAG}" || echo -e " ${YELLOW}⚠ Tag push to ${GITEA_REMOTE} failed.${NC}"
else
echo -e " ${YELLOW}⚠ Remote '${GITEA_REMOTE}' not found in git — skipping Gitea tag push.${NC}"
fi
if git remote | grep -q -x "${GITHUB_REMOTE}"; then
git push "${GITHUB_REMOTE}" "${TAG}" || echo -e " ${YELLOW}⚠ Tag push to ${GITHUB_REMOTE} failed.${NC}"
fi
# Update VERSION file for ISO builds
echo "${VERSION}" > VERSION
git add VERSION
git commit -m "chore: bump VERSION to ${TAG} for ISO naming" || true
echo -e " ${GREEN}✓${NC} VERSION file updated to ${VERSION}"
# ─────────────────────────────────────────────────────────────────────────────
# Step 2b: Auto-update README.md ISO download references
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 2b: Updating README.md ISO references to ${VERSION}...${NC}"
README_FILE="README.md"
if [ -f "$README_FILE" ]; then
# Extract the version currently used in the README's ISO filenames,
# e.g. "Sovran_SystemsOS-1.0.6.iso" -> "1.0.6"
OLD_ISO_VER=$(grep -oE 'Sovran_SystemsOS-[0-9]+\.[0-9]+\.[0-9]+\.iso' "$README_FILE" | head -1 | sed 's/Sovran_SystemsOS-//; s/\.iso//')
if [ -n "$OLD_ISO_VER" ] && [ "$OLD_ISO_VER" != "$VERSION" ]; then
# Portable in-place edit (works with GNU sed and BSD/macOS sed)
sed "s/Sovran_SystemsOS-${OLD_ISO_VER}/Sovran_SystemsOS-${VERSION}/g" \
"$README_FILE" > "$README_FILE.tmp" && mv "$README_FILE.tmp" "$README_FILE"
UPDATED=$(grep -c "Sovran_SystemsOS-${VERSION}" "$README_FILE")
echo -e " ${GREEN}✓${NC} README.md: ${OLD_ISO_VER} -> ${VERSION} (${UPDATED} references updated)"
git add "$README_FILE"
git commit -m "docs: update README ISO download links to ${TAG}" || true
echo -e " ${GREEN}✓${NC} Committed README update"
else
echo -e " ${YELLOW}⚠${NC} README.md already references ${VERSION} or no versioned ISO links found — skipping"
fi
else
echo -e " ${YELLOW}⚠${NC} README.md not found — skipping"
fi
# ─────────────────────────────────────────────────────────────────────────────
# Step 3: Auto-update CHANGELOG.md
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 3: Updating ${CHANGELOG_FILE}...${NC}"
TODAY=$(date +%Y-%m-%d)
# Create new changelog entry from the generated notes (no placeholders)
NEW_ENTRY="## [${VERSION}] - ${TODAY}
${RELEASE_NOTES}
[${VERSION}]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/${TAG}
"
# Prepend to changelog (after the header)
if [ -f "$CHANGELOG_FILE" ]; then
# Backup
cp "$CHANGELOG_FILE" "${CHANGELOG_FILE}.bak"
# Insert new section after the first --- line
awk -v new_entry="$NEW_ENTRY" '
BEGIN { printed=0 }
/^---$/ && !printed {
print
print ""
print new_entry
printed=1
next
}
{ print }
' "$CHANGELOG_FILE" > "${CHANGELOG_FILE}.tmp" && mv "${CHANGELOG_FILE}.tmp" "$CHANGELOG_FILE"
rm -f "${CHANGELOG_FILE}.bak"
echo -e " ${GREEN}✓${NC} CHANGELOG.md updated with new section for ${TAG}"
else
echo -e " ${YELLOW}⚠${NC} CHANGELOG.md not found — skipping"
fi
# Commit the changelog update
git add "$CHANGELOG_FILE"
if git diff --cached --quiet; then
echo " (No changes to commit in changelog)"
else
git commit -m "docs: update CHANGELOG.md for ${TAG}"
echo -e " ${GREEN}✓${NC} Committed changelog update"
# Ask if user wants to push
echo
read -rp "Push the changelog commit to GitHub (main) and Gitea (staging-dev) now? (y/N): " push_confirm
if [[ "$push_confirm" =~ ^[Yy]$ ]]; then
if git remote | grep -q -x "${GITHUB_REMOTE}"; then
echo -e "${BLUE}Pushing changelog commit to GitHub main...${NC}"
if git push "${GITHUB_REMOTE}" HEAD:main; then
echo -e " ${GREEN}✓${NC} Changelog pushed to GitHub main (${GITHUB_REMOTE})"
else
echo -e " ${YELLOW}⚠ Push to GitHub main failed — verify credentials or remote name.${NC}"
fi
fi
if git remote | grep -q -x "${GITEA_REMOTE}"; then
echo -e "${BLUE}Pushing changelog commit to Gitea staging-dev...${NC}"
if git push "${GITEA_REMOTE}" HEAD:staging-dev; then
echo -e " ${GREEN}✓${NC} Changelog pushed to Gitea staging-dev (${GITEA_REMOTE})"
else
echo -e " ${YELLOW}⚠ Push to Gitea staging-dev failed.${NC}"
fi
fi
else
echo " (Changelog commit left local — remember to push later)"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Step 4: Create GitHub Release via gh CLI
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 4: Creating GitHub Release...${NC}"
if command -v gh &>/dev/null; then
title_suffix="${RELEASE_MESSAGE#Sovran_SystemsOS v* — }"
title_suffix="${title_suffix#Sovran_SystemsOS * — }"
if gh release create "${TAG}" \
--repo naturallaw777/Sovran_SystemsOS \
--title "${TAG} — ${title_suffix}" \
--notes-file "${NOTES_FILE}"; then
echo -e " ${GREEN}✓${NC} GitHub release created successfully"
else
echo -e " ${YELLOW}⚠${NC} GitHub release creation failed (check 'gh auth status' or create via web GUI)"
fi
else
echo -e " ${YELLOW}⚠${NC} gh CLI not found — skipping GitHub release (create via GitHub web GUI)"
fi
# ─────────────────────────────────────────────────────────────────────────────
# Step 5: Create Gitea Release via API
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 5: Creating Gitea Release via API...${NC}"
# ── Gitea Token Handling ─────────────────────────────────────────────────────
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo
echo -e "${YELLOW}GITEA_TOKEN is not set.${NC}"
read -rsp "Enter your Gitea API token (input will be hidden): " GITEA_TOKEN
echo
if [[ -z "$GITEA_TOKEN" ]]; then
echo -e " ${YELLOW}⚠${NC} No token provided — skipping Gitea release"
GITEA_TOKEN=""
fi
fi
if [[ -n "${GITEA_TOKEN:-}" ]]; then
GITEA_REPO="Sovran_Systems/Sovran_SystemsOS"
# Build JSON payload safely (release body may contain quotes/newlines)
if command -v jq &>/dev/null; then
PAYLOAD=$(jq -n \
--arg tag "${TAG}" \
--arg name "${TAG} — Stable Release" \
--arg body "${RELEASE_BODY}" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
else
PAYLOAD=$(python3 -c "import json,sys; print(json.dumps({'tag_name': sys.argv[1], 'name': sys.argv[1] + ' — Stable Release', 'body': open(sys.argv[2]).read(), 'draft': False, 'prerelease': False}))" "${TAG}" "${NOTES_FILE}")
fi
RESPONSE=$(curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "${PAYLOAD}" \
"${GITEA_API_URL}/repos/${GITEA_REPO}/releases" 2>/dev/null || echo "")
if echo "$RESPONSE" | grep -q '"id"'; then
echo -e " ${GREEN}✓${NC} Gitea release created successfully"
elif echo "$RESPONSE" | grep -q "write:repository"; then
echo -e " ${YELLOW}⚠${NC} Gitea token scope issue: your token requires the 'write:repository' scope (currently has write:package)."
echo " To fix: In Gitea, navigate to Settings → Applications → Manage Access Tokens and generate a token with 'write:repository'."
else
echo -e " ${YELLOW}⚠${NC} Gitea release creation failed or already exists"
echo " Response: $RESPONSE"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Final Summary
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ✅ Release ${TAG} completed successfully! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
echo
echo "Next manual steps (recommended):"
echo " • Build the installer ISO:"
echo " nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage"
echo " • Package, verify, and upload ISO to CDN:"
echo " ./scripts/upload-cdn.sh --upload"
echo " • Review and enhance the new section in CHANGELOG.md"
echo " • Push changes: git push ${GITHUB_REMOTE} HEAD:main && git push ${GITEA_REMOTE} HEAD:staging-dev"
echo " • Verify releases on both GitHub and Gitea"
echo
echo -e "${CYAN}Tag created: ${TAG}${NC}"
git show "${TAG}" --quiet
# Clean up temp notes file
rm -f "${NOTES_FILE}"
-168
View File
@@ -1,168 +0,0 @@
#!/usr/bin/env bash
#
# upload-cdn.sh
# Copies built ISO out of the nix store, generates versioned SHA-256 checksum,
# verifies it, and optionally uploads to CDN.
#
# Usage:
# ./scripts/upload-cdn.sh [--upload]
#
# EVERYTHING this script creates lives OUTSIDE the repository, even when you
# run it from inside the repo:
# - the nix build output symlink -> $ISO_OUT_DIR/result
# - the ISO and .sha256 files -> $ISO_OUT_DIR
# The repo working tree stays completely clean.
#
# Environment variables (optional):
# ISO_OUT_DIR - dir for the build symlink + ISO + checksum
# (default: ~/Sovran-builds)
# CDN_RSYNC_TARGET - rsync destination (e.g. user@server:/var/www/downloads/)
# CDN_RCLONE_REMOTE - rclone remote target (e.g. s3:my-bucket/downloads/)
# CDN_UPLOAD_CMD - custom upload command
#
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# Always operate from the repo root, no matter where the script is invoked from
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
# Read version from VERSION file
if [ ! -f VERSION ]; then
echo -e "${RED}Error: VERSION file not found in $REPO_ROOT.${NC}" >&2
exit 1
fi
VERSION=$(cat VERSION | tr -d '\n\r ')
ISO_NAME="Sovran_SystemsOS-${VERSION}.iso"
SHA_NAME="${ISO_NAME}.sha256"
# Output directory OUTSIDE the repo (override with ISO_OUT_DIR)
OUT_DIR="${ISO_OUT_DIR:-$HOME/Sovran-builds}"
mkdir -p "$OUT_DIR"
OUT_DIR="$(cd "$OUT_DIR" && pwd)"
ISO_PATH="$OUT_DIR/${ISO_NAME}"
SHA_PATH="$OUT_DIR/${SHA_NAME}"
RESULT_LINK="$OUT_DIR/result"
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Sovran_SystemsOS CDN ISO Packaging & Upload Tool ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
echo -e " Version : ${CYAN}${VERSION}${NC}"
echo -e " ISO : ${CYAN}${ISO_NAME}${NC}"
echo -e " Repo : ${CYAN}${REPO_ROOT}${NC} (left untouched)"
echo -e " Output : ${CYAN}${OUT_DIR}${NC} (outside the repo)"
echo
# Locate or build ISO
SRC_ISO=""
if [ -f "$RESULT_LINK/iso/${ISO_NAME}" ]; then
SRC_ISO="$RESULT_LINK/iso/${ISO_NAME}"
elif [ -f "$RESULT_LINK/iso/Sovran_SystemsOS.iso" ]; then
SRC_ISO="$RESULT_LINK/iso/Sovran_SystemsOS.iso"
elif [ -f "result/iso/${ISO_NAME}" ]; then
SRC_ISO="result/iso/${ISO_NAME}" # legacy: from an older in-repo build
elif [ -f "result/iso/Sovran_SystemsOS.iso" ]; then
SRC_ISO="result/iso/Sovran_SystemsOS.iso"
else
FOUND=$(find "$RESULT_LINK" result -name "*.iso" 2>/dev/null | head -n 1 || true)
if [ -n "$FOUND" ]; then
SRC_ISO="$FOUND"
fi
fi
if [ -z "$SRC_ISO" ] || [ ! -f "$SRC_ISO" ]; then
echo -e "${YELLOW}Built ISO not found. Building via Nix (result link goes outside the repo)...${NC}"
rm -f "$RESULT_LINK"
nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage \
--out-link "$RESULT_LINK"
if [ -f "$RESULT_LINK/iso/${ISO_NAME}" ]; then
SRC_ISO="$RESULT_LINK/iso/${ISO_NAME}"
else
FOUND=$(find "$RESULT_LINK" -name "*.iso" 2>/dev/null | head -n 1 || true)
if [ -n "$FOUND" ]; then
SRC_ISO="$FOUND"
fi
fi
fi
if [ -z "$SRC_ISO" ] || [ ! -f "$SRC_ISO" ]; then
echo -e "${RED}Error: Failed to locate built ISO.${NC}" >&2
exit 1
fi
echo -e "${BLUE}Step 1: Copying built ISO to output dir (outside repo)...${NC}"
cp -v "$SRC_ISO" "$ISO_PATH"
echo -e " ${GREEN}✓${NC} Copied to $ISO_PATH"
echo
echo -e "${BLUE}Step 2: Generating versioned SHA-256 checksum...${NC}"
sha256sum "$ISO_PATH" > "$SHA_PATH"
echo -e " ${GREEN}✓${NC} Generated $SHA_PATH"
cat "$SHA_PATH"
echo
echo -e "${BLUE}Step 3: Verifying checksum...${NC}"
sha256sum --check "$SHA_PATH"
echo -e " ${GREEN}✓${NC} Checksum verified successfully"
# Parse arguments for upload
DO_UPLOAD=0
for arg in "$@"; do
case $arg in
--upload)
DO_UPLOAD=1
shift
;;
esac
done
if [ "$DO_UPLOAD" -eq 1 ]; then
echo
echo -e "${BLUE}Step 4: Uploading to CDN...${NC}"
UPLOADED=0
if [ -n "${CDN_UPLOAD_CMD:-}" ]; then
echo -e " Running custom CDN_UPLOAD_CMD..."
eval "$CDN_UPLOAD_CMD"
UPLOADED=1
fi
if [ -n "${CDN_RSYNC_TARGET:-}" ]; then
echo -e " Uploading via rsync to ${CDN_RSYNC_TARGET}..."
rsync -avP "$ISO_PATH" "$SHA_PATH" "${CDN_RSYNC_TARGET}"
UPLOADED=1
fi
if [ -n "${CDN_RCLONE_REMOTE:-}" ]; then
echo -e " Uploading via rclone to ${CDN_RCLONE_REMOTE}..."
rclone copy "$ISO_PATH" "$SHA_PATH" "${CDN_RCLONE_REMOTE}"
UPLOADED=1
fi
if [ "$UPLOADED" -eq 0 ]; then
echo -e " ${YELLOW}⚠ Warning: --upload requested, but no upload method specified.${NC}"
echo -e " Set CDN_RSYNC_TARGET, CDN_RCLONE_REMOTE, or CDN_UPLOAD_CMD."
else
echo -e " ${GREEN}✓${NC} Upload complete."
fi
else
echo
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ✅ ISO packaging & verification complete! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
echo -e "Ready files (in ${CYAN}${OUT_DIR}${NC}, outside the repo):"
ls -lh "$ISO_PATH" "$SHA_PATH"
echo
echo "To upload to CDN, run:"
echo " ./scripts/upload-cdn.sh --upload"
echo "(configure CDN_RSYNC_TARGET, CDN_RCLONE_REMOTE, or CDN_UPLOAD_CMD)"
fi
-146
View File
@@ -1,146 +0,0 @@
{ nixpkgs, overlay-stable, system ? "x86_64-linux" }:
let
lib = nixpkgs.lib;
pkgs = import nixpkgs {
inherit system;
overlays = [ overlay-stable ];
};
normalize = s:
lib.replaceStrings [ "\n" "\\" " " ] [ " " "" " " ] s;
extractAfter = prefix: str:
let
match = builtins.match ".*${prefix} ([^ ]+).*" (normalize str);
in
if match == null then
throw "Unable to extract ${prefix} from: ${normalize str}"
else
builtins.head match;
extractFlagValue = flag: str:
let
match = builtins.match ".*${flag}=([^ ]+).*" (normalize str);
in
if match == null then
throw "Unable to extract ${flag} from: ${normalize str}"
else
builtins.head match;
config = (lib.nixosSystem {
inherit system;
modules = [
{ nixpkgs.hostPlatform = system; nixpkgs.overlays = [ overlay-stable ]; }
../modules/bitcoin
{
nix-bitcoin.generateSecrets = true;
nix-bitcoin.secretsDir = "/build/secrets";
services.btcpayserver.enable = true;
services.btcpayserver.lightningBackend = "lnd";
services.nbxplorer.dataDir = "/build/nbxplorer";
services.btcpayserver.dataDir = "/build/btcpayserver";
services.lnd.dataDir = "/build/lnd";
services.bitcoind.dataDir = "/build/bitcoind";
}
];
}).config;
nbxplorerPreStart = config.systemd.services.nbxplorer.preStart;
bitcoindPreStart = config.systemd.services.bitcoind.preStart;
btcpayExecStart = config.systemd.services.btcpayserver.serviceConfig.ExecStart;
btcpayWorkingDir = config.systemd.services.btcpayserver.serviceConfig.WorkingDirectory;
nbxplorerConfigPath = extractAfter "install -m 600" nbxplorerPreStart;
btcpayConfigPath = extractFlagValue "--conf" btcpayExecStart;
nbxplorerConfig = builtins.readFile nbxplorerConfigPath;
btcpayConfig = builtins.readFile btcpayConfigPath;
in
assert lib.assertMsg
(config.users.users.${config.services.btcpayserver.user}.home == config.services.btcpayserver.dataDir)
"btcpayserver user home must match btcpayserver dataDir";
assert lib.assertMsg
(config.users.users.${config.services.nbxplorer.user}.home == config.services.nbxplorer.dataDir)
"nbxplorer user home must match nbxplorer dataDir";
assert lib.assertMsg
(config.nix-bitcoin.secrets.bitcoin-HMAC-btcpayserver.user == config.services.bitcoind.user)
"bitcoin-HMAC-btcpayserver must be owned by bitcoind";
assert lib.assertMsg
(config.nix-bitcoin.secrets.bitcoin-rpcpassword-btcpayserver.user == config.services.bitcoind.user)
"bitcoin-rpcpassword-btcpayserver must be owned by bitcoind";
assert lib.assertMsg
(config.nix-bitcoin.secrets.bitcoin-rpcpassword-btcpayserver.group == config.services.nbxplorer.group)
"bitcoin-rpcpassword-btcpayserver must be group-readable by nbxplorer";
assert lib.assertMsg
(!(lib.elem config.services.nbxplorer.group config.users.users.${config.services.btcpayserver.user}.extraGroups))
"btcpayserver must not receive the nbxplorer group";
assert lib.assertMsg
(lib.elem "nix-bitcoin-secrets.target" config.systemd.services.nbxplorer.after)
"nbxplorer must wait for nix-bitcoin-secrets.target";
assert lib.assertMsg
(config.systemd.services.nbxplorer.serviceConfig.MemoryDenyWriteExecute == false)
"nbxplorer needs MemoryDenyWriteExecute = false";
assert lib.assertMsg
(config.systemd.services.btcpayserver.serviceConfig.MemoryDenyWriteExecute == false)
"btcpayserver needs MemoryDenyWriteExecute = false";
assert lib.assertMsg
(lib.hasInfix "network=mainnet" nbxplorerConfig
&& lib.hasInfix "btcrpcuser=btcpayserver" nbxplorerConfig
&& lib.hasInfix "btcnodeendpoint=127.0.0.1:8335" nbxplorerConfig
&& lib.hasInfix "bind=127.0.0.1" nbxplorerConfig
&& lib.hasInfix "port=24444" nbxplorerConfig
&& lib.hasInfix "postgres=User ID=nbxplorer;Host=/run/postgresql;Database=nbxplorer" nbxplorerConfig)
"nbxplorer base config must contain the expected non-secret settings";
assert lib.assertMsg
(lib.hasInfix "btcexplorerurl=http://127.0.0.1:24444/" btcpayConfig
&& lib.hasInfix "btcexplorercookiefile=/build/nbxplorer/Main/.cookie" btcpayConfig)
"btcpayserver config must contain btcexplorerurl and btcexplorercookiefile";
assert lib.assertMsg
(lib.hasSuffix "/lib/btcpayserver" btcpayWorkingDir)
"btcpayserver WorkingDirectory must end with /lib/btcpayserver";
assert lib.assertMsg
(!lib.hasInfix "/build/btcpayserver/settings.config" btcpayExecStart
&& lib.hasInfix "--datadir='/build/btcpayserver'" btcpayExecStart)
"btcpayserver must use a deterministic config file plus --datadir";
assert lib.assertMsg
(lib.hasInfix "network=mainnet" btcpayConfig
&& lib.hasInfix "bind=127.0.0.1" btcpayConfig
&& lib.hasInfix "port=23000" btcpayConfig
&& lib.hasInfix "btcexplorerurl=http://127.0.0.1:24444/" btcpayConfig
&& lib.hasInfix "explorer.postgres=User ID=nbxplorer;Host=/run/postgresql;Database=nbxplorer" btcpayConfig
&& lib.hasInfix "postgres=User ID=btcpayserver;Host=/run/postgresql;Database=btcpayserver" btcpayConfig
&& lib.hasInfix "btclightning=type=lnd-rest;server=https://127.0.0.1:8080/;macaroonfilepath=/run/lnd/btcpayserver.macaroon;certfilepath=/build/secrets/lnd-cert" btcpayConfig)
"btcpayserver config must preserve BTCPay, NBXplorer, database, and LND settings";
assert lib.assertMsg
(lib.hasInfix "readValidatedRpcHmac()" bitcoindPreStart
&& lib.hasInfix ''if [[ ! -e "$hmacFile" ]]; then'' bitcoindPreStart
&& lib.hasInfix ''if [[ ! -r "$hmacFile" ]]; then'' bitcoindPreStart
&& lib.hasInfix ''if [[ -z "$hmacPayload" ]]; then'' bitcoindPreStart
&& lib.hasInfix ''^[[:xdigit:]]+\$[[:xdigit:]]+$'' bitcoindPreStart
&& lib.hasInfix ''Bitcoin RPC HMAC file has invalid format'' bitcoindPreStart
&& lib.hasInfix ''hmacPayload="$(readValidatedRpcHmac '/build/secrets/bitcoin-HMAC-btcpayserver')" || exit 1'' bitcoindPreStart)
"bitcoind preStart must validate missing, unreadable, empty, and malformed HMAC files";
pkgs.runCommand "bitcoin-btcpay-hardening" {} ''
mkdir -p /build/secrets /build/nbxplorer
printf '%s' 'first-password' > /build/secrets/bitcoin-rpcpassword-btcpayserver
bash -euo pipefail -c ${lib.escapeShellArg nbxplorerPreStart}
test "$(stat -c '%a' /build/nbxplorer/settings.config)" = "600"
test "$(grep -c '^btcrpcuser=' /build/nbxplorer/settings.config)" = "1"
test "$(grep -c '^btcrpcpassword=' /build/nbxplorer/settings.config)" = "1"
test "$(grep -c '^postgres=' /build/nbxplorer/settings.config)" = "1"
printf '%s' 'rotated-password' > /build/secrets/bitcoin-rpcpassword-btcpayserver
bash -euo pipefail -c ${lib.escapeShellArg nbxplorerPreStart}
test "$(grep -c '^btcrpcuser=' /build/nbxplorer/settings.config)" = "1"
test "$(grep -c '^btcrpcpassword=' /build/nbxplorer/settings.config)" = "1"
test "$(grep -c '^postgres=' /build/nbxplorer/settings.config)" = "1"
! grep -q 'first-password' /build/nbxplorer/settings.config
grep -q 'rotated-password' /build/nbxplorer/settings.config
touch "$out"
''
File diff suppressed because it is too large. Load diff