Updater/rebuild self-heal:
- Add a shared run_step wrapper used by both the update and rebuild
scripts. On the first failure matching a transient fetch/cache signature
(truncated tarball, corrupt NAR, hash mismatch, network timeout,
interrupted download), clear Nix's fetch caches and repair the store,
then retry once. Real config errors do not match and still fail loudly.
- The kernel-change boot fallback in the rebuild path is also wrapped.
- Fixes the reported 'cannot read file from tarball: Truncated tar archive
detected' failure, which a plain re-run cannot clear because Nix reuses
the corrupt cached archive.
Failed-update recovery / reporting:
- check_for_updates() now compares the running Hub version against the
branch VERSION, so a failed 'nix flake update' (lock advanced but no
generation staged) can no longer masquerade as 'up to date' and block
retries.
- /api/updates/check surfaces a persistent 'failed' state; /api/updates/run
never blocks a retry after a failure.
- Dashboard shows a red 'Update failed - click to retry' tile; the modal
offers a Retry Update button and stops offering a reboot on failure.
- Replace 3 long instruction bullets with 1 plaintext line
- Shorten intro (drop long parenthetical on IP location)
- Remove 'how to confirm it worked' note — if router applies rules, ports are open
- Keep port table intact (40000-40099 current set)
User-facing polish to match the element-calling port changes:
- helpers.js: port-forward guide example now shows the 40000-40099 range
instead of the removed 30000-40000.
- versions.json: livekit dev fallback 1.5.2 -> 1.13.6 (the Nix-generated
version in sovran-hub.nix already derives from pkgs.livekit.version).
- server.py: docstring/comment examples reference 40000-40099.
- CHANGELOG.md: record the calling changes under [Unreleased].
The tile/modal port tables themselves are data-driven from server.py's
port_requirements / _PORTS_ELEMENT_CALLING (updated in the previous commit),
so no further UI changes are needed.
Root cause of 'calls fail on Wi-Fi but work on mobile data': LiveKit only
advertised the public/WAN IP (rtc.node_ip), so LAN clients had to hairpin
through the router for media. Fixes and cleanup:
- rtc.advertise_internal_ip: true — also advertise the primary interface's
LAN host candidate, so Wi-Fi callers connect directly (no hairpin).
- Drop rtc.port_range_start/end (30000-40000) and keep the single UDP mux
(udp_port: 7882). In LiveKit 1.13.x the range takes precedence over
udp_port, so media was actually spread over 10000 ports.
- Drop turn.tls_port: 5349 — LiveKit advertises turns:<domain>:443 to clients
regardless of tls_port, so a 5349 TURN/TLS listener was unreachable dead
config (and needless attack surface).
- Pin TURN relay allocation to 40000-40099 (disjoint from the media mux) and
open/forward that range; the old default overlapped RTC media.
- turn.allow_restricted_peer_cidrs with the LAN subnet derived from the
primary interface: without it the relay refuses to deliver to the private
LAN host candidate and its final hop would fall back to WAN hairpin.
- Update Hub port guidance (server.py) to the new list.
The Hub vendored a second copy of the NWC stack
(app/sovran_systemsos_web/nwc_hub_manager.py, nwc_audit.py,
nwc_lnurl_service.py, nwc_wallet_cli.py) and built its own nwc-wallet /
nwc-lnurl binaries from it. That copy drifted from the pinned Alby Hub
API contract (appId vs toAppId) and duplicated code that Sovran_Bitcoin
already ships and fixes.
Changes:
- Delete the four vendored modules; server.py now imports the canonical
implementation directly (from sovran_nwc import nwc_hub_manager) from
the sovran-nwc package (pkgs.sovran-bitcoin.nwc). API fixes in
Sovran_Bitcoin now propagate to the Hub web app automatically.
- sovran-hub-web launcher: add <sovran-nwc>/lib/sovran-nwc to
sys.path so the import resolves.
- Stop shipping nwc-wallet / nwc-lnurl binaries from sovran-hub-web:
the flake already provides them (env-wrapped nwc-wallet with
NWC_* vars via albyhub.nix, and nwc-lnurl.service via lnurl.nix).
Requires a Sovran_Bitcoin rev containing the toAppId fix (and the
LNURL module audit-log fix); bump the flake input afterwards:
nix flake update sovran-bitcoin
Test:
- nixos-rebuild switch
- Hub Wallet Connections tab still lists/creates wallets
- nwc-wallet list works from the operator shell
- journalctl -u nwc-lnurl shows no import/contract errors
Fix the version metadata generation in sovran-hub.nix for Alby Hub, RTL, and Mempool. Previously, the build would incorrectly fall back to older upstream nixpkgs versions because the package names exist upstream, despite the OS deploying custom vendored forks locally. This replaces the fragile checks with direct evaluations of the local packages.
Also updates the development fallback versions.json to reflect the current vendored Alby Hub version (1.24.0).
The public IP was previously detected independently in three places,
each contacting a different third party: the Hub (HTTPS echo via
api.ipify.org / ifconfig.me / icanhazip.com on every API call and
background tick), DDNS (myip.opendns.com via OpenDNS), and LiveKit
(embedded STUN). Consolidate into a single detector with one shared
cache so every consumer reads the same value with minimal exposure.
- add modules/core/public-ip.nix: installs /var/lib/sovran/public-ip.py
(pure Python stdlib, no new deps) writing /var/lib/secrets/external-ip
- detection chain (first success wins): explicit pin, fresh cache
(default TTL 300s), STUN binding request over UDP (one packet, no
metadata), DNS myip.opendns.com query, then OPT-IN HTTPS echo
(publicIP.httpsEcho, empty by default — never contacted unless listed)
- privacy: while the cache is fresh zero third parties are contacted;
at most one party learns the IP per refresh interval, via the least
exposing mechanism available
- hub (server.py): _get_external_ip() now reads the shared detector /
cache instead of calling ipify/ifconfig/icanhazip directly
- ddns (njalla.nix): use the shared detector instead of a separate
OpenDNS dig; allow the hardened service to write /var/lib/secrets
- element-calling: livekit-turn-setup falls back to the shared
detector on cold boot; add LiveKit webhooks to lk-jwt-service
(sfu_webhook) so abrupt disconnects are cleaned up immediately;
set LIVEKIT_SANITY_CHECK_INTERVAL_SECONDS=60 as a missed-webhook
guard; drop the dead services.livekit.settings block and set
openFirewall=false (Caddy fronts the SFU; no public 7880/tcp)
- new options: sovran_systemsOS.publicIP.{stunServer,stunPort,
dnsResolver,httpsEcho,cacheTTL}
NixOS already knows whether a reboot is pending: /nix/var/nix/profiles/
system vs /run/current-system. Marker files only the Hub's own updater
wrote desynced for terminal-updated machines (and markers from older
updaters could never clear), pinning the badge on forever. Reconcile
REBOOT_REQUIRED against live state on every read; the stale marker
self-heals to IDLE. The .generation marker write is now informational.
The full-system updater runs as a detached systemd service and can finish
successfully even when the browser loses its status connection. In that
case the update log and status file correctly report REBOOT_REQUIRED, but
the Hub modal can remain on "Updating..." with its controls disabled.
There were four independent ways for the frontend to get stuck:
* update status fetches had no deadline, so a request that stayed pending
never rejected and never advanced the existing failure counter;
* setInterval started async polls without waiting for the previous poll,
allowing slow requests to overlap and responses to arrive out of order;
* each log chunk used textContent +=, replacing the complete and growing
Nix build log every two seconds, which could stall browser rendering and
was especially visible over RDP; and
* page reload, tab resume, and RDP reconnect did not reattach the modal to
the update status persisted by the backend.
This produced a dangerous UX mismatch: the machine had a fully staged
NixOS generation and was ready to reboot, while the Hub continued telling
the user that the update was still running.
Bound status requests with AbortController, prevent overlapping polls, and
replace the endless spinner after sustained failures with an explicit
"Update status unavailable" state and Retry Status action. Reconcile state
immediately on focus, visibility, online, page startup, and before starting
a new update. Use no-store requests and render verbose logs incrementally
with a bounded visible tail while retaining the complete report in memory.
Apply the same timeout and single-flight protection to rebuild polling.
Record the exact generation produced by `nixos-rebuild boot`. The Hub now
keeps REBOOT_REQUIRED visible until that generation matches
/run/current-system, then clears the marker after reboot. For an update
started by an older updater that did not write the marker, recover the
staged generation from the final nixos-rebuild log line. The dashboard
sidebar also distinguishes update-in-progress and restart-required states.
Regression coverage verifies generation marker/log recovery, pre- versus
post-reboot detection, request timeout wiring, single-flight polling,
connection-loss UX, RDP/tab resume reconciliation, bounded log rendering,
page-reload recovery, and JavaScript syntax.
Validation:
* python3 -m unittest discover -s tests -p 'test_*.py' -v (170 passed)
* node --check app/sovran_systemsos_web/static/js/*.js
* python3 -m py_compile for changed Python modules
* git diff --check
A Nix evaluation was not available in the development sandbox; the NixOS
module should still be evaluated and built in CI or on a test machine before
release.
The LND-only rewrite of lndconnect.nix shipped a wrapper Zeus cannot
use: unknown flags (--cert/--macaroon), a non-existent onion path
(free/lnd.onion), and a REST hidden service that collided with LND's
P2P onion. Restore the nix-bitcoin contract — dedicated lnd-rest
onion on port 8080, --nocert over Tor, admin macaroon in the URI —
and only persist a valid lndconnect:// URI for the Hub QR.
- Replace plain-text write of new_root_password in api_security_reset()
with scrypt-hashed storage via _hash_password(), matching how the free
password is already handled.
- Return new_root_password in the API response so the user sees it once
before it is irreversibly hashed on disk.
- Teach _resolve_credential() to detect scrypt hashes and display a
human-readable placeholder instead of raw hex in the Hub credentials UI.
- Harden root-password-setup systemd service: if the secrets file already
contains a hash, skip chpasswd so a manual restart never sets the hash
as the literal login password.
Keeps the modal-matching neutral pill styling; the badge now reads
"v1.0.5" as plain uniform text instead of the bare number.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The header badge now uses the exact visual treatment of
.creds-title-version-badge from modals.css: neutral translucent pill
(rgba(255,255,255,0.06) background, 0.08 white border, 12px radius,
--text-secondary text, 0.72rem/600/2px-10px padding), no hover
animation, and a bare version number like the modals show (e.g.
"2.8.4") instead of the green pill with a "v" prefix.
The badge markup flattens to a single text span; the .version-label
and .version-number rules are removed.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Mirrors the service modal pattern where the version badge sits to the
right of the title: .title-group switches from a centered column
(badge under the title, PR #377) to a centered row with the badge
vertically centered directly after "Sovran_SystemsOS Hub".
flex-wrap keeps a graceful fallback: on very narrow screens the badge
wraps below the title, centered — the previous stacked look.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Fix QR hint for zeus-connect-setup.service to use correct LND REST steps
- Expand 'How to Connect' instructions with title/intro/steps/note
- Add matching styled guide block in credentials modal
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The Manual Backup screen in the Hub always listed the Node / Server +
Desktop items (nix-bitcoin secrets, /var/lib system service data, and the
database/blockchain caveat), and the backup script mirrored /var/lib and
counted it in the free-space estimate even on the Desktop Only role.
Desktop Only systems run no server or Bitcoin services and have no
internal second data drive, so none of that applies.
Hub UI (support.js):
- 'What gets backed up' is role-aware: Desktop Only lists only the NixOS
configuration (/etc/nixos) and home directory (/home)
- Database/blockchain note hidden on Desktop Only
- Intro copy corrected: external USB copy is a second location on
Desktop Only (no internal second drive); third-location wording kept
for Node / Server + Desktop
Backup script (sovran-hub-backup.sh):
- Desktop Only runs 2 stages (1/2 /etc/nixos, 2/2 /home); secrets and
/var/lib stages no longer run on that role
- Free-space estimate skips /var/lib on Desktop Only
- BACKUP_MANIFEST.txt sources/exclusions/limitations/restore guidance
and blockchain note are role-aware
- Completion message role-aware ('second, external location' on
Desktop Only); header comments updated
Node and Server + Desktop behavior is unchanged. Added CHANGELOG entry.
Verified: bash -n / node --check, 6 role-detection cases, manifest
generation for both role groups (non-desktop output identical to
before), and simulated UI renders for all three roles.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>