Compare commits

...
1545 Commits
Author SHA1 Message Date
naturallaw777 1d59a995e8 chore(flake): update sovran-bitcoin with btcpayserver RPC whitelist fix 2026-10-18 17:15:44 -05:00
Sovran Systems f1f0bf0d11 fix(bitcoin): expand btcpayserver RPC whitelist for NBXplorer
- Ensure services.bitcoind.rpc.users.btcpayserver.rpcwhitelist includes
  public.rpcwhitelist and NBXplorer capability-scan/wallet RPC methods
  (validateaddress, testmempoolaccept, signrawtransactionwithkey,
  signrawtransactionwithwallet, getblockfrompeer, generatetoaddress,
  getindexinfo, createwallet, loadwallet, importdescriptors, etc.).
2026-10-18 17:15:03 -05:00
naturallaw777 7b5f25d599 chore(flake): update sovran-bitcoin with electrs 0.12.0 fix 2026-10-18 16:47:38 -05:00
Sovran Systems 497ac49cfd fix(bitcoin): support electrs 0.12.0 (drop --daemon-p2p-addr, enable bitcoind rest=1)
- Ensure bitcoind enables rest=1 and electrs.service omits the removed
  --daemon-p2p-addr flag even before Sovran_Bitcoin flake input is re-locked.
- Clean up pre-0.12.0 (FixedPrefix.8) RocksDB directories in electrs
  preStart so bindex 0.12.0 reindexes cleanly.
2026-10-18 16:47:36 -05:00
naturallaw777 8fd0318083 chore(release): prepare v1.2.1 2026-10-18 16:09:46 -05:00
Sovran Systems aed1ed5bd6 chore: update flake.lock (nixpkgs, Sovran_Bitcoin 1.1.1, Bisq2 2.1.14) and integration
- Update flake.lock inputs:
  - sovran-bitcoin: b0da63b -> 50a8b76 (1.1.1 with RTL 0.15.13 and Nix-native sovran-nwc 2.0.0)
  - nixpkgs (nixos-unstable): c59305b -> fe13242 (Caddy 2.11.7, Tor 0.4.9.14, GNOME Remote Desktop 50.2, Bitcoin Core 31.1, Electrs 0.12.0, LND 0.21.4-beta, BTCPay Server 2.4.5, Matrix Synapse 1.162.0, LiveKit 1.13.8, Vaultwarden 1.37.4, Haven 1.2.2)
  - nixpkgs-stable (nixos-26.05): 78e9c78 -> 26f4107
  - btc-clients: 99b0442 -> 990d29e (Bisq 1.10.9, Bisq2 2.1.14, Sparrow 2.5.5)
  - nixvim: 5980a62 -> 453dc9c
- flake.nix: replace deprecated pkgs.system with pkgs.stdenv.hostPlatform.system for btc-clients packages
- modules/sovran-bitcoin-integration.nix: guard config.networking.domain against null/empty when both haven and nwc-wallets features are enabled
- app/sovran_systemsos_web/versions.json: sync static fallback service versions with updated flake inputs
- CHANGELOG.md: document updates under [Unreleased]
2026-10-18 15:36:45 -05:00
Sovran_SystemsOS contributor 8f5e5394e6 mullvad: install the client on the Desktop Role only
The Mullvad VPN app and its daemon now ship with Desktop Only, as the stock
NixOS service with nothing configured on top of it: no local-network boot
service, no DNS or firewall edits of our own, no Hub tile, no Feature Manager
card, no toggles. The account, the relay, the kill switch and the app's other
switches belong to the app.

modules/mullvad.nix is the whole change to the system: a role gate around

  services.mullvad-vpn = {
    enable = true;      # daemon, pkgs.mullvad
    gui.enable = true;  # app, pkgs.mullvad-vpn
  };

Server + Desktop and Bitcoin Node are machines that serve; a VPN client with its
own routing and DNS policy is not something their services asked for, so only
roles.desktop builds it. Every other option stays at the distribution's default
(early boot blocking off, the mullvad-exclude wrapper on), and the only
consequences are the ones the NixOS module already defines — the tun module, and
systemd-resolved with NetworkManager pointed at it while the client is enabled.

The Hub and the desktop shell are untouched: no tile, no feature card, no
service description, no icon, no GNOME folder edit. An app that is simply
installed has no decision to put in front of the user, and no state to report.

README, SECURITY.md (what the app's own kill switch and DNS behaviour mean, so
the change in network behaviour is not a surprise), CHANGELOG, the custom.nix
template (the mkForce line that removes it) and the installer's Desktop card
document it as an app rather than a service.

tests/test_mullvad_desktop_only.py guards the gate and the restraint: the client
builds on Desktop and nowhere else, the module adds no services, no DNS or
firewall edits and no packages, the Hub and desktop shell never mention it, and
it ships no tile icon.

Checked by evaluating the real module against the revision flake.lock pins
(3d23ea05a8a3) with eval-config.nix, four cases: Desktop -> daemon and GUI on,
one unit (mullvad-daemon), mullvad + mullvad-vpn in systemPackages, every other
option at the distribution default; Bitcoin Node and Server + Desktop -> nothing
at all, not even the tun module or resolved; Desktop with
`services.mullvad-vpn.enable = lib.mkForce false;` -> no unit, no package.
2026-10-16 13:07:29 -05:00
naturallaw777 36bd2b4e78 password-model.nix: fix Nix string escaping in the generate fallback
The word-picking one-liner used `eval echo \${...}`, but in a Nix
indented string a backslash does not escape `${` -- it starts
interpolation, so evaluation failed with "syntax error, unexpected
invalid token". Replace the eval/positional-parameter trick with a
plain `shuf` pipeline that contains no `${` at all.

Found by nixos-rebuild boot on staging-dev. The shell logic itself was
tested; the file had not been parse-checked by Nix. Re-scanned the
whole module: every remaining `${` is an intended interpolation and
every `''` is a string delimiter.
2026-10-16 12:39:01 -05:00
naturallaw777 4a25ef24d1 password & keyring cleanup: one password, one truth, no prompts
The Hub, GDM and sudo now share /etc/shadow as the single source of
truth. /var/lib/secrets/{free,root}-password become root-only display
copies, written only by system_password.py and verified against shadow
before being shown. The GNOME login keyring is decoupled from the login
password (blank master password, pam_gnome_keyring off), so nothing
needs re-keying or wiping when the password changes and the keyring
never pops an unlock prompt.

- add app/sovran_systemsos_web/system_password.py: the single module
  that verifies / sets / displays the free + root passwords and drives
  the legacy-migration onboarding step (same endpoints and UI)
- add modules/password-model.nix: PAM gnome-keyring off, blank
  login.keyring seed with a one-time migration of old encrypted
  keyrings, first-boot-only password application (no more boot-revert),
  LIBCRYPT for the Hub
- server.py: login verifies against shadow and heals stale display
  copies; change-password takes the current password; security reset
  sets both passwords through the same writer (root is shown as a
  password again, not a hash); all keyring wipes removed
- credentials.nix: drop root-password-setup, free-password-setup and
  free-password-migration (replaced by password-model.nix) and the
  keyring wipe in change-free-password
- configuration.nix: drop enableGnomeKeyring and the keyring tmpfiles
  hack (replaced by password-model.nix)
- service-detail.js: "Current password" field; credentials changed
  outside the Hub render with a hint instead of a stale value

Installed machines keep their exact passwords: the first-boot services
no-op once an account has a real hash, and the upgrade never writes
/etc/shadow. The legacy-migration onboarding step and "reset all
passwords" keep working.
2026-10-16 12:25:16 -05:00
Sovran Systems 5e56b07465 Fix README formatting and clarify service exposure
Corrected formatting and clarified public service exposure details.
2026-10-02 11:58:57 -05:00
Sovran Systems 2867954808 Update security details in SECURITY.md
Clarified access and security details for the Hub, including SSH Remote Access and support access.
2026-10-02 11:56:14 -05:00
Sovran Systems c20a47bab7 Enhance README formatting and emphasis on features
Updated README to emphasize key features and improve clarity.
2026-10-02 11:55:19 -05:00
Sovran Systems 708b502a17 Fix formatting and clarify server access instructions 2026-10-02 11:36:35 -05:00
Sovran Systems cc566016d1 Revise SECURITY.md for clarity on Hub operations
Updated security model details for Hub operation and access control.
2026-10-02 11:35:53 -05:00
naturallaw777 3a87302645 chore(release): prepare v1.2.0 2026-10-02 02:40:39 -05:00
Security Fix b79a6fd7f2 ssh: don't open port 22 for the loopback-only sshd
sshd-localhost.nix turns sshd on for every role so that "ssh root@localhost"
works, and binds it to 127.0.0.1 only ("zero network exposure", as its
own comment says, and as sshd.nix expects: the sshd feature "extends this
to 0.0.0.0 and opens port 22 on the firewall when the user enables remote
SSH").

It never turned off services.openssh.openFirewall, which NixOS defaults
to true and applies whether or not sshd listens on the ports. So port 22
was open in the firewall on every role, Desktop Only included, with
nothing behind it. No listener means no live exposure today; it does mean
the firewall was not saying what the documentation says, and the day
something does bind 22 on a wider address (a listenAddresses change, a
second daemon) it would be reachable without anyone having opened it.

openFirewall is now mkDefault false there. Nothing that wants SSH
published loses it: sshd.nix (feature sshd) and remote-deploy.nix both
add 22 explicitly, and only when enabled. Found by evaluating the real
module set; grepping for allowedTCPPorts does not see a NixOS default.

Evaluated with nix eval, TCP firewall ports:

                            before     after
  Server + Desktop          22 80 443 3051 8937   80 443 3051 8937
  Bitcoin Node Only         22 3051 8937 60847    3051 8937 60847
  Desktop Only              22                    (none)
  Desktop + features.sshd   22                    22
  Desktop + deploy.enable   22 3389               22 3389

and sshd's listen addresses are unchanged: 127.0.0.1 by default,
127.0.0.1 and 0.0.0.0 with the sshd feature. Desktop Only now opens no
TCP port at all; UDP 5353 (mDNS) is the only port open there, and
SECURITY.md says so.

Add tests/test_ssh_exposure.py.
2026-10-02 02:24:29 -05:00
Security Fix 3694ea6489 bitcoin: drop the stray UDP 3051 firewall rule
allowedUDPPorts was set to [ 3051 ] alongside allowedTCPPorts, which looks
like it was copied from the line above. Caddy serves Ride The Lightning
over TCP on 3051; nothing listens for UDP there, so the rule only opened a
port for no reason.

The comment above the pair also said "Hub management port"; 3051 is RTL.

Evaluated with nix eval, UDP firewall ports per role: Server + Desktop
80 443 3051 5353 -> 80 443 5353, Bitcoin Node Only 3051 5353 -> 5353,
Desktop Only 5353. (80 and 443 are Caddy's; 5353 is mDNS.)
2026-10-02 02:24:29 -05:00
Security Fix 6987d9bf2c installer: raise generated password entropy from ~23 to ~33 bits
generate_diceware_password() built the password from 3 words out of a 96
word list plus a single digit: 96^3 x 10 = 8,847,360 combinations, about
23 bits.

That one password is the desktop login, the 'free' account password, and
the only thing standing in front of the Hub, which runs as root and
displays the root password, the SSH passphrase, the RTL password and the
Vaultwarden admin token. 23 bits is thin for something that valuable, and
the rate limiting in front of it was weaker than intended (see "hub: make
the login lockout that LOGIN_FAIL_MAX described").

Now 4 words plus 2 digits: 96^4 x 100 = 8,493,465,600, about 33 bits,
for the cost of one more word to write down.

- iso/installer.py: generate_diceware_password().
- modules/credentials.nix: the three fallback generators in
  root-password-setup, free-password-setup and free-password-migration,
  so a machine provisioned without the installer gets the same strength.

Affects new installs only; existing passwords are untouched.

Checked by running the real thing. The installer function was exercised
2000 times: 96 words in the list, always word-word-word-word-NN, 33.0
bits. For the three services, the generator lines were taken from the
script the evaluated module really produces (nix eval on the nixpkgs
revision flake.lock pins) and run 1500 times each: 96 words in each
list, always word-word-word-word-NN, every two-digit suffix from 00 to 99
seen, and no repeated password.
2026-10-02 02:24:29 -05:00
Security Fix ebcc17ae3c caddy: stop filtering the RTL and Mempool sites by client address
c33457f put an address check (sovran_lan_only) on the Hub, RTL and
Mempool sites. It was written for ports 80/443 being forwarded and a
Host header selecting a site, and that only ever applied to the Hub. RTL
and Mempool are sites on ports of their own (:3051, :60847): a request
on 80/443 cannot select them, whatever Host it carries.

Checked with Caddy 2.9.1 and the Caddyfile this module generates for
Server + Desktop with every domain configured, serving the public sites
on a stand-in port: Host: sovransystemsos.local, localhost:8937,
127.0.0.1 and x:3051 all get an empty 200, and Host: matrix.example.org
gets the Synapse stand-in. With the Hub off Caddy the guard has nothing
left to guard, and it could not be made right for the two sites that
remain:

- IPv6. A laptop's global address on the LAN looks exactly like a
  stranger's. The choice was between letting all of 2000::/3 through,
  which is the whole IPv6 internet and is what c33457f does, and
  refusing every LAN device that connects over a global address unless
  the operator copies the ISP's prefix into a Nix option.
- They do not need it. RTL has a random 20-character password
  (pwgen -s 20, about 119 bits) and, since 0.15.12, which Sovran_Bitcoin
  pins, a 30-minute lockout keyed on the client address. Mempool shows
  public chain data. If someone forwards 3051 or 60847 that is the same
  exposure as any other port on the machine, and SECURITY.md says not to.

Remove the snippet and its two imports, and tests/test_caddy_lan_only.py
with them. What is still worth pinning moves to test_hub_direct.py: no
address filter anywhere in caddy.nix, the two sites are plain proxies to
their loopback ports, and no option for a declared prefix is left
half-wired.

Behaviour change: RTL and Mempool answer any client that can reach :3051
or :60847, as they did before c33457f. In practice that is the local
network, because nothing asks you to forward those ports.

Checked with the real generator and Caddy 2.9.1: Node Only generates
`auto_https off` and the two plain sites and validates. Run live next to
the real Hub, a LAN client gets the Hub, RTL and Mempool; a stranger's
address gets RTL and Mempool (by design) and a 403 from the Hub; port 80
is not listening on Node Only.
2026-10-02 02:24:29 -05:00
Security Fix 78bfc5b408 hub: serve the Hub on its own port instead of through Caddy
Caddy fronted the Hub at http://sovransystemsos.local, but the Hub
already listens on 0.0.0.0:8937 itself, and nothing Caddy added is
something it needs:

- Not the name. That is avahi's: mDNS advertises a hostname, not a port,
  so the name resolves wherever the Hub listens.
- Not TLS (the site was plain http), not authentication, not cache
  headers. The header block duplicated NoCacheMiddleware, and its
  Clear-Site-Data ("cache") overrode the app's stronger ("cache",
  "storage").
- Not access control, and this is the point. With ports 80/443 forwarded
  for public services, a Host header on those ports reached the Hub. That
  second door is how the reported bug happened, and c33457f guards it
  with an address check instead of closing it.

The Hub is now served on port 8937 only, at
http://sovransystemsos.local:8937, and Caddy has no site for it. The
only thing Caddy answers on 80/443 is the public sites. Caddy keeps
Ride The Lightning (:3051) and Mempool (:60847), because those do need
it: Sovran_Bitcoin binds both to 127.0.0.1 and RTL's unit is sandboxed to
loopback besides, so Caddy is how the local network reaches them.

- caddy.nix: no Hub site. Caddy runs wherever RTL and Mempool do, which
  includes Bitcoin Node Only. There it did not run at all (enable was
  needsHttpsPorts || extraVhosts != ""), so :3051 and :60847 were open
  in the firewall with nothing listening. Ports 80/443 still follow
  needsHttpsPorts alone, so Node Only does not open them. The two sites
  are written only where their service exists; they were unconditional.
- sovran-hub.nix: 8937 follows the new hub.directPort, 60847 follows
  Mempool. It used to be `[ 8937 60847 ]` on every role, Desktop Only
  included.
- roles.nix: hub.directPort defaults to !roles.desktop: open on Server +
  Desktop and Bitcoin Node Only, closed on Desktop Only, where the Hub is
  reached from the machine itself through the desktop window on
  localhost.
- The bind stays 0.0.0.0, which is IPv4 only: with that bind [::1]:8937
  is refused and "localhost" falls back to 127.0.0.1. That is on purpose
  and is now said in the comment. An IPv6 listener would let in clients
  whose global addresses the Hub cannot tell from a stranger's, which is
  the question the previous commit declines to answer by guessing.
- README, SECURITY.md and two strings in index.html give the new URL.

Behaviour changes: the Hub's address gains :8937, and http://sovransystemsos.local
on port 80 no longer reaches it. Bitcoin Node Only now runs Caddy.

Evaluated with nix eval (nixpkgs as flake.lock pins it, Sovran_Bitcoin at
the locked revision), firewall TCP ports per role:

                      c33457f                      this commit
  Server + Desktop    22 80 443 3051 8937 60847    22 80 443 3051 8937
  Bitcoin Node Only   22 3051 8937 60847           22 3051 8937 60847   (Caddy now runs)
  Desktop Only        22 8937 60847                22

Port 22 is open on every role although sshd listens on loopback only;
the last commit of this series deals with that.

The Caddyfile the module really generates (the evaluated generator
script, run, then `caddy validate` with Caddy 2.9.1): Node Only gets the
two sites and nothing else; Server + Desktop with every domain
configured gets the seven domain sites plus :3051 and :60847 and no
mention of the Hub; with Bitcoin off there are no local-network sites;
Node Only with Bitcoin off and no domains leaves Caddy off.

Add tests/test_hub_direct.py and keep tests/test_caddy_lan_only.py for
the two sites it still covers.
2026-10-02 02:24:29 -05:00
Security Fix 361b25a8bd hub: answer the local network only, whichever way a client arrives
The reported bug was the Hub being reachable from outside the local
network when Server + Desktop is active. c33457f guards the Caddy site
for sovransystemsos.local, but the Hub is an application that also
listens on a port of its own (8937), and a check in Caddy does nothing
for a client that never goes through Caddy. Whether a client could reach
the Hub depended on which door it used.

The Hub now asks the question itself. LanOnlyMiddleware is registered
outermost, so a client that is not on this computer or the local network
gets a bare 403 before authentication is considered; it never sees the
login page.

- Local means loopback, 10/8, 172.16/12, 192.168/16, 100.64/10
  (Tailscale and other CGNAT/VPN ranges) and 169.254/16 over IPv4, and
  ::1, fc00::/7 and fe80::/10 over IPv6.
- IPv6 global addresses are not on the list. A global address belonging
  to a laptop on the LAN cannot be told apart from a stranger's by the
  address alone, and 2000::/3 is every public IPv6 address there is.
- ::ffff:a.b.c.d is read as the IPv4 address inside it.
- sovran_systemsOS.hub.extraLanNetworks adds networks (IPv4 or IPv6 CIDR)
  for setups whose own devices use addresses outside those ranges. It is
  checked at build time. The app ignores an entry it cannot parse and
  refuses 0.0.0.0/0 and ::/0: it must never widen its policy by
  guessing, and "everyone" is hub.lanOnly = false, asked for by name.
- sovran_systemsOS.hub.lanOnly (default true) turns the check off.
- The first refusal from each address is logged, naming the option to
  change, so an operator whose own device is refused can find out why.
  The list is capped so a scanner cannot fill the journal or memory.

Behind Caddy the policy applies to the real client, not to Caddy: uvicorn
takes the address from X-Forwarded-For only when the peer is 127.0.0.1.

Checked, not only reviewed:
- The real app with the config.json the module really generates (nix
  eval on the nixpkgs revision flake.lock pins, read back from the
  derivation), on a real socket with the source address chosen per
  request: 127.0.0.1, 192.168/16, 100.64/10 and a declared extra
  network are served; 203.0.113.9, 8.8.4.4 and an address just outside
  the declared /28 get 403 on /login, / and /api/ping. /auto-login still
  answers 303 to loopback and 403 to everyone else.
- With c33457f's Caddy guard in front, an IPv6 client at 2001:db8::9
  passes Caddy (it is inside 2000::/3) and is refused by the Hub; an
  IPv4 stranger has the connection closed by Caddy; a LAN client is
  served.
- hub.extraLanNetworks accepts 203.0.113.0/28, 2001:db8:abcd::/48 and
  bare hosts, and fails the build for /33, 300.1.1.1/8, 0.0.0.0/0, ::/0,
  2001:db8::/129 and junk, with a message that says what to write.

Add tests/test_lan_policy.py and tests/test_hub_lan_only.py, and a note
in SECURITY.md.
2026-10-02 02:24:29 -05:00
Security Fix 7d784eb653 hub: make the login lockout that LOGIN_FAIL_MAX described
LOGIN_FAIL_MAX was declared as "max failures in window before extra
delay" and then never read anywhere in the file. _record_failure() only
ever slept a flat LOGIN_FAIL_DELAY. So the Hub's entire defence against
online guessing was a constant 2 second pause per wrong password: no
escalation, no lockout, no ban, and fail2ban is configured for SSH only.

Worse, the old 60 second window could not have worked even if the
constant had been wired up. With a delay per attempt, reaching 10
failures takes about 80 seconds, so the earliest failures aged out of the
window before the count could ever reach the limit.

- security_helpers.py: new LoginThrottle. The delay ramps with the
  failure count (2s, 4s, ... capped at 10s), and once LOGIN_FAIL_MAX
  failures land inside the window the address is refused outright for
  LOGIN_LOCKOUT_SECONDS (5 minutes). A successful login clears the
  address, so an operator who fumbles a password is not penalised later.
  The sleep is never taken under the lock, so one slow client cannot
  stall every other login. Tracked addresses are evicted, so a
  distributed sweep cannot grow the table without bound.
  The window moves from 60s to 900s so the whole ramp fits inside it.
  clock and sleep are injectable, which is what makes it testable.
- server.py: /api/login checks the lockout before the scrypt hash, so a
  locked-out client costs almost nothing to reject, and answers 429 with
  a human-readable wait instead of a bare 401.
- tests/test_login_throttle.py: covers the ramp, the cap, the lockout
  firing and expiring, per-address isolation, clearing on success,
  eviction, and that the limit is actually reachable inside the window.

Verified against the real app with TestClient: 10 wrong passwords return
401 and the 11th returns 429 "Too many failed attempts. Try again in
about 5 minute(s)." A correct password clears the counter.
2026-10-02 02:24:29 -05:00
Arena.ai Agent c33457fff2 caddy: serve the Hub, RTL and Mempool sites to local clients only
The Hub (sovransystemsos.local), Ride The Lightning (:3051) and Mempool
(:60847) sites are meant for the home network. With ports 80/443
forwarded for public services, Caddy also receives requests from other
clients, so these sites now check the client address as well as the Host
header.

A new snippet, sovran_lan_only, closes the connection unless the client
is on this computer or the local network: private_ranges, 100.64.0.0/10
(Tailscale), 169.254.0.0/16, fe80::/10 and fc00::/7. IPv6 global
addresses (2000::/3) are not filtered: computers on the network often
connect over their own global address, which cannot be told apart from
one on the internet by the address alone. Only the three local sites
import the snippet; the domain sites for public services are unchanged.

Clients with a public IPv4 address on the local network are no longer
served on these sites. The Hub is still available on port 8937.

Checked with Caddy 2.11.4 and the Caddyfile the generator writes: public
IPv4 clients get the connection closed on all three sites, local clients
are served, and the public domain sites answer as before.

Add tests/test_caddy_lan_only.py and a note in SECURITY.md.
2026-10-01 21:43:47 -05:00
Arena.ai Agent 34cfba4282 docs, hub, installer: say Server + Desktop makes the home IP public
Server + Desktop publishes services under the operator's own domain, and
the DNS record for that domain points at the home connection, so anyone
can look up the home IP address. None of the places that offer Server +
Desktop said so.

- README: new section "Server + Desktop and your home IP address" (what
  becomes public, what does not, the alternatives, and what happens
  technically), plus a note on the role table and in the security
  overview.
- SECURITY.md: a matching section, the consequence noted next to "Public
  web services exposed only when enabled by the operator", and the
  supported versions row no longer pins 1.0.x.
- ISO installer: the Server + Desktop role card ends with the warning.
- Hub: the domain setup text (onboarding, feature setup and domain
  reconfiguration share renderDomainNeedsHtml) and the upgrade dialog
  carry the same notice.
- Add tests/test_exposure_guards.py. It fails if one of these places
  loses the notice or the README anchor stops resolving.
2026-10-01 21:43:47 -05:00
Arena.ai Agent 2d777450e1 ddns: take the public IP from Njal.la only and give it to LiveKit
The Hub no longer asks a STUN server, a public DNS resolver or a "what
is my IP" service for the home IP address. The DDNS update asks Njal.la
to use the address the request comes from ("&auto"), reads back the
address Njal.la says it recorded and saves it to
/var/lib/secrets/external-ip. Njal.la is the only third party that
learns the address; it has to, to publish it.

- Add app/sovran_systemsos_web/ddns_update.py, installed as
  /etc/sovran/ddns-update.py and run by sovran-ddns-update.service. It
  runs curl --ipv4 without redirects, accepts only a public IPv4
  address, and rewrites the file atomically and only when the address
  changes. Stored "&a=${IP}" URLs are converted when they are used and
  "&quiet" is dropped.
- Rewrite modules/core/njalla.nix around that runner and delete
  modules/core/public-ip.nix. Setting a sovran_systemsOS.publicIP.*
  option now fails with a message that says where the address comes
  from. Activation removes the old scripts in /var/lib/sovran. The
  existing external-ip file keeps working.
- server.py reads the saved address and starts
  sovran-ddns-update.service after a domain is saved, instead of looking
  the address up itself.
- Element calling uses sovran_systemsOS.elementCalling.externalIP if
  set, otherwise the saved address, and fails with a clear message when
  neither exists or the address is not public. It no longer falls back
  to STUN. livekit-external-ip.path re-runs livekit-turn-setup and
  starts LiveKit when the address changes.
- Add tests/test_ddns_update.py.
2026-10-01 21:43:47 -05:00
naturallaw777 726fec1990 updated nixpkgs and Sovran_Bitcoin update and the new Bisq 1.10.9 2026-10-01 13:36:12 -05:00
naturallaw777 70ccf1eba1 chore(release): prepare v1.1.7 2026-09-21 18:09:10 -05:00
naturallaw777 f6caa2ff32 updated nixpkgs 2026-09-21 18:07:21 -05:00
Sovran Contributor 8bc325b148 postgresql: drop per-database autovacuum ALTERs (rejected by Postgres)
Follow-up to the previous two commits: the ALTER DATABASE ... SET
autovacuum_* commands fail at boot with

  ERROR: parameter "autovacuum_vacuum_scale_factor" cannot be changed now

and take matrix-synapse-db-tune.service (and nextcloud-db-init.service)
down with them.

Root cause: ALTER DATABASE/ROLE ... SET validates through
set_config_option() with an interactive context, and guc.c rejects any
PGC_SIGHUP parameter set that way. All autovacuum_* GUCs are
SIGHUP-context, so per-database scoping is impossible for them — only
USERSET-level parameters (e.g. work_mem, statement_timeout) can be set
per-database.

Nothing is lost: the same values are already set cluster-wide in
configuration.nix, which covers both nextclouddb and matrix-synapse.
Remove the ALTERs from nextcloud-db-init and delete the now-purposeless
matrix-synapse-db-tune service.
2026-09-21 14:21:01 -05:00
Sovran Contributor e31094c194 synapse: performance tuning for 32 GB Server+Desktop hosts
Monolith Synapse spends most of its RAM on caches to avoid Postgres
round-trips, but Sovran ships stock cache settings (global_factor 0.5,
10K event cache, no autotuning) and a default 5-connection DB pool.

- caches.global_factor 4.0 + 100K event cache + autotuning capped at
  2G (target 1G), with boosts for the /sync and room-join hot paths.
- DB pool cp_min 5 / cp_max 15, txn_limit 10000 (fewer reconnects).
- gc_thresholds raised to cut GC pauses on a 32 GB box.
- cache-memory extra for cache-size statistics.
- Per-database autovacuum (ALTER DATABASE, scoped to matrix-synapse)
  matching the nextclouddb tuning.

Deliberately unchanged: presence and URL previews stay enabled
(disabling them is faster but user-visible), and no workers — monolith
is the right call under ~100 users. Workers would need Redis
replication, the redis extra, and Caddy reverse-proxy rework; revisit
if federation load ever justifies it.
2026-09-21 14:00:19 -05:00
Sovran Contributor 09d4cc9b83 nextcloud, postgresql: fix Nextcloud 35 DB warnings on 32 GB hosts
Nextcloud 35's Database checks flag three Performance issues out of the
box: buffer cache hit ratio ~96% (wants 99%+), 100k+ dead tuples, and
million-plus sequential scans on oc_mail_tags / oc_guests_users.

Root causes in Sovran: stock 128MB shared_buffers, stock 60s autovacuum
naptime, APCu file locking, and db:add-missing-indices running exactly
once at install time (never on upgrades or app installs).

Size Postgres for the README's Server + Desktop recommendation (32 GB
RAM, NVMe): 2GB shared_buffers, 12GB effective_cache_size, 512MB
maintenance_work_mem, 32MB work_mem, 4GB max_wal_size, 30s autovacuum
naptime with 4 workers. shared_buffers stays below the 25% rule because
Postgres shares the box with bitcoind, Electrs, LND, MariaDB and PHP-FPM.

Scope the aggressive autovacuum to nextclouddb via ALTER DATABASE so the
shared matrix-synapse DB keeps the milder cluster defaults.

Add a local Redis (127.0.0.1:6379, Nextcloud only) and move
memcache.distributed/locking to Redis; migrate existing installs with a
one-shot since nextcloud-init never re-runs.

Add a weekly nextcloud-db-maintenance timer (VACUUM ANALYZE +
db:add-missing-*) so upgrades and later app installs can't regress the
checks again.

Note: shared_buffers needs one 'systemctl restart postgresql', which
briefly takes down both Nextcloud and Matrix. Everything else is
reload-only or scoped to nextclouddb.
2026-09-21 13:59:52 -05:00
naturallaw777 3341659a0c updated to php85 and fixes 2026-09-19 16:24:57 -05:00
Sovran Systems 32e1119e33 Updated to proper syntax to prevent build errors. 2026-09-17 16:28:37 -05:00
Sovran Systems 0f7ef8422d Clean up flake.nix by removing comments and LiveKit override
Removed comments and overridden attributes for LiveKit version in flake.nix.
2026-09-17 16:19:37 -05:00
naturallaw777 dd6042928a updated flake lock which contains Bisq 1.10.8 and Bisq2 2.1.13 2026-09-17 15:37:28 -05:00
naturallaw777 74405b2ffc chore(release): prepare v1.1.6 2026-09-15 15:17:10 -05:00
naturallaw777 258da6a337 updated nix packages includes the new mempool version 2026-09-15 14:15:20 -05:00
Sovran Systems 73ab3f40c1 Rename 'The Sovran Hub' to 'The Hub' in README 2026-09-09 11:19:50 -05:00
Sovran Systems 0768712bf7 Rename 'Sovran Hub' to 'The Hub' in README
Updated references from 'Sovran Hub' to 'The Hub' for consistency.
2026-09-09 11:19:03 -05:00
naturallaw777 2ac30dc10a docs: update Sovran Hub screenshot to the v1.1.5 redesign
Replace the pre-redesign Hub capture with the new welcome dashboard
introduced in v1.1.5 — the default view showing system status, Bitcoin
sync, and the update card at a glance.

The capture is rendered from the real Hub frontend (Server + Desktop
role, demo credentials/domains) at 1920x1080, and doubles as the hero
shot of the marketing kit.  Also drops the asset from 391 KB to 59 KB
with no visible loss.
2026-09-09 11:04:57 -05:00
naturallaw777 499676569e chore(release): prepare v1.1.5 2026-09-09 08:41:01 -05:00
naturallaw777 35dbd198b8 updated nixpkgs 2026-09-09 08:39:07 -05:00
naturallaw777 e3f8a2579a hub: don't claim the OS "keeps itself current" on the dashboard
The welcome dashboard's updates card showed "Sovran_SystemsOS keeps
itself current" whenever no updates were pending.  Nothing in the OS
auto-updates: the Hub only *checks* for updates (on load and every
30 minutes while the Hub is open); applying an update is a manual
"Update System" action followed by a reboot.  The sub-line promised
behavior that does not exist.

Tie "up to date" to the last check instead ("Last check found no
updates · Click to check again"), matching the actionable phrasing
of the card's other states ("Click to review and update", "Click to
retry the update").

Also add a "Checking for updates" state for the first paint, so the
card never claims "up to date" before the first /api/updates/check
has returned.
2026-09-09 07:29:13 -05:00
naturallaw777 9919966070 updated Sovran-Bitcoin 2026-09-08 21:57:22 -05:00
naturallaw777 859f25f0c1 hub: point RTL credentials at /rtl/ and bump dev version to 0.15.12
Sovran_Bitcoin 0.15.12 serves the RTL UI under /rtl/ (upstream Angular
<base href="/rtl/"> + PathLocationStrategy); the package redirects / to
/rtl/ only for the exact root path.

Update the Hub's RTL tile so the Tor and Local Network credentials show
the canonical /rtl/ URLs (with trailing slash, which the redirect does
not cover) instead of relying on the root-path redirect. Bump the dev
versions.json fallback for rtl.service from 0.15.10 to 0.15.12 to match
the flake (deployed systems already read pkgs.sovran-bitcoin.rtl.version).
2026-09-08 21:39:27 -05:00
naturallaw777 4c22c2363b Make dashboard cards uniform and symmetric; stop clipping update text
The four welcome cards varied in shape and typography: the grid
produced a 3+1 orphan layout at wide widths, sub text was
single-line ellipsized (the updates card ended mid-sentence with
"…"), the Network card's values used a different size than the
other cards' sub text, and only the clickable cards carried a
chevron circle.

- Grid is now a symmetric 2×2 at wide widths (equal-height rows);
  the three-card Desktop-only role renders one balanced row
- Card sub text wraps instead of being cut off — "Sovran_SystemsOS
  keeps itself current" is fully visible at every width
- One type scale across all cards: 0.9rem titles, 0.8rem sub text
  (Network values aligned to it), 46px chips, uniform min-height
- Chevron circles removed from the cards — hover/focus lift is the
  click affordance, and every card has the same shape
- The updates card now reflects the real update state (failed,
  restart required, running) instead of only "updates available";
  it can no longer claim up-to-date while a restart is pending
2026-09-08 20:08:52 -05:00
naturallaw777 bbf53d089a Fix role fit-and-finish: Desktop-only router note, narrow-viewport layout
Systems Operational on roles with no enabled domain services (a
fresh Desktop-only install, or everything turned off) rendered no
Router card but still showed a "Who uses these ports" note listing
services the machine does not have. Any role with no enabled domain
service now gets the calm "No router setup needed yet" card with
role-appropriate wording, and the who-uses note only appears when
a domain service is actually enabled.

Narrow viewports (phones, half-screen RDP) had no layout at all:
the sidebar and topbar forced ~900px of horizontal scroll on every
role. The sidebar now collapses to a 76px icon rail below 920px,
the topbar wraps its search onto a second row below 640px, and the
welcome column reflows to one card per row. Legacy pre-redesign
media rules in onboarding.css (which targeted the old DOM and set
.sidebar{width:100%}) are removed — they silently overrode the new
layout, and one shrank the Zeus QR to 200px; QR codes keep their
repo-original 240px at every width.
2026-09-08 19:38:34 -05:00
naturallaw777 4476a5e0e2 Add boot splash; separate Systems Operational and Security icons
Loading indicator:
- Branded boot splash (Hub logo inside an accent spinner ring,
  "Starting The Hub") covers the shell while the first services
  data loads, then fades out once the welcome dashboard has
  rendered; never blocks longer than 25s and reassures the user
  after 8s (message about post-reboot delays)
- Fire the network and update checks before the first services
  render so the dashboard cards are current at first paint
- Sidebar Update button now adopts the last known update state
  when built (order-independent), and the welcome dashboard
  re-renders when the update state changes

Icons:
- New monochrome g-pulse glyph (activity line) for Systems
  Operational: welcome card, dialog header, and System Status
  section — the shield no longer doubles as Security
- Tech Support / Security dialog header gets a standard chip;
  Security shows the shield chip with a plain "Security" title
  (no emoji), and the shared dialog title now resets correctly
  when reopening Tech Support after Security
2026-09-08 19:15:06 -05:00
naturallaw777 36b77e3f0a Extend welcome dashboard background to the panel edges
The drifting orb glow was clipped to the centered 1040px column,
leaving visible walls where it met the content padding. Make the
welcome section full-bleed instead: cancel the content-area padding
with negative margins and stretch it to all four panel edges
(sidebar border, topbar, viewport right edge, bottom). The heading,
cards, and Browse button keep their centered 1040px column via
.welcome-inner.
2026-09-08 18:58:48 -05:00
naturallaw777 78ae9e78ed Add welcome dashboard as default view
Replace the All Services grid as the landing view with a
Nextcloud-style welcome dashboard:

- Greeting (time-of-day), "Welcome to Your Sovereign Digital &
  Financial Life" headline, version + role meta line
- Status cards: Systems Operational, Network (LAN/WAN/hostname),
  Bitcoin sync progress with ETA, and available-updates card
- Calm drifting orb background animation (transform-only,
  disabled under prefers-reduced-motion)
- Services grid stays mounted but hidden until the user browses
  (Browse button, search, or a nav category); Dashboard nav item
  returns to the welcome view
- Move role badge and autolaunch preference out of the sidebar;
  autolaunch toggle now lives in the Systems Operational modal
- Remove LAN/WAN chip from the topbar; IPs live on the Network card
2026-09-08 18:46:43 -05:00
naturallaw777 61ef286420 Monochrome updater glyph; fix oversized dialog header icon
The colored updater icon broke the monochrome system-action set in the
sidebar. Replace it with a new g-update glyph — a down arrow dropping
into an open tray ("get / install updates") — drawn in the same 24×24,
2px round-cap stroke style as the other sidebar glyphs.

- Sidebar Update System row: g-update glyph, currentColor like the rest.
- Update dialog header: same glyph in the green chip, matching the
  rebuild dialog and Systems Operational header treatment.
- Also fixes a latent sizing bug: the previous colored <img> had no CSS
  rule (the .upd-icon rule from the earlier patch never landed), so the
  dialog header icon rendered at the source file's intrinsic 128px. The
  chip markup uses the existing 54px .upd-chip rules.
2026-09-08 18:25:47 -05:00
naturallaw777 ca1a5c9eb8 Merge Bitcoin categories into one; rename Self-Hosted Apps to Personal Apps
- One "Bitcoin" category: the service catalog distinguishes bitcoin-base
  from bitcoin-apps, which surfaced as two sidebar menus and two tile
  sections. The Hub now normalizes both into a single "Bitcoin" category
  (nav item with combined count, one tile section). CATEGORY_ALIASES in
  constants.js maps the catalog keys; server config sends one
  ("bitcoin", "Bitcoin") entry and the node role allowlist is updated
  to match. The nix catalog is unchanged.
- "Self-Hosted Apps" is now "Personal Apps" — every service here is
  self-hosted, so the label added no distinction.
2026-09-08 18:18:36 -05:00
naturallaw777 6d32bebdc6 Neutral graphite theme; branded updater icon
Coloring: the base surfaces were green-tinted darks, which read as a
green-hued background rather than green highlights. Shift the whole
surface ramp to neutral graphite (slightly cool, and lighter overall)
and keep green strictly for highlights — brand, buttons, switches,
status dots and pills, sync bars, focus rings.

- Tokens: bg #17191d, surface #1c1f24, card #23272c, hover #292e34,
  elevated #26292e, inset #121417; text neutrals #e9edec/#a9b0b3/
  #7a8388. Accent, borders, radii, shadows unchanged.
- Remove the green ambient radial washes behind the app (and login).
- Reboot / security-reset overlay gradients neutralized.

Updater icon: restore the repo's branded Sovran updater icon
(/static/icons/update.svg, true colors) in the sidebar Update System
row and in the update dialog header, replacing the generic refresh
glyph. The rebuild dialog keeps its glyph chip (no branded icon
exists for it).
2026-09-08 18:09:15 -05:00
naturallaw777 e6078b9c89 Simplify Systems Operational, reword Domain Status, blue restart buttons
- Restart and retry actions (topbar Reboot, Restart Entire System in the
  update and rebuild dialogs and the restart confirmation, Retry Update,
  Try Again) are now blue instead of amber — amber read as an error.
  Status pills keep their semantic colors (amber = restart required).
- The service-modal domain section is no longer a checklist: it is titled
  "Domain Status" and shows a single green "Domain is active" line with
  the domain (or the backend's not-configured detail and Configure
  Domain action when there is no domain yet). No "Step 1" wording.
- Systems Operational keeps the complexity hidden: the Router card now
  shows just a verdict — green "Ports 80 and 443 are open" or red
  "Ports 80 and 443 are not open" — derived from the same live backend
  diagnostics. Port-forwarding instructions are gone (onboarding covers
  them); on the Node-only role the card still explains that ports only
  matter once BTCPay Server or Lightning Wallet Connections (LNURL) is
  enabled.
2026-09-08 17:52:04 -05:00
naturallaw777 ea9d4f21d7 Polish pass: sysops wording, QR size, NWC toolbar, logo, diagnostics placement
Eight fixes from live testing:

Systems Operational modal:
- Drop the "test from your phone on mobile data" (hairpin NAT) sentence
  from the router note — too technical for the intended audience.
- Node-only role: when BTCPay Server and Lightning Wallet Connections
  (LNURL) are both off, the router card becomes a simple "No router
  setup needed yet" note explaining that ports 80/443 only matter if
  one of those services is turned on. When one is enabled, the card
  shows the same port steps and live domain/port diagnostics as the
  Desktop + Server role (diagnostics now poll only enabled services).

Service modals:
- The Domain Diagnostic Checklist now shows only the domain-active
  step (Domain Configured). DNS and port diagnostics live in Systems
  Operational, which shows the full checklist.
- Node-only role: BTCPay Server and Lightning Wallet Connections
  modals gain a "Ports to Forward in Your Router" section with the
  standard 80/443 wording and this computer's LAN address.
- Domain setup and reconfigure dialogs no longer contain router
  port-forwarding instructions (already handled during Desktop +
  Server onboarding and shown in Systems Operational).

Lightning Wallet Connections:
- Refresh and New Wallet toolbar buttons now share one height and
  baseline (a leftover 12px top margin on Refresh was offsetting it).
- The header status chip has a proper gap between the status dot and
  its label.

Zeus Connect / QR codes:
- QR codes render at the original 240px with the white frame and
  pixelated upscaling, restoring scannability.

Brand:
- The sidebar logo loads via /static/sovran-hub-icon.svg (same as the
  login page) instead of an inline <use> symbol — the gradient-heavy
  symbol did not render reliably. The icon sprite is hidden with the
  browser-safe zero-size pattern instead of display:none.
2026-09-08 17:19:26 -05:00
naturallaw777 d164d423ad Rework Update System dialog to the approved design; match rebuild dialog
The update dialog kept the old bare title + spinner layout from the
previous theme. Rework it (and the rebuild dialog, for consistency) to
the approved The Hub dialog anatomy:

- Header: green chip icon, title, version chip, status pill
  (Checking… / Up to date / Updating… / Restart required / Update
  failed / Status unknown) and a header close button. A spinner appears
  in the header while an update is starting or running.
- System Details card: current version, release channel, and last
  checked (relative time, refreshed on every check).
- The log renders as a console with green "ok" and dim hint lines; the
  up-to-date result shows as a single green console line exactly once
  (the redundant status line is hidden in that state).
- Footer: Close plus a "Check again" primary action that re-runs the
  update check. Close and Check again are disabled while a check or
  update is in flight; all existing recovery actions (Save Error
  Report, Retry Status, Retry Update, Restart Entire System) keep their
  exact semantics.
- Opening the dialog now shows a "Checking…" state immediately while
  the existing reattach-then-check logic runs; reattaching to an
  in-progress update (page reload, RDP reconnect) is unchanged.

Sidebar Update System status tints now use the theme palette (red /
amber / blue / green), and the periodic background check refreshes the
dialog's "last checked" value.

Rebuild dialog gets the same header (icon, version chip, Applying… /
Done / Restart required / Failed pill, header close disabled while a
rebuild runs); its log stays hidden as before.

No API or state-machine changes: same endpoints, same polling, same
reattach and recovery behavior.
2026-09-08 16:40:06 -05:00
naturallaw777 2013df55bd Redesign The Hub web UI: softer dark theme, sidebar nav, status widgets
Apply the approved "The Hub" redesign to the web admin while keeping every
existing mechanic intact (polling, service-detail modals, Matrix and system
password management, NWC wallet manager, update/rebuild/backup/security/
reboot flows, feature manager, onboarding, role handling).

Layout (templates/index.html):
- Old header bar + IP bar replaced by a sidebar + topbar app shell.
  Sidebar carries the brand (The Hub / Sovran_SystemsOS version), category
  navigation with live counts, the System actions (Update System, Tech
  Support, Manual Backup, Security, node-only Upgrade), Feature Manager /
  Preferences, and the role badge.
- Topbar carries the page title, a service search box, the LAN | WAN
  network chip (external IP always visible, one line), Reboot and Sign Out.
- New widgets row: Systems Operational summary (opens the new Systems
  Operational modal) and Bitcoin Core sync progress with block/ETA.
- New Systems Operational modal: service counts, router port-forwarding
  steps (80/443 to this machine's LAN IP), the live domain diagnostics
  checklist, and which services use those ports.

New static/js/dashboard.js (namespaced IIFE, no new globals) renders the
nav, search filtering, widgets, and the Systems Operational modal; it is
driven by the existing /api/services payloads via a
window.dashboardServicesUpdated() hook called from buildTiles/updateTiles.

Visual design (static/css/*):
- New token set (softer dark surfaces, lifted contrast, Sovran green
  reserved for status and actions) with legacy variable names aliased so
  every secondary sheet re-skins automatically.
- Tiles, dialogs, buttons, inputs, toggles, tables, forms, overlays and
  the login page restyled to the GNOME/libadwaita-flavored surfaces:
  20px cards, 26px dialogs, pill buttons, libadwaita switches, mono value
  pills with Copy buttons, consistent modal anatomy.
- Inline SVG symbol set for chrome/nav icons (monochrome, currentColor);
  service icons still load from /static/icons/*.svg as before.
- Sidebar system buttons now use vector glyphs instead of emoji.

Behavioral details:
- Service detail modal header gains a status pill next to the version
  chip; credentials render with pre-wrap for multiline values.
- First-login security banner now renders as a card inside the content
  area instead of a full-width strip above the app.
- Search + category filtering hide/show sections and tiles without
  touching the polling or update logic.

Onboarding and login pages rebranded to "The Hub" with aligned palette.
2026-09-08 15:52:49 -05:00
naturallaw777 1d46d036c0 nixpkgs update 2026-09-07 10:55:32 -05:00
Sovran Systems 9ae4e34fe0 Switch LibreOffice from fresh to stable version 2026-09-03 16:31:12 -05:00
Sovran Systems d1e226a687 fix(hub): self-heal truncated/corrupt Nix downloads and keep failed updates retryable
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.
2026-09-03 11:54:29 -05:00
Patch Bot 49e41eeea9 ui: simplify element-calling port modal — trim verbose steps and remove extra verification task
- 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)
2026-09-03 09:52:27 -05:00
naturallaw777 88be5b99dd updated flake to build through the new sovran-bitcoin input 2026-09-02 15:29:20 -05:00
naturallaw777 93492e88fb chore(release): prepare v1.1.4 2026-09-02 15:13:56 -05:00
naturallaw777 7da37cdb34 updated nixpkgs 2026-09-02 14:52:15 -05:00
Sovran_SystemsOS f34d1533c3 element-calling: fix Nix string interpolation of LAN_CIDR echo
The debug echo used bash ${LAN_CIDR:-<none>} syntax, but inside a Nix
indented string ${...} is Nix interpolation, not bash. Nix parsed
'LAN_CIDR:-<none>' as a lambda and failed the build with 'cannot coerce a
function to a string'. Rewrite the echo without brace expansion.
2026-09-01 12:29:47 -05:00
Sovran_SystemsOS 220d6dff2c docs/hub: update Element Calling port guidance to the new port 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.
2026-09-01 12:15:16 -05:00
Sovran_SystemsOS 81ab3b2280 element-calling: fix Wi-Fi calls and tighten media/TURN ports
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.
2026-09-01 12:15:16 -05:00
Sovran_SystemsOS d5d40a1697 flake: pin LiveKit to 1.13.6 for rtc.advertise_internal_ip support
rtc.advertise_internal_ip (which lets LAN callers reach the SFU via a LAN
host candidate, avoiding NAT-hairpin on the router) is only honoured when
node_ip is set manually from LiveKit v1.13.6 (mediatransportutil f234b53).
nixpkgs-unstable currently ships 1.13.5, so pin 1.13.6 with the hashes from
nixpkgs master. Remove this override once nixpkgs-unstable reaches >= 1.13.6.
2026-09-01 12:15:16 -05:00
naturallaw777 3756a0058d updated sovran-bitcon 2026-08-31 20:37:46 -05:00
naturallaw777 be5d47cb9c updated sovran-bitcon 2026-08-31 20:31:48 -05:00
naturallaw777 ddf87a1c1c refactor(nwc): dedupe NWC tooling — use Sovran_Bitcoin's sovran-nwc
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
2026-08-31 20:28:10 -05:00
naturallaw777 f0e4c33a5f fix: set lnurl domainFile for Hub-managed Lightning Address domain 2026-08-31 10:58:04 -05:00
naturallaw777 a6ef13688e chore: update sovran-bitcoin flake input 2026-08-31 10:50:59 -05:00
naturallaw777 ef1c045e0e fix: correct disablewallet casing 2026-08-31 10:46:19 -05:00
naturallaw777 e27bf0ef1f fixed typo 2026-08-31 10:27:11 -05:00
Sovran Patch 362fa0b36c refactor: extract bitcoin stack into Sovran_Bitcoin flake input
Decouple the Bitcoin/Lightning modules and packages into the standalone
Sovran_Bitcoin flake, consumed as a NixOS module input.

Deleted (now in Sovran_Bitcoin):
  - modules/bitcoin/          (19 files — vendored nix-bitcoin modules)
  - modules/bitcoinecosystem.nix
  - modules/nwc-wallets.nix
  - modules/mempool.nix
  - packages/{albyhub,mempool,rtl,build-support}/
  - tests/bitcoin-btcpay-hardening.nix

Created:
  - modules/sovran-bitcoin-integration.nix — the OS-specific bridge that
    maps sovran_systemsOS.* options to sovran-bitcoin.* and applies
    Second_Drive paths, operator 'free', forced wallet, firewall 3051,
    and Sovran Hub NWC environment wiring.

Modified:
  - flake.nix — added sovran-bitcoin flake input, updated module imports
  - modules/modules.nix — removed deleted imports
  - modules/core/sovran-hub.nix — version metadata now reads from
    pkgs.sovran-bitcoin.* overlay instead of local packages/
  - tests/test_bitcoin_tor_gossip.py — updated to check integration layer

The sovran_systemsOS.* option namespace is preserved. The Hub, roles,
and custom.nix continue to work unchanged.
2026-08-31 10:17:14 -05:00
naturallaw777 043981a34b chore(release): prepare v1.1.3 2026-08-27 09:24:31 -05:00
naturallaw777 5529fccab2 Sparrow update 2026-08-27 09:12:03 -05:00
naturallaw777 5f84154dca nixpkgs update with bisq, bisq2, and sparrow update 2026-08-27 09:10:45 -05:00
Arena Agent 60b55f715e fix(hub): evaluate local vendored package versions directly
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).
2026-08-27 09:05:20 -05:00
naturallaw777 01037cd55c albyhub: re-harvest hashes for fixed sovran tag 2026-08-27 02:09:35 -05:00
naturallaw777 7980833fc2 albyhub: re-harvest hashes for fixed sovran tag 2026-08-27 02:08:13 -05:00
naturallaw777 837fffc8f9 albyhub: re-harvest hashes for fixed sovran tag 2026-08-27 02:05:35 -05:00
naturallaw777 cb4585ecf8 albyhub default.nix hash update 2026-08-27 01:13:48 -05:00
naturallaw777 1de1bf95ad albyhub default.nix hash update 2026-08-27 01:01:56 -05:00
naturallaw777 e9d6edca56 albyhub default.nix hash update 2026-08-27 01:00:17 -05:00
naturallaw777 a7bfc9aa0d albyhub default.nix fix 2026-08-27 00:58:17 -05:00
naturallaw777 ed2e4059c1 albyhub: build from sovran fork tag (LND-only, no frontend) 2026-08-27 00:50:35 -05:00
naturallaw777 f057e43ca0 fix(albyhub): use proxyVendor to preserve secp256k1-zkp cgo headers 2026-08-25 19:28:18 -05:00
naturallaw777 d600198049 feat(albyhub): vendor v1.24.0 LND-only, no-frontend build
Replace the nixpkgs albyhub overrideAttrs patch-chain with a fully
vendored package at packages/albyhub for v1.24.0.

- modules/core/sovran-hub.nix: bump fallback version 1.8.0 -> 1.24.0
- modules/nwc-wallets.nix: build via pkgs.callPackage ../packages/albyhub
- packages/albyhub:
  - drop 0002-isolated-invoice-app-id.patch (fixed upstream)
  - add 0004-lnd-only.patch: strip LDK/Bark/Cashu/CLN/Phoenix backends
    from service/start.go, leaving only the LND case
  - add 0005-no-frontend.patch: remove //go:embed dist and the
    frontend handler registration
  - add default.nix: buildGoModule for v1.24.0 with no nodejs/yarn/
    bark-ffi-go/ldk-node deps (only stdenv.cc.cc), subPackages cmd/http

Keeps 0001-private-route-hints and 0003-loopback-bind-host. Does not
touch flake.nix, VERSION, or CHANGELOG.
2026-08-25 19:11:25 -05:00
naturallaw777 14db4282b7 ux: simplify domain and router setup guidance 2026-08-22 09:10:05 -05:00
naturallaw777 d31887cae6 chore(release): prepare v1.1.2 2026-08-21 09:47:43 -05:00
naturallaw777 669181c75b nixpkgs update with Bisq update 2026-08-20 17:04:36 -05:00
naturallaw777 47e9bb99b0 fix(public-ip): move system.activationScripts under the config attribute
The module mixed the `options` keyword attribute with a bare top-level
`system.*` setting. Once a module declares `options` (or `config`), every
other top-level attribute must be a reserved module keyword — the nixpkgs
unifyModuleSyntax check rejects anything else, so every nixos-rebuild
aborted at evaluation time with:

  error: Module '.../modules/core/public-ip.nix' has an unsupported
  attribute `system'. ... move all of them (namely: system) into the
  `config' attribute.

Prefix the activation script with `config.` (equivalent to wrapping it in
`config = { ... };`) so the module evaluates again. The detector script
itself is unchanged.

Fixes: ac6c498615 (-feat(public-ip): unify public-IP detection into one privacy-first script-)
2026-08-20 17:00:23 -05:00
naturallaw777 ac6c498615 feat(public-ip): unify public-IP detection into one privacy-first script
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}
2026-08-20 16:40:00 -05:00
naturallaw777 224ea99ce4 fix(element-calling): reuse Hub external IP for LiveKit, drop egress service calls 2026-08-20 16:12:00 -05:00
naturallaw777 c54dbfe2a5 feat(element-calling): fix Element X discovery and harden federated calling
The element-calling feature only advertised the LiveKit focus via the
well-known org.matrix.msc4143.rtc_foci file, and relied on STUN
auto-detection for the public IP. Element X queries the MatrixRTC
transports registry endpoint and fails with MISSING_MATRIX_RTC_TRANSPORT
when it is absent, and blocked STUN egress silently left LiveKit
advertising a private IP (call connects but no video across servers).

- synapse: enable msc4143_enabled and advertise matrix_rtc.transports
  (MSC4519) with the site's element-calling URL, so Element X can
  discover the LiveKit focus instead of erroring out
- livekit: determine the public IP to advertise at runtime —
  explicit pin, then HTTPS egress detection (api.ipify.org /
  checkip.amazonaws.com / ifconfig.me), then STUN fallback with a
  warning; reject non-routable results (private/loopback/CGNAT)
- lk-jwt-service: append optional extra homeservers to
  LIVEKIT_FULL_ACCESS_HOMESERVERS via the new
  sovran_systemsOS.elementCalling.fullAccessHomeservers option
- add sovran_systemsOS.elementCalling.externalIP option to pin the
  advertised public IP for multi-WAN/VPN setups
- add element-calling-public-check.service: boot-time diagnostics for
  public DNS (via 1.1.1.1, bypassing local loopback overrides), JWT
  healthz through Caddy and via the public IP, and the transports
  endpoint — turns the silent -no media- failure into a visible error
- add restartTriggers so livekit/lk-jwt-service pick up regenerated
  runtime configs on rebuild
2026-08-20 14:07:06 -05:00
naturallaw777 a1fa40cacf fix(hub): derive Restart required from boot default vs running system
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.
2026-08-19 11:31:29 -05:00
naturallaw77 48dacbeef3 refactor(lnd): use pkgs.lndinit, drop local packages/lndinit
The local packages/lndinit/default.nix is a verbatim copy of the
upstream Nixpkgs expression, frozen at v0.1.3-beta (the version that
was vendored in from nix-bitcoin before the Aug 10 2026 refactor in
commit 1fbeafd). It carries no Sovran-specific patches, no local
overrides, and no behavioral modifications - it is byte-for-byte
identical to what Nixpkgs ships, except ~19 minor versions older
(Nixpkgs currently ships 0.1.22-beta; the developer upstream
lightninglabs/lndinit is at v0.1.36-beta as of June 10 2026).

Why this matters
----------------

The Aug 10 2026 refactor (1fbeafd, "refactor: move vendor/nix-bitcoin
to modules/bitcoin, remove overlays") stated the new convention:

    No more random vendor/ or pkgs/ dirs - follows Sovran convention:
    modules/ for NixOS modules, packages/ for packages

That refactor successfully removed:
  * pkgs/sovran-overlay.nix
  * pkgs/nbxplorer.nix
  * pkgs/README.md
  * modules/vendor/ (entire directory)
  * overlay-sovran from flake.nix

It moved the lndinit expression into packages/lndinit/default.nix as
an intermediate step, but the file is still a verbatim upstream copy
and therefore still incurs the maintenance burden the refactor was
meant to eliminate: manual version bumps, manual hash refreshes, and
no upstream security or bug-fix flow. Removing it completes the
intent of 1fbeafd.

The change
----------

modules/bitcoin/lnd.nix (line 153):

    -  lndinit = "${(pkgs.callPackage ../../packages/lndinit {})}/bin/lndinit";
    +  lndinit = "${pkgs.lndinit}/bin/lndinit";

The two later uses of `lndinit` in the same file (lines 243 and 247,
inside the systemd.services.lnd.preStart block that calls
`lndinit gen-seed` and `lndinit init-wallet`) are unchanged because
they reference the let-bound `lndinit` value, not the callPackage
expression. They continue to work with the new pkgs.lndinit binary
path transparently.

Removed:
  * packages/lndinit/default.nix
  * packages/lndinit/ (now empty directory)

No other files in the repository reference packages/lndinit. Verified
by:
  * Git tree search for "packages/lndinit" -> only the file and its
    parent directory match
  * Content grep of flake.nix, configuration.nix,
    modules/bitcoin/default.nix, modules/bitcoin/common.nix, and
    iso/common.nix -> zero matches

Why this is safe
----------------

1. CLI compatibility. The preStart script only invokes two
   lndinit subcommands:
     * `lndinit gen-seed`
     * `lndinit -v init-wallet --file.seed=... --file.wallet-password=... --init-file.output-wallet-dir=...`
   Both subcommands and all four flags have been stable since the
   0.1.x line. The Nixpkgs 0.1.22-beta binary produces a wallet.db
   and admin.macaroon in the same on-disk format that 0.1.3-beta did
   for the same LND version (LND is pinned separately by pkgs.lnd
   from Nixpkgs and is unaffected by this change).

2. No coupled Go modules or shared vendor tree. The local
   packages/lndinit/default.nix is a self-contained buildGoModule
   derivation; it has no shared state with any other Sovran package.

3. Nixpkgs pin is current. flake.nix pins
   github:NixOS/nixpkgs/nixos-unstable, which has shipped pkgs.lndinit
   since 2022 and is currently at 0.1.22-beta. There is no
   "missing attribute" risk.

4. Wallet data is forward-compatible. The wallet.db format is owned
   by LND, not lndinit. lndinit is only used at first boot to create
   the seed and initialize the wallet; subsequent LND restarts do
   not invoke lndinit. So even if a user already initialized a
   wallet with 0.1.3-beta, the binary being upgraded to 0.1.22-beta
   is irrelevant - LND owns the wallet from that point on.

5. Single call site. Only modules/bitcoin/lnd.nix references
   lndinit. No other modules, scripts, or tests need to change.

Operational notes
-----------------

* After this commit, lndinit updates flow through the normal
  `nix flake update` workflow (or whatever automated dependency
  tooling is already in use, e.g. for the recent "chore(deps):
  update RTL to 0.15.10" commits). No Sovran-side action is needed
  to pick up future lndinit versions.

* If a future LND version requires a specific lndinit version, the
  pin can be done in flake.nix via a one-line overlay:

      nixpkgs.overlays = [ (final: prev: {
        lndinit = prev.lndinit.overrideAttrs (o: {
          version = "X.Y.Z-beta";
          src = prev.fetchFromGitHub { ... };
          vendorHash = "...";
        });
      }) ];

  This keeps the upgrade path explicit without bringing the entire
  expression back into the Sovran tree.

* This drops ~20 lines of frozen derivation code, eliminates one
  source of upstream drift, and reduces the surface area of what
  Sovran needs to keep current.
2026-08-18 12:30:59 -05:00
naturallaw777 b6e6adbe31 chore(deps): update RTL to 0.15.10
Refresh the RTL source and Node dependency hashes, and align Hub version metadata with the vendored package.
2026-08-18 11:19:23 -05:00
naturallaw777 64624002bb fix(hub): reconcile completed updates after polling stalls
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.
2026-08-18 10:31:28 -05:00
naturallaw777 1ccce429a5 fix(bitcoin): migrate i2pd SAM settings for nixpkgs 26.11
nixpkgs commit c8f9654 refactored the services.i2pd module to use
an RFC42-style settings attribute set and removed services.i2pd.proto.
After updating the root nixpkgs input from f13ff45 to ec2d622, the
vendored bitcoind module failed evaluation on the obsolete
services.i2pd.proto.sam.enable definition.

The error occurred even with services.bitcoind.i2p at its false default:
bitcoind was enabled, so NixOS still validated the obsolete option path
inside the conditional i2pd integration.

Read the SAM endpoint from services.i2pd.settings.sam and configure its
new upstream-style fields explicitly. Keep 127.0.0.1:7656, matching the
old typed option defaults that bitcoind uses to generate its i2psam
setting.

This preserves optional I2P support without activating it by default.
i2pd remains disabled until services.bitcoind.i2p is set to true or
"only-outgoing".

Nixpkgs migration: https://github.com/NixOS/nixpkgs/commit/c8f965411e812060a9377fa4c2d7d0f84e8b10e0
2026-08-18 09:19:15 -05:00
naturallaw777 f832efb0d1 nixpkgs update 2026-08-18 08:59:35 -05:00
Sovran Systems 67ae53ad3f Add tor-browser to the package list 2026-08-17 19:10:19 -05:00
naturallaw777 db5b9f6b60 fix(livekit): order turn-setup after network-online to fix boot-time red dot
livekit-turn-setup.service detects the primary interface from the IPv4
default route, but had no ordering against network-online.target. With
NetworkManager+DHCP the default route is applied late at boot, so the
oneshot could run before it existed, exit 1, and — being a hard
dependency of livekit.service — take livekit down with it. The Hub then
showed a 'failed' red dot until livekit was restarted manually.

Order both livekit.service and livekit-turn-setup.service after
network-online.target. Also add a bounded retry when copying Caddy's ACME
cert so we never write an empty turn.crt/turn.key on a fresh boot.
2026-08-17 19:07:54 -05:00
Sovran Systems 2ea1427766 fix: restore a Zeus-scannable LND REST connect QR
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.
2026-08-17 18:22:36 -05:00
Sovran Systems b35b327a07 Merge pull request #435 from naturallaw777/security/lnd-macaroon-argv
security: prevent LND admin macaroon exposure in curl argv
2026-08-15 23:03:44 -05:00
naturallaw777 a9ff168fd6 security: prevent LND admin macaroon exposure in curl argv 2026-08-15 23:00:59 -05:00
naturallaw777 b436dbed95 docs: update desktop and Sovran Hub screenshots 2026-08-15 18:53:15 -05:00
naturallaw777 1cf611d44c chore(release): prepare v1.1.1 2026-08-15 17:49:55 -05:00
Sovran Systems 1e3dd2dd76 Update release-stable.sh 2026-08-15 17:48:45 -05:00
Sovran Systems 45322db41a Enhance release script with preflight checks and metadata
Refactor release script to improve preflight checks and metadata preparation. Update release steps for GitHub and Gitea.
2026-08-15 17:43:03 -05:00
naturallaw777 2a1d73af25 Migrate dock/folder/mime entries from brave to brave-origin on upgrade 2026-08-15 17:31:25 -05:00
Sovran Systems e83c7fdb18 Merge pull request #434 from naturallaw777/fix/hub-logout-persistence
fix(hub): persistent browser profile so logout survives window reopen
2026-08-15 17:24:14 -05:00
naturallaw777 587c19c2a5 fix(hub): persistent browser profile so logout survives window reopen
The Hub launcher used an ephemeral /tmp profile deleted on exit, which
wiped the hub_manual_logout marker cookie. On reopen, /auto-login minted a
new session and logged the user straight back in without a password.

Use a persistent per-user profile under XDG_STATE_HOME and drop the
deletion trap so the logout marker survives close/reopen. Keep
--skip-origin-startup-dialog. Adds regression tests.
2026-08-15 17:22:58 -05:00
naturallaw777 c862ed5806 Skip Brave Origin startup dialog in Hub launcher 2026-08-15 17:15:12 -05:00
Sovran Systems 1d79acb083 Merge pull request #433 from naturallaw777/brave-origin-default
Switch default browser to Brave Origin (brave-origin)
2026-08-15 17:00:24 -05:00
naturallaw777 5edf594eac Switch default browser to Brave Origin (brave-origin) 2026-08-15 16:58:33 -05:00
Sovran Systems 94dba571c1 Merge pull request #432 from naturallaw777/fix/hub-session-auth-recovery
fix(hub): recover from expired sessions and preserve logout
2026-08-15 16:45:24 -05:00
naturallaw777 de32699539 fix(hub): recover from expired sessions and preserve logout 2026-08-15 16:43:37 -05:00
Sovran Systems 892397589f Merge pull request #431 from naturallaw777/feat/bitcoin-tor-ibd-gossip
feat: add Bitcoin Core Tor IBD gossip control
2026-08-15 14:47:21 -05:00
naturallaw777 30b753ec40 feat: add Bitcoin Core Tor IBD gossip control 2026-08-15 14:44:13 -05:00
Sovran Systems e98a0b7554 Merge pull request #430 from naturallaw777/chore/bitcoin-core-only
Replace Bitcoin Knots with Bitcoin Core
2026-08-13 13:48:28 -05:00
naturallaw777 3541f6baa1 Replace Bitcoin Knots with Bitcoin Core 2026-08-13 13:43:17 -05:00
Sovran Systems e0ee8a50a4 Merge pull request #429 from naturallaw777/fix/hub-ui-preformance-improvement
fix: prevent Bitcoin Core switch from hanging the Hub UI
2026-08-11 18:50:00 -05:00
naturallaw777 8f89a4350a fix: prevent Bitcoin Core switch from hanging the Hub UI 2026-08-11 18:47:40 -05:00
naturallaw777 cb2b49174e docs: update CHANGELOG.md for v1.1.0 2026-08-11 13:51:41 -05:00
naturallaw777 6563acef0a docs: update README ISO download links to v1.1.0 2026-08-11 13:51:41 -05:00
naturallaw777 a053ee77fb chore: bump VERSION to v1.1.0 for ISO naming 2026-08-11 13:51:41 -05:00
Sovran Systems 9ce169c4f0 Merge pull request #428 from naturallaw777/fix/hub-load-performance
perf: speed up Hub service status loading
2026-08-11 13:37:29 -05:00
naturallaw777 61287dfece perf: speed up Hub service status loading 2026-08-11 13:35:59 -05:00
Sovran Systems f48f210209 Merge pull request #427 from naturallaw777/fix/hub-service-version-badges
fix: correct RTL and Mempool Hub versions
2026-08-11 13:09:48 -05:00
naturallaw777 4193c56397 fix: correct RTL and Mempool Hub versions 2026-08-11 13:07:28 -05:00
naturallaw777 f85f1a2c9f Update Documentation 2026-08-11 12:18:21 -05:00
Sovran Systems 802474fe6d Merge pull request #426 from naturallaw777/fix/ddns-validation-placeholder
Fix DDNS URL validation: replace ${IP} temporarily for validator, kee…
2026-08-11 11:51:53 -05:00
Arena Agent 679c7a039f Fix DDNS URL validation: replace ${IP} temporarily for validator, keep placeholder for storage 2026-08-11 11:47:32 -05:00
Copilot 1b43a34e9c Merge pull request #424 from naturallaw777/copilot/security-hardening-final
Security hardening: key removal, session expiry, DDNS validation, journal allowlist, and production-backed tests
2026-08-11 11:34:51 -05:00
copilot-swe-agent[bot]andnaturallaw777 3f233beea0 njalla.nix: fail on ImportError; fix redundant except clause
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 15:40:04 +00:00
copilot-swe-agent[bot]andnaturallaw777 947c04834d Fix all 8 security hardening blockers for PR #423
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 15:38:08 +00:00
naturallaw777 894707a87c Correctly escape DDNS placeholder in Nix string 2026-08-11 10:11:44 -05:00
naturallaw777 43fc01d350 Fix Nix interpolation in DDNS runner 2026-08-11 10:06:32 -05:00
naturallaw777 1f0a1ab865 Import consolidated security hardening from PRs #419 and #420 2026-08-11 09:57:06 -05:00
Sovran Systems 79f3e8efd8 Merge pull request #422 from naturallaw777/copilot/security-hardening-draft-pr-combined
Consolidate security hardening for DDNS, support access, auth gating, and upgrade migrations
2026-08-11 09:32:49 -05:00
copilot-swe-agent[bot] ff6abf3b8e Initial plan 2026-08-11 14:00:34 +00:00
copilot-swe-agent[bot]andnaturallaw777 a111de1ece Security hardening: fix all 8 blocking findings for PR #419
Fix 1: Update support.js to collect SSH public key and POST JSON
Fix 2: Legacy njalla.sh migration - parse safely, archive non-executable, replace cron with systemd timer
Fix 3: DDNS SSRF prevention - allowlist only njal.la, reject other hosts, disable curl redirects
Fix 4: Legacy root support-key removal migration (_remove_legacy_root_support_key)
Fix 5: Automatic support-key expiration (expires_at + _expire_support_if_stale)
Fix 6: Move security helpers to security_helpers.py, tests import production code
Fix 7: Real NIP-19/Bech32 npub validation (_bech32_decode + _validate_npub)
Fix 8: Replace journalctl sudo wildcard with restricted sovran-journal-helper.py
Also: Make _write_hub_overrides() atomic with tempfile+os.replace
94 tests passing

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 12:07:18 +00:00
copilot-swe-agent[bot]andnaturallaw777 9b77b04741 Fix IP validation in DDNS and document journalctl sudo rule
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 10:45:50 +00:00
copilot-swe-agent[bot]andnaturallaw777 f2ad9c1f17 Security hardening: fix DDNS injection, Nix injection, reboot auth, support key, sudo rules
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 10:44:26 +00:00
copilot-swe-agent[bot] 590ed134b7 Initial plan 2026-08-11 10:35:07 +00:00
Sovran Systems cc438aa524 Merge pull request #418 from naturallaw777/fix/btcpay-version-display
fix: report configured BTCPay Server version
2026-08-11 05:18:59 -05:00
Arena.ai Agent 0d0a1888f9 fix: report configured BTCPay Server version 2026-08-11 05:15:11 -05:00
Sovran Systems 14b8258420 Merge pull request #417 from naturallaw777/copilot/fix-nixos-service-startup-regressions-again
Fix Matrix SIGPIPE crash and RTL v0.15.8 config schema regressions
2026-08-10 22:43:24 -05:00
copilot-swe-agent[bot]andnaturallaw777 09b34997e4 Fix Matrix SIGPIPE and RTL v0.15.8 config schema regressions
- modules/synapse.nix: replace tr|head pipeline (causes SIGPIPE under
  set -euo pipefail) with pwgen -sA0 20 1 which is already in PATH
- modules/bitcoin/rtl.nix: lowercase Authentication->authentication and
  Settings->settings per RTL v0.15.8 schema; add lnServerUrl pointing
  to LND REST endpoint; move swapServerUrl/boltzServerUrl inside
  settings; apply same fixes to CLN branch

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 03:41:41 +00:00
copilot-swe-agent[bot] a372b88488 Initial plan 2026-08-11 03:39:21 +00:00
Sovran Systems 7716c950cd Merge pull request #415 from naturallaw777/copilot/fix-btcpay-runtime-regressions
fix(btcpay): NBXplorer cookie auth and WorkingDirectory for wwwroot
2026-08-10 22:25:27 -05:00
copilot-swe-agent[bot]andnaturallaw777 2c223d1166 fix: add NBXplorer cookie auth and WorkingDirectory for BTCPay service
- Add btcexplorercookiefile to BTCPay deterministic config so NBXplorer
  cookie authentication succeeds (fixes 401 Unauthorized)
- Set WorkingDirectory to package lib dir so ASP.NET Core can locate
  wwwroot and LanguageService.ctor does not throw ArgumentNullException
- Update regression test to assert cookie file path and WorkingDirectory

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 03:24:13 +00:00
copilot-swe-agent[bot] 503bd2467d Initial plan 2026-08-11 03:22:13 +00:00
Sovran Systems 57070644cc Merge pull request #414 from naturallaw777/copilot/fix-btcpay-startup-regression
Fix BTCPay Server startup crash: assign home directories to service users
2026-08-10 22:13:18 -05:00
copilot-swe-agent[bot]andnaturallaw777 1b6b1ade46 Fix BTCPay startup regression: add home dirs to service users
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 03:12:27 +00:00
copilot-swe-agent[bot] a7b149f58a Initial plan 2026-08-11 03:11:02 +00:00
Sovran Systems 90f89f14ba Merge pull request #413 from naturallaw777/copilot/harden-btcpay-nbxplorer-integration
Harden BTCPay/NBXplorer secret handling and deterministic config generation
2026-08-10 22:01:00 -05:00
copilot-swe-agent[bot]andnaturallaw777 dcfee1fb32 fix: address btcpay hardening review feedback
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 02:44:12 +00:00
copilot-swe-agent[bot]andnaturallaw777 05a42bcc4b feat: harden btcpay and nbxplorer config handling
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 02:40:16 +00:00
copilot-swe-agent[bot] 9befcd06e6 Initial plan 2026-08-11 02:34:27 +00:00
Sovran Systems c9169164b5 Merge pull request #412 from naturallaw777/copilot/fix-btcpayserver-hmac-authentication
fix(btcpayserver): register bitcoin-HMAC-btcpayserver as managed nix-bitcoin secret
2026-08-10 21:27:10 -05:00
copilot-swe-agent[bot]andnaturallaw777 cb84ca839f fix: register bitcoin-HMAC-btcpayserver as managed secret owned by bitcoind user
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-11 02:25:21 +00:00
copilot-swe-agent[bot] 6d59441b13 Initial plan 2026-08-11 02:24:45 +00:00
Copilot 5475e33d08 Merge pull request #410 from naturallaw777/copilot/409-update-fetch-node-modules
Unblock flake checks and document vendored nix-bitcoin packaging provenance
2026-08-10 21:07:38 -05:00
copilot-swe-agent[bot]andnaturallaw777 88896f00c4 Add provenance headers and fix flake checks for PR409
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-10 21:07:10 -05:00
Copilot cb2d6de0ef Merge pull request #409 from naturallaw777/copilot/fix-nixos-build-mempool-3-2-1
Restore upstream nix-bitcoin fetchNodeModules helper to fix mempool and RTL builds
2026-08-10 21:07:08 -05:00
copilot-swe-agent[bot]andnaturallaw777 c2dce6cf94 Restore upstream nix-bitcoin fetchNodeModules for mempool and RTL packages
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-10 23:24:31 +00:00
copilot-swe-agent[bot] 7879a2771d Initial plan 2026-08-10 23:21:00 +00:00
Sovran Systems 820d850d00 Refactor Nix build configuration for mempool 2026-08-10 18:07:46 -05:00
Sovran Systems d733187ec5 Update npm dependencies and patch handling in default.nix 2026-08-10 17:40:53 -05:00
Sovran Systems f7b1f42e0e Update npmDepsHash for backend and frontend packages 2026-08-10 17:33:21 -05:00
Sovran Systems 03fb7791eb Mempool: use postPatch to copy lock file for npmDeps fetcher
prefetch-npm-deps runs in the deps fetcher's buildPhase and checks for
package-lock.json at CWD (repo root). postPatch runs in both the
fetcher and the main build, so we copy the lock file there. The main
build then cds into the subdir in buildPhase.

npmDepsHash values remain placeholders (lib.fakeHash).
2026-08-10 17:21:42 -05:00
Sovran Systems 953cacc72c Mempool: use preBuild cd instead of postUnpack so npmDeps fetcher finds lock files
postUnpack did not run in the npmDeps fixed-output derivation, so
prefetch-npm-deps couldn't find package-lock.json. preBuild runs in
both the fetcher and main build, ensuring consistent CWD for the lock
file lookup. Backend patch moved to prePatch/postPatch so it only
applies in the main build.

npmDepsHash values remain placeholders (lib.fakeHash).
2026-08-10 17:15:07 -05:00
Sovran Systems 6021691b3f Mempool: fix npmDeps fetcher — postUnpack instead of sourceRoot, postPatch instead of patches
The `patches` attribute was being applied inside the npmDeps
fixed-output derivation where patch runs interactively and fails.
sourceRoot also caused the deps fetcher to hash a different tree than
expected. Now:

- postUnpack cds into the subdir for both the fetcher and main build
- postPatch applies the mining-pool patch only in the main build
- npmDepsHash values remain placeholders (lib.fakeHash)
2026-08-10 17:11:30 -05:00
Sovran Systems 4d8bb06d41 Mempool: switch backend and frontend to buildNpmPackage with placeholder npmDepsHash
npm ci in buildPhase hangs because the Nix build sandbox has no network
access. buildNpmPackage pre-fetches dependencies as a fixed-output
derivation with network access, then the main build runs offline.

npmDepsHash values are placeholders (lib.fakeHash) — the first builds
will fail with hash mismatches that report the correct hashes.
2026-08-10 17:07:02 -05:00
Sovran Systems b96d2bcb24 Update npmDepsHash with correct hash value 2026-08-10 12:46:02 -05:00
Sovran Systems 228a48af03 RTL: switch to buildNpmPackage with placeholder npmDepsHash
buildNpmPackage is built into nixpkgs and pre-fetches dependencies as a
fixed-output derivation with network access. npmDepsHash is a
placeholder (lib.fakeHash) — the first build will fail with a hash
mismatch that reports the correct hash.
2026-08-10 12:41:51 -05:00
Sovran Systems 0df4cde9b8 Fix RTL: use fetchNodeModules instead of npm ci in buildPhase
npm ci in buildPhase hangs because the Nix build sandbox has no network
access. Adopt nix-bitcoin's approach: fetchNodeModules pre-fetches
node_modules as a fixed-output derivation (which has network access),
and the main build copies them offline. Uses the same node_modules hash
as upstream nix-bitcoin for RTL 0.15.8.
2026-08-10 12:36:58 -05:00
Sovran Systems a37b1a91bf Fix PostgreSQL ensureUsers: use ensureDBOwnership instead of ensureClauses
ensureClauses generates ALTER ROLE clauses, not GRANT statements, so
'"DATABASE btcpayserver" = "ALL PRIVILEGES"' produced invalid SQL.
ensureDBOwnership = true makes each user own its database, which is
the intended effect.
2026-08-10 11:44:19 -05:00
Sovran Systems ebcc086a7e Fix lnd macaroons: replace invalid 'enable' with 'user'
The services.lnd.macaroons submodule has no 'enable' option — macaroons
are implicitly enabled by being defined. Replace 'enable = true' with
'user = cfg.btcpayserver.user' which is the correct option for
controlling macaroon file ownership.
2026-08-10 11:40:36 -05:00
Sovran Systems dcc9e4904e Add bitcoind.rpc.users.btcpayserver for NBXplorer
Define the btcpayserver RPC user in bitcoind with passwordHMACFromFile
and the full RPC whitelist required by NBXplorer.
2026-08-10 11:34:24 -05:00
Sovran Systems fd209254a2 Fix postgresql ensurePermissions -> ensureClauses for nixpkgs unstable
The option services.postgresql.ensureUsers.*.ensurePermissions was renamed
to ensureClauses in nixpkgs unstable. Update both btcpayserver and nbxplorer
database user configurations.
2026-08-10 11:21:10 -05:00
Sovran Systems a68130625e Fix btcpayserver.nix to use pkgs.stable overlay
Replace all pkgs-stable references with pkgs.stable (from overlay-stable in flake.nix):
- Remove pkgs-stable from function arguments
- Use pkgs.stable.nbxplorer (2.6.10 from nixos-26.05)
- Use pkgs.stable.btcpayserver (2.4.2 from nixos-26.05)
2026-08-10 11:16:26 -05:00
Sovran Systems 7b04adbbe6 Restore original flake.nix with nixosModules.Sovran_SystemsOS export
Restores the complete original flake.nix that includes:
- nixosModules.Sovran_SystemsOS (required by deployed machines)
- overlay-stable for accessing pkgs.stable packages
- nixosConfigurations.nixos for direct builds
- nixosConfigurations.sovran_systemsos-iso for ISO builds
- nixosTests and checks
2026-08-10 11:12:21 -05:00
Sovran Systems 2c6ef7b60e Fix btcpayserver syntax error and pin to nixpkgs-stable (2.4.2)
1. flake.nix: Add pkgs-stable as specialArg for modules
2. btcpayserver.nix:
   - Fix preStart script syntax error (closing brace on new line)
   - Use pkgs-stable.btcpayserver (2.4.2) instead of pkgs.btcpayserver
   - Use pkgs-stable.nbxplorer (2.6.10) instead of pkgs.nbxplorer
   - Remove clightning references (Sovran is LND-only)
2026-08-10 10:56:51 -05:00
Sovran Systems 179fc501f9 Add vendored RTL package and fix rtl.nix to use it
- Create packages/rtl/default.nix to build RTL from source
- Update modules/bitcoin/rtl.nix to use vendored package instead of pkgs.rtl
- Remove clightning/lightning-loop references (Sovran is LND-only)
- Simplify to LND-only configuration
2026-08-10 10:38:20 -05:00
Sovran Systems 8bcabe2aa1 Fix mempool package: remove fetchNodeModules dependency
Replace fetchNodeModules (nix-bitcoin specific) with standard npm ci approach.
The package-lock.json files in the mempool repo will be used directly.
2026-08-10 10:25:39 -05:00
Sovran Systems 5d5e369411 Merge pull request #408 from naturallaw777/copilot/vendor-mempool-packages
Vendor mempool packages and make `services.mempool` self-contained
2026-08-10 10:09:33 -05:00
copilot-swe-agent[bot]andnaturallaw777 425a1845b4 chore: clean mempool module comment typo
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-10 15:07:42 +00:00
copilot-swe-agent[bot]andnaturallaw777 546dacf396 vendor mempool packages and wire mempool module to vendored pkgs
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-10 15:06:50 +00:00
copilot-swe-agent[bot] 17c0db8290 Initial plan 2026-08-10 15:03:18 +00:00
Sovran Systems dac733de9a Refactor onion service configuration for LND 2026-08-10 09:55:59 -05:00
Sovran Systems 3baf504c60 Fix typo in onion-addresses service check 2026-08-10 09:52:55 -05:00
Sovran Systems f220b93b12 Add missing bitcoind-rpc-public-whitelist.nix
This file is imported by bitcoind.nix line 375 but was missing from the repo.
Vendored from nix-bitcoin to make the module self-contained.
2026-08-10 09:45:23 -05:00
Sovran Systems b2a5233653 Merge pull request #407 from naturallaw777/copilot/fixnix-bitcoin-vendoring
Complete nix-bitcoin vendoring for fully self-contained Bitcoin modules
2026-08-10 09:41:26 -05:00
copilot-swe-agent[bot] 2bb5c50196 Initial plan 2026-08-10 14:30:27 +00:00
Sovran Systems 80c4b052a0 Merge pull request #406 from naturallaw777/fix/onion-services-remove-joinmarket-ob-watcher
Remove joinmarket-ob-watcher from onion-services defaults
2026-08-10 09:15:26 -05:00
Sovran Systems 2accada021 Remove joinmarket-ob-watcher from onion-services defaults
The joinmarket-ob-watcher module was not imported from nix-bitcoin,
so referencing it in the onion-services defaults caused an
"attribute 'joinmarket-ob-watcher' missing" evaluation error.
2026-08-10 09:13:33 -05:00
Sovran Systems a6296b9473 Fix formatting of extraGroups in btcpayserver.nix 2026-08-10 08:55:12 -05:00
Sovran Systems 5173fb3df6 Merge pull request #404 from naturallaw777/fix/syntax-final
fix: add missing semicolon after extraGroups in btcpayserver.nix
2026-08-10 08:53:36 -05:00
Syntax Fix 147afcc4ac fix: add missing semicolon after extraGroups in btcpayserver.nix
- Previous lnd-only refactor removed the '++ optional clightning' line
  but left 'extraGroups = [ cfg.nbxplorer.group ]' without trailing ';'
  -> syntax error: unexpected '=', expecting ';' at home = ...
- Add ';' to fix build on both f13ff45 and 8b8c811
2026-08-10 08:51:39 -05:00
Sovran Systems 2e35539ae6 Merge pull request #403 from naturallaw777/tailor/lnd-only-final
tailor: make bitcoin modules truly Sovran-only (lnd-only) and delete …
2026-08-10 08:40:08 -05:00
Sovran Tailor 31cd5f96f2 tailor: make bitcoin modules truly Sovran-only (lnd-only) and delete stubs.nix
- lndconnect.nix: rewrite to lnd-only (320 lines removed) - remove
  services.clightning.plugins.clnrest.lnconnect and
  services.clightning-rest.lndconnect (Sovran only uses services.lnd.lndconnect for Zeus)
  Fixes: 'services.clightning.plugins.clnrest.address does not exist' on f13ff45
  and 'attribute enable missing' at lndconnect.nix:216

- btcpayserver.nix: remove liquidd (lbtc) dead branch - Sovran uses lbtc=false
  * remove inherit (config.services) bitcoind liquidd -> just bitcoind
  * lbtc chains/rpc handling is dead code, keep guards but no need for liquidd service

- mempool.nix: remove fulcrum branch - Sovran uses electrs only
- rtl.nix: remove lightning-loop block and clightning service block (lnd only)
- Delete modules/bitcoin/stubs.nix entirely - no dead service references left
  * liquidd, fulcrum, lightning-loop, joinmarket now not referenced at all
  * clightning already handled via guards, not stubs
- Result: modules/bitcoin/ is now 100% tailored to Sovran (bitcoind+knots, lnd, electrs, rtl, btcpayserver, mempool)
  No more stubs whack-a-mole on any nixos-unstable
2026-08-10 08:37:29 -05:00
Sovran Systems 90262e672e Merge pull request #402 from naturallaw777/refactor/clean-bitcoin
refactor: move vendor/nix-bitcoin to modules/bitcoin, remove overlays
2026-08-10 08:24:07 -05:00
Sovran Clean 1fbeafd02f refactor: move vendor/nix-bitcoin to modules/bitcoin, remove overlays
- Move modules/vendor/nix-bitcoin/* -> modules/bitcoin/* (clean, Sovran-owned)
  * modules/bitcoin/default.nix imports the 6 tailored services
  * modules/bitcoin/common.nix bundles secrets/onion/lib
  * modules/bitcoin/stubs.nix kept minimal (no clightning)
  * packages/lndinit/default.nix replaces pkgs/sovran-overlay.nix
    (lnd.nix now uses pkgs.callPackage ../../packages/lndinit {})
- Remove pkgs/sovran-overlay.nix, pkgs/nbxplorer.nix, pkgs/README.md
  * No global overlay - lndinit is a normal package in packages/
- Remove modules/vendor/ entirely
- Update flake.nix: drop overlay-sovran, import ./modules/bitcoin
  instead of ./modules/vendor/nix-bitcoin/modules.nix
- No more random vendor/ or pkgs/ dirs - follows Sovran convention:
  modules/ for NixOS modules, packages/ for packages
2026-08-10 08:20:40 -05:00
Sovran Systems 953cedce3f Merge pull request #401 from naturallaw777/fix/final-btcpayserver
fix: remove services.clightning.enable assignment that fails on f13ff45
2026-08-10 04:45:38 -05:00
Sovran Fix 58d7e21d28 fix: remove services.clightning.enable assignment that fails on f13ff45
- f13ff45 HAS services.clightning but only as { plugins = ... }, no enable
  -> 'option does not exist, did you mean plugins' (your error)
- Sovran never uses lightningBackend == clightning (uses lnd),
  so just don't set services.clightning.enable at all
- Also make lnd port conflict check always true for clightning case
- Fixes both f13ff45 (has clightning/plugins) and 8b8c811 (removed)
2026-08-10 04:43:20 -05:00
Sovran Systems 9f7dcfa6de Merge pull request #400 from naturallaw777/fix/final-clightning-both
fix: remove clightning and clightning-rest from stubs to avoid duplic…
2026-08-10 04:35:03 -05:00
Sovran Fix 6c2c69d18a fix: remove clightning and clightning-rest from stubs to avoid duplicate on f13ff45
- f13ff45 (staging-dev) HAS both services -> unconditional stubs duplicate
- Remove both from stubs.nix, keep only liquidd/fulcrum/etc.
- lndconnect.nix left as is (defines clightning-rest.lndconnect) - safe on f13ff45 where base exists
- On 8b8c811 where clightning is removed, Sovran doesn't use it anyway (lnd only), so guards in btcpayserver/lnd prevent use
2026-08-10 04:31:23 -05:00
Sovran Systems 6b9c68e3b2 Merge pull request #399 from naturallaw777/fix/nixpkgs-unstable-clightning-2026-08
fix: add stubs for nixpkgs-unstable 2026-08 where services.clightning…
2026-08-09 21:23:01 -05:00
Sovran Fix 4b511348ae fix: add stubs for nixpkgs-unstable 2026-08 where services.clightning removed
nixpkgs 8b8c811 (2026-08-08) removed services.clightning.enable,
causing btcpayserver.nix:124 to throw 'option does not exist' on
nixos-rebuild (your error). Sovran uses lnd only, never clightning,
but evaluation still throws.

- Add modules/vendor/nix-bitcoin/stubs.nix to provide missing options
  as false stubs: clightning, clightning-rest, liquidd, fulcrum,
  lightning-loop/pool, joinmarket
- Guard btcpayserver/rtl/lnd/mempool clightning/liquidd references
  with config.services ? X checks
- Trim enable-tor.nix onionServices for removed services
2026-08-09 21:21:19 -05:00
Sovran Systems 81946895db Merge pull request #398 from naturallaw777/vendor/nixpkgs-only
Vendor/nixpkgs only
2026-08-09 20:24:11 -05:00
naturallaw777 a10ef95b5c chore: nix flake update - drop nix-bitcoin 2026-08-09 20:19:35 -05:00
Sovran PR Bot 278d480653 vendor: replace nix-bitcoin flake input with minimal vendored modules (nixpkgs-only)
- Remove inputs.nix-bitcoin (fort-nix/nix-bitcoin/release) from flake.nix
- Vendor only 6 services actually used by Sovran: bitcoind, electrs,
  lnd (+lndconnect), rtl, btcpayserver, mempool + supporting infra:
  secrets, onion-services/addresses, operator, nodeinfo, security,
  versioning
- All packages now from nixpkgs directly (pkgs.*) — no pinned pkgs
- Keep nix-bitcoin.* option namespace for compatibility
- backups.nix removed: Sovran uses rsnapshot to Second_Drive
  (configuration.nix: hourly/daily to BTCEcoandBackup) — duplicity
  remote backup not needed
- netns-isolation.nix replaced with stub (5 lines): original 365-line
  bridge/iptables/ip-netns broke Caddy/AlbyHub/RTL a year ago and
  is incompatible with nwc-wallets (requires enable=false). Stub
  keeps option valid but warns if enabled.
- Add pkgs/sovran-overlay.nix for gaps only: lndinit + netns-exec stub
2026-08-09 20:16:34 -05:00
naturallaw777 b64061135a fix(scripts): keep ISO artifacts out of the repo and auto-update README on release 2026-08-07 18:31:20 -05:00
Sovran Systems 825c3fbb4b Remove duplicate 1.0.6 release notes from CHANGELOG
Remove duplicate version 1.0.6 details from CHANGELOG.md.
2026-08-07 16:17:24 -05:00
naturallaw777 8b169a04e3 chore: release v1.0.6 2026-08-07 16:11:59 -05:00
naturallaw777 d0c2ea933c docs: update CHANGELOG.md for v1.0.6 2026-08-07 16:03:21 -05:00
naturallaw777 b66c50c195 fix(scripts): resolve tag range detection and improve release diagnostics 2026-08-07 16:02:09 -05:00
naturallaw777 1737e6c7d8 fix(scripts): add token scope diagnostics and un-silence gh release errors 2026-08-07 15:59:22 -05:00
naturallaw777 782b279e0a docs: update CHANGELOG.md for v1.0.6 2026-08-07 15:55:26 -05:00
naturallaw777 564e9835ea chore: bump VERSION to v1.0.6 for ISO naming 2026-08-07 15:55:26 -05:00
Sovran Systems 3b8f809e7c Merge pull request #397 from naturallaw777/fix/security-error-hardening
fix: harden error handling and sanitize exception details across secu…
2026-08-07 15:38:49 -05:00
naturallaw777 9df52256d4 fix: harden error handling and sanitize exception details across security endpoints (CWE-209) 2026-08-07 15:36:44 -05:00
Sovran Systems de481db97c Merge pull request #396 from naturallaw777/fix/exception-exposure-5351
fix: sanitize exception handling in verify-integrity and security-res…
2026-08-07 15:00:10 -05:00
naturallaw777 9051aed737 fix: sanitize exception handling in verify-integrity and security-reset (CWE-209) 2026-08-07 14:58:24 -05:00
Sovran Systems 65376ec8fb Merge pull request #395 from naturallaw777/fix/exception-exposure-5252
fix: sanitize api_security_reset errors to prevent exception informat…
2026-08-07 14:40:08 -05:00
naturallaw777 899f570ded fix: sanitize api_security_reset errors to prevent exception information exposure (CWE-209) 2026-08-07 14:31:38 -05:00
Sovran Systems d3b41b14fb Merge pull request #394 from naturallaw777/fix/path-injection-4338
fix: pass sanitized abs_path to os.chown to resolve CodeQL path injec…
2026-08-07 14:23:57 -05:00
naturallaw777 56db634900 fix: use canonical prefix containment check for CodeQL path-injection 2026-08-07 14:22:44 -05:00
naturallaw777 3522270373 fix: pass sanitized abs_path to os.chown to resolve CodeQL path injection at 4338 2026-08-07 14:17:34 -05:00
Sovran Systems efda70c187 Merge pull request #393 from naturallaw777/fix/path-injection-4334-4532
fix: add CodeQL-recognized path sanitization for domain_name
2026-08-07 13:51:28 -05:00
copilot-swe-agent[bot]andnaturallaw777 a32b353eda fix: resolve merge conflict with origin/main in server.py
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-07 18:50:01 +00:00
naturallaw777 4555764d87 fix: add CodeQL-recognized path sanitization for domain_name 2026-08-07 13:44:26 -05:00
Sovran Systems c38d4be851 Merge pull request #392 from naturallaw777/fix/path-injection-4334-4532
fix: add CodeQL-recognized path sanitization for domain_name
2026-08-07 13:36:03 -05:00
naturallaw777 5130076400 fix: add CodeQL-recognized path sanitization for domain_name 2026-08-07 13:33:55 -05:00
Sovran Systems 1dbbd6a943 Merge pull request #391 from naturallaw777/fix/njalla-sentinel-4371
fix: remove domain substring check for CodeQL incomplete-url
2026-08-07 13:28:14 -05:00
naturallaw777 f0a640519d fix: remove domain substring check for CodeQL incomplete-url 2026-08-07 13:26:41 -05:00
naturallaw777 a14ef03d40 fix: use sentinel for njalla header check (CodeQL incomplete-url-substring) 2026-08-07 13:07:06 -05:00
Sovran Systems a0cc7e6bec Merge pull request #390 from naturallaw777/fix/xss-lnurl-qr-print-5013
fix: prevent reflected XSS in lnurl-qr print endpoint
2026-08-07 13:00:46 -05:00
naturallaw777 1ecf245a07 fix: prevent reflected XSS in lnurl-qr print endpoint 2026-08-07 12:59:08 -05:00
Sovran Systems f8730cc0ab Merge pull request #389 from naturallaw777/fix/clear-text-root-password
fix: hash root password instead of storing in clear text (CWE-312)
2026-08-07 12:39:48 -05:00
Contributor 8a766181de fix: hash root password instead of storing in clear text (CWE-312)
- 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.
2026-08-07 12:34:13 -05:00
naturallaw777 bd0d2cd812 removed temp patch file 2026-08-07 12:16:11 -05:00
naturallaw777 592f2bd12f fix: separate web auth hash from system password file
- Add FREE_PASSWORD_FILE_WEB for scrypt hashes
- Legacy fallback + auto-migrate in _check_password
- chpasswd sync in api_change_password and security reset endpoint
2026-08-07 12:04:07 -05:00
Sovran Systems b7bba228fe Merge pull request #387 from naturallaw777/fix/matrix-hub-service-admin
Fix Matrix Hub service-admin provisioning
2026-08-07 11:10:48 -05:00
Sovran Systems 3010557064 Fix Matrix Hub admin API credentials 2026-08-07 16:09:13 +00:00
Sovran Systems 69f64c18d1 Merge pull request #386 from naturallaw777/copilot/fix-code-scanning-alerts
Fix CWE-78 command injection in Matrix create-user endpoint
2026-08-07 10:13:04 -05:00
copilot-swe-agent[bot]andnaturallaw777 942da64332 Fix CWE-78: replace subprocess call with Synapse Admin API in create-user endpoint
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-08-07 15:09:36 +00:00
copilot-swe-agent[bot] bae790ebb4 Initial plan 2026-08-07 15:07:32 +00:00
naturallaw777 501c5abe64 docs: add SECURITY.md detailing security policy and best practices 2026-08-06 14:09:35 -05:00
Sovran Systems 63f1069e4a Merge pull request #385 from naturallaw777/arena/019fd2b5-sovran-systemsos
docs: versioned CDN downloads + add CDN upload script
2026-08-05 11:18:26 -05:00
naturallaw777andarena-agent f2842ffafb docs: versioned CDN downloads + add CDN upload script
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-05 16:16:59 +00:00
Sovran Systems 591ae766d2 Merge pull request #384 from naturallaw777/arena/019fd28e-sovran-systemsos
iso: fix replaceStrings length mismatch breaking ISO build
2026-08-05 10:39:02 -05:00
naturallaw777andarena-agent f81b27cc19 iso: fix replaceStrings length mismatch in cleanVersion
builtins.replaceStrings requires the 'from' and 'to' lists to have the
same length. The 'to' list had a single empty string while 'from' had
three entries (v, newline, CR), which made evaluating image.baseName
fail with: 'from' and 'to' arguments passed to builtins.replaceStrings
have different lengths. Add the two missing empty strings.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-05 15:38:21 +00:00
Sovran Systems dda6fb8e91 Merge pull request #383 from naturallaw777/arena/019fcf25-sovran-systemsos
Promote virtual machine trial option in README
2026-08-04 19:43:06 -05:00
naturallaw777andarena-agent cf250604f9 Promote virtual machine trial option in README
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-05 00:42:42 +00:00
Sovran Systems 963d41f1ed Merge pull request #382 from naturallaw777/arena/019fce1d-sovran-systemsos
Improve installer VM compatibility
2026-08-04 14:08:45 -05:00
naturallaw777andarena-agent 5188cca9aa Improve installer VM compatibility
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-04 19:07:51 +00:00
Sovran Systems f39f59fb81 Merge pull request #381 from naturallaw777/arena/019fcde4-sovran-systemsos
Trim redundant dev-vs-stable Gitea explanations in README
2026-08-04 13:24:52 -05:00
naturallaw777andarena-agent 61fc61cceb Trim redundant dev-vs-stable Gitea explanations in README
Replace repeated prose across 4 sections with a compact table + flow arrow:
- Top callout: 7-line paragraph → 2-line summary + link
- Development workflow: 2 bullets + 3-step list + warning → 3-row table + flow arrow
- Build from source: re-explained sync relationship → single labels on clone commands
- Contributing footer: repeated branch/host info → one sentence

No information lost; ~270 words removed.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-04 18:23:54 +00:00
Sovran Systems 4b9800aef3 Merge pull request #380 from naturallaw777/arena/019fcdbe-sovran-systemsos
docs: correct active development workflow
2026-08-04 12:17:34 -05:00
naturallaw777andarena-agent c447674ef9 docs: correct active development workflow
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-04 17:16:46 +00:00
Sovran Systems a77bfc5070 Merge pull request #379 from naturallaw777/arena/019fcda3-sovran-systemsos
docs: clarify GitHub dev-mirror workflow and Gitea stable home
2026-08-04 12:04:46 -05:00
naturallaw777andarena-agent 7b381d0387 docs: make repo references mirror-neutral for Gitea readers
The README and CONTRIBUTING are mirrored to both Gitea branches, so
GitHub-specific references now name the GitHub repository explicitly
instead of saying 'this repository'.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-04 17:03:52 +00:00
naturallaw777andarena-agent ec43d631d4 docs: clarify GitHub is the dev mirror of the Gitea stable repo
The GitHub repo mirrors Gitea's staging-dev branch; changes are tested
here and promoted to the stable branch on the self-hosted Gitea instance.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-04 16:39:27 +00:00
Sovran Systems 82a9952e63 Add Arean.Ai to AI tools used for development
Updated the mention of AI tools used in development.
2026-08-04 11:33:50 -05:00
arena-ai-coding-agent[bot] 1eb4be2c01 Merge pull request #378 from naturallaw777/arena/019fcd8d-sovran-systemsos
Move OS version badge inline right after the Hub title
2026-08-04 16:31:20 +00:00
naturallaw777andarena-agent 6f1edb938e Prefix OS version badge with 'v' (v1.0.5)
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>
2026-08-04 16:29:52 +00:00
naturallaw777andarena-agent bd96782229 Match OS version badge styling to service modal version badges
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>
2026-08-04 16:27:36 +00:00
naturallaw777andarena-agent cf23438a5c Place OS version badge inline right after the Hub title
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>
2026-08-04 16:21:39 +00:00
Sovran Systems d2c3fd066e Merge pull request #377 from naturallaw777/arena/019fcd82-sovran-systemsos
Center OS version badge under the Hub title
2026-08-04 11:05:33 -05:00
naturallaw777andarena-agent 1bfec68d77 Center OS version badge under the Hub title
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-04 16:04:56 +00:00
Sovran Systems bda1372a1d Merge pull request #376 from naturallaw777/arena/019fcd69-sovran-systemsos
Add 'About Bitcoin wallet entropy' section to README
2026-08-04 10:59:09 -05:00
naturallaw777andarena-agent 13c700ca44 Strengthen entropy section: add DYOR emphasis and hardware wallet verification note
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-04 15:51:39 +00:00
naturallaw777andarena-agent f6f87d6b03 Add 'About Bitcoin wallet entropy' section to README
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-04 15:48:01 +00:00
Sovran Systems 4f75117540 Merge pull request #375 from naturallaw777/arena/019fcd5a-sovran-systemsos
README: balance intro between Bitcoin sovereignty and sovereign computing
2026-08-04 10:27:07 -05:00
naturallaw777andarena-agent 2bb02e50d9 README: balance intro between Bitcoin sovereignty and sovereign computing
- Replace tagline with 'Bitcoin sovereignty. Sovereign computing. One system.'
- Open with both pillars as 'inseparable freedoms'
- Extend growth path beyond node/infrastructure to include private cloud and comms
- Remove standalone 'Privacy. Sovereignty. Bitcoin.' from intro (branding holds it at page close)

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-08-04 15:26:27 +00:00
naturallaw777 4d1c3565c9 docs: update CHANGELOG.md for v1.0.5 2026-08-04 09:33:09 -05:00
naturallaw777 3678aedc88 chore: bump VERSION to v1.0.5 for ISO naming 2026-08-04 09:33:09 -05:00
naturallaw777 991c790816 Nixpkgs Update 2026-08-04 09:07:09 -05:00
naturallaw777 a343afffaa Nixpkgs Upddate 2026-08-03 18:06:18 -05:00
Sovran Systems eeef81ffc7 Merge pull request #374 from naturallaw777/arena/019fb590-sovran-systemsos
Update LND REST Zeus Connect instructions and design to match NWC
2026-07-30 19:35:40 -05:00
naturallaw777andarena-agent bc6b3f0843 Update LND REST Zeus Connect instructions and design to match NWC exactly
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-31 00:33:22 +00:00
Sovran Systems f01e776f34 Merge pull request #373 from naturallaw777/arena/019fb564-sovran-systemsos
UI: Align LND REST and NWC Zeus connection instructions styling
2026-07-30 18:53:28 -05:00
naturallaw777andarena-agent a19fa53468 UI: Align LND REST and NWC Zeus connection instructions styling
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-30 23:44:20 +00:00
Sovran Systems baefca0c68 Merge pull request #372 from naturallaw777/arena/019fb535-sovran-systemsos
Coherent Zeus LND REST / NWC Zeus connect instructions
2026-07-30 18:28:41 -05:00
naturallaw777andarena-agent 9ed6040425 Make Zeus LND REST instructions coherent with NWC Zeus connect guide
- 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>
2026-07-30 23:26:37 +00:00
Sovran Systems ca17b381f6 Merge pull request #371 from naturallaw777/arena/019fb509-sovran-systemsos
Clarify Zeus NWC wallet setup
2026-07-30 17:37:27 -05:00
naturallaw777andarena-agent 3f74388359 Clarify Zeus NWC wallet setup
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-30 22:36:16 +00:00
Sovran Systems a814ed332f Merge pull request #370 from naturallaw777/arena/019fb4e9-sovran-systemsos
Manual Backup: match the system role (Desktop Only scope fix)
2026-07-30 16:50:21 -05:00
naturallaw777andarena-agent 94e4412e67 Make Manual Backup match the system role (Desktop Only scope)
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>
2026-07-30 21:49:04 +00:00
Sovran Systems 670cfb9acd Merge pull request #369 from naturallaw777/arena/019fb396-sovran-systemsos
Seat new domains in running Caddy + unify Njal.la/router guidance across onboarding & feature modals
2026-07-30 11:52:43 -05:00
naturallaw777andarena-agent 2015a3ecd4 Reload Caddy immediately when a domain (or ACME email) is saved
The Caddyfile is generated at runtime by caddy-generate-config.service
from /var/lib/domains/*, but the generator only re-runs when caddy.service
starts fresh. Saving a domain while Caddy is already running therefore
never seats the new virtual host — no proxying and no ACME cert — and the
Hub's reachability check shows a misleading 'ports 80/443' router error
until the next reboot or rebuild.

api_domains_set and api_domains_set_email now restart the generator and
reload Caddy (ExecReload: caddy reload --force, no dropped connections)
right after saving. Entirely skipped when Caddy is inactive — e.g. Node
role before its first domain-based service is enabled — because the
rebuild that enables the service starts caddy.service for the first time,
runs the generator first (requiredBy), and seats the already-saved domain
on its own. Best-effort throughout: a domain save never fails because of
a Caddy reload issue.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-30 16:50:32 +00:00
naturallaw777andarena-agent 2622223033 Unify Njal.la + router port-forwarding guidance across onboarding and feature enable modals
Single source of truth (static/js/domain-prereqs.js) for the domain
prerequisite instructions so all three surfaces read identically:

- Server + Desktop first-boot onboarding wizard (step 3)
- Lightning Wallet Connections (NWC) enable modal (Node-only mode)
- BTCPay Server (web) enable modal (Node-only mode)

Both feature-enable flows share openDomainSetupModal(), which now drops
its role-branched intro and 'Option A/B' blocks in favor of the shared
renderers. All surfaces now consistently cover:

  1. A domain from Njal.la (account, subdomain-vs-separate-domain,
     Dynamic record with host-part-only Name field, auto-filled IP,
     DDNS curl command)
  2. Router access — forward ports 80 & 443 (TCP) to this computer's
     internal IP, once, for HTTPS/SSL (with CGNAT note)
  3. How to get Njal.la working, step by step

The reconfigure/troubleshooting modal gains the same router reminder,
since 'domain not reachable' is often the port forwarding rather than
DNS. domain-prereqs.js is loaded via asset_version cache busting and
ships automatically (installPhase copies the package wholesale).

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-30 16:45:36 +00:00
Sovran Systems d0ec6dd760 Merge pull request #368 from naturallaw777/arena/019fb370-sovran-systemsos
Enhance Lightning Wallet Connect modal UI/UX
2026-07-30 09:47:45 -05:00
naturallaw777andarena-agent 90b1c45a0d Enhance Lightning Wallet Connect modal with professional benefits grid and refined messaging
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-30 14:46:09 +00:00
Sovran Systems 0ac1529a1d Merge pull request #367 from naturallaw777/arena/019faeb9-sovran-systemsos
docs: real v1.0.4 changelog + auto-generated release notes in release script
2026-07-29 11:38:42 -05:00
naturallaw777andarena-agent 1b579271e5 docs: write real v1.0.4 changelog and auto-generate release notes in release script
- CHANGELOG.md: replace v1.0.4 placeholder section with the actual changes
  (Lightning Wallet Connections/NWC, Hub version badges, backup overhaul,
  automated releases, security hardening, fixes)
- release-stable.sh: generate categorized Keep-a-Changelog release notes
  from commits since the last tag (feat/fix/docs/security grouping),
  let the user review/edit before publishing, and use the same notes for
  CHANGELOG.md, the GitHub release, and the Gitea release
- Fix broken /api/ path in the v1.0.4 changelog release link
- Build Gitea API payload with jq/python so multi-line notes are JSON-safe

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 16:36:35 +00:00
Sovran Systems c6b4b89be3 Merge pull request #366 from naturallaw777/arena/019fae93-sovran-systemsos
fix(hub): resolve 'vdev' version badge and align it under the Hub title
2026-07-29 11:07:52 -05:00
naturallaw777andarena-agent a8ff366992 fix(hub): resolve 'vdev' version badge and align it under the Hub title
- The Hub header badge could render 'vdev' because the runtime
  /etc/nixos/VERSION lookup fell back to the literal string 'dev' when
  the file was missing (e.g. dev/test environments, or before the
  Nix-generated config carried a version at all).
- modules/core/sovran-hub.nix now reads the repo's VERSION file at
  Nix eval time and bakes a real semantic version (sovran_version)
  into the generated config.json and a VERSION file shipped with the
  package, so the Hub always has a solid value to display.
- server.py's _get_sovran_version() now reads that baked-in
  sovran_version first, and explicitly rejects a literal 'dev' value
  from any of its file-based fallbacks so the badge never shows
  'vdev' again.
- templates/index.html + header.css: wrapped the title and the
  version badge in a '.title-group' column so the version badge sits
  directly underneath 'Sovran_SystemsOS Hub', left-aligned with it.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 16:06:49 +00:00
Sovran Systems ddba59cce8 Merge pull request #365 from naturallaw777/arena/019fae73-sovran-systemsos
fix(scripts): auto-detect git remotes and clean backup files in release-stable.sh
2026-07-29 10:30:10 -05:00
naturallaw777andarena-agent e12e9ba01b fix(scripts): align release script with Gitea staging-dev and stable branch workflow
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 15:28:50 +00:00
naturallaw777andarena-agent 853c3edf92 fix(scripts): auto-detect git remotes and clean up temp files in release-stable.sh
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 15:22:58 +00:00
naturallaw777 4a6ba3eacc docs: update CHANGELOG.md for v1.0.4 2026-07-29 10:19:00 -05:00
naturallaw777 537d4781f2 chore: bump VERSION to v1.0.4 for ISO naming 2026-07-29 10:19:00 -05:00
Sovran Systems ae473b6041 Merge pull request #364 from naturallaw777/arena/019fae70-sovran-systemsos
fix: fetch tags with --force to prevent clobber errors
2026-07-29 10:15:49 -05:00
naturallaw777andarena-agent a92dd19d98 fix: use --force on git fetch to avoid tag clobber errors on gitea
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 15:15:24 +00:00
Sovran Systems e18a59404c Merge pull request #363 from naturallaw777/arena/019fae58-sovran-systemsos
feat: Automated stable releases, versioned ISOs, and polished Hub version badge
2026-07-29 10:12:40 -05:00
naturallaw777andarena-agent c54823ff68 feat: automated stable releases, versioned ISOs, and Hub version badge
- Add release-stable.sh script with automatic tagging, CHANGELOG updates,
  GitHub/Gitea release creation, and VERSION file management
- Update iso/common.nix to include version from VERSION file in ISO filename
- Add VERSION file (current: 1.0.3)
- Polish OS version badge in Sovran Hub header (top-right)
- Update README.md download link to versioned ISO
- Update CHANGELOG.md with existing tags (v1.0.0 – v1.0.3)

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 15:11:59 +00:00
Sovran Systems ac6a2e26bb Merge pull request #362 from naturallaw777/arena/019fae4f-sovran-systemsos
Show deployed PHP app versions in service titles
2026-07-29 09:45:19 -05:00
naturallaw777andarena-agent 199bde0bc6 Show deployed PHP app versions in service titles
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 14:39:21 +00:00
Sovran Systems 17797d55bc Merge pull request #361 from naturallaw777/arena/019fae31-sovran-systemsos
Fix Tile Versioning with Build-Time Reference File
2026-07-29 09:19:49 -05:00
naturallaw777andarena-agent 310731b3e0 Expose and beautifully render service version badges next to modal window titles
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 14:17:29 +00:00
naturallaw777andarena-agent dec0aebc95 Introduce build-time version reference file to make versioning instantaneous and solid
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 14:10:55 +00:00
Sovran Systems 9df83e8a11 Merge pull request #360 from naturallaw777/arena/019fade2-sovran-systemsos
Make service version metadata less prominent
2026-07-29 08:50:00 -05:00
naturallaw777andarena-agent 97990f5092 Make service version metadata less prominent
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 13:49:24 +00:00
Sovran Systems 1c0ef15855 Merge pull request #359 from naturallaw777/arena/019faddd-sovran-systemsos
docs: acknowledge LiveKit and Alby Hub
2026-07-29 07:36:04 -05:00
naturallaw777andarena-agent e9f39a7b21 docs: acknowledge LiveKit and Alby Hub
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 12:35:38 +00:00
Sovran Systems 0283cbb049 Merge pull request #358 from naturallaw777/arena/019fadc2-sovran-systemsos
Improve service version details and naming
2026-07-29 07:18:55 -05:00
naturallaw777andarena-agent 4210ba899f Improve service version details and naming
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 12:18:33 +00:00
Sovran Systems 415a036858 Merge pull request #357 from naturallaw777/arena/019fabe6-sovran-systemsos
docs: add router and ISP port forwarding requirements for Server + Desktop
2026-07-28 22:26:57 -05:00
naturallaw777andarena-agent 236baf66e6 docs: add router and ISP port forwarding requirements for Server + Desktop mode
Add clear guidance in the recommended hardware section explaining that
Server + Desktop mode requires router admin panel access with port
forwarding capability and ISP support for port forwarding. Framed as
empowering guidance rather than barriers to minimize onboarding friction.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 03:26:27 +00:00
Sovran Systems f37654f3ce Merge pull request #356 from naturallaw777/arena/019fabd4-sovran-systemsos
docs: restructure README — tighter, linked, and accurate
2026-07-28 22:18:34 -05:00
naturallaw777andarena-agent fb6bff085e docs: restructure README for clarity, links, and accuracy
- Rework intro to co-headline Bitcoin self-custody and digital sovereignty
- Add table of contents
- Merge overlapping sections (middle-man pitch, day-one tools, differentiators)
  into a single 'Why Sovran_SystemsOS?' section; describe each app once
- Collapse Desktop/Node/Server hardware lists into one comparison table
- Explain sovransystemsos.local once; other sections link to the Hub section
- Compress install guide (7 steps to 6) with collapsible per-OS verify blocks
- Link every upstream project at first mention
- Map features to module files in a table for developers
- Fix clone URL (naturallaw777/Sovran_SystemsOS)
- Remove repo-map rows for docs/wallet-connections.md and role-state.nix;
  note install-time generated files imported by flake.nix
- Add packages/ to repo map; list Alby Hub, Mempool, Haven where relevant
- Fix H1/H2 heading hierarchy and deduplicate footer slogans

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 03:16:53 +00:00
Sovran Systems 020462509d Merge pull request #355 from naturallaw777/arena/019fabce-sovran-systemsos
fix(nwc-wallets): replace incorrect lib.mkIf usage with plain if expressions
2026-07-28 21:59:05 -05:00
naturallaw777andarena-agent 1e4a988ca2 fix(nwc-wallets): replace incorrect lib.mkIf usage with plain if expressions
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 02:58:33 +00:00
Sovran Systems 037a3a00af Merge pull request #354 from naturallaw777/arena/019fabb1-sovran-systemsos
security: harden Lightning Wallet Connections (NWC)
2026-07-28 21:54:23 -05:00
naturallaw777andarena-agent 2e2a9b2d44 security: harden Lightning Wallet Connections (NWC)
- Add rate limiting to public LNURL endpoints (30 req/min per IP)
- Add audit logging for wallet lifecycle events (create, drain, delete, rotate)
- Add Unix socket support for Python ↔ Alby Hub communication
- Add LND macaroon permission documentation/warning
- Add pairing secret rotation API endpoint + CLI command
- Make Nostr relay configurable; auto-use Haven relay when enabled
- Strengthen domain validation (FQDN only, reject localhost/IP)
- Add structured audit log at /var/log/sovran-nwc-audit.log

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 02:52:45 +00:00
Sovran Systems 2f46f8994d Merge pull request #353 from naturallaw777/arena/019fabbe-sovran-systemsos
feat: display version numbers on all Hub service tiles
2026-07-28 21:44:46 -05:00
naturallaw777andarena-agent 43764e8c8d feat: display version numbers on all Hub service tiles
Previously only bitcoind.service showed a version on the tile.
Now every service with a parsable Nix store path gets its version
displayed (electrs, btcpayserver, lnd, rtl, mempool, vaultwarden,
matrix-synapse, caddy, tor, etc.).

The existing _get_service_version() helper reads systemctl show
<unit> --property=ExecStart and extracts the version from the Nix
store path. Results are cached for 5 minutes per unit.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 02:44:03 +00:00
Sovran Systems adde78320d Merge pull request #352 from naturallaw777/arena/019fab62-sovran-systemsos
Rename NWC feature to Lightning Wallet Connections and split modal into tabs
2026-07-28 21:02:21 -05:00
naturallaw777andarena-agent aa4ffaa803 Rename NWC feature to Lightning Wallet Connections and split modal into tabs
Naming: user-facing 'Wallet Connections' -> 'Lightning Wallet Connections'
across the Hub, feature registry, tile, and NixOS modules. Internal ids
(nwc-wallets, albyhub.service, /api/nwc/*) are unchanged.

UX: the service-detail modal put status, domain diagnostics, router ports,
the enable/disable toggle, restart, the liquidity guide and the whole wallet
manager in one cramped scrolling column. For this feature the modal is now
980px wide and split into two tabs:

  - Wallets: wallet grid, create/share/verify flows, collapsible liquidity guide
  - Service & Setup: description, status, domain checklist, ports, enable, restart

A status dot and domain chip sit in the tab bar so state is visible from both
tabs, and the modal opens on Setup when the service is off or the Lightning
Address domain is unconfigured. Wallet cards gain a balance chip, pending
badge, a prominent address row, and separated destructive actions.

Non-NWC services keep the original single-column layout and width.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 01:59:28 +00:00
Sovran Systems 7d92026303 Merge pull request #351 from naturallaw777/arena/019fab50-sovran-systemsos
Add NWC Channel & Liquidity Guide UI/UX for LND Nodes
2026-07-28 19:46:09 -05:00
naturallaw777andarena-agent 9ed4409e25 Add NWC channel liquidity guide and UI/UX explanation for new LND nodes
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-29 00:45:06 +00:00
Sovran Systems 57555fc5dd Merge pull request #350 from naturallaw777/arena/019faaea-sovran-systemsos
NWC: downloadable & printable LNURL QR for wallet connections
2026-07-28 18:53:00 -05:00
naturallaw777andarena-agent 5e3432aa89 nwc: add downloadable/printable LNURL QR for wallet connections
Each NWC wallet connection gets its own shareable LNURL QR so the owner
can receive payments from anyone without creating invoices (the core
LNURL value proposition: a static QR that never expires).

Hub UI (service detail modal):
- New 'Share QR' button on every wallet connection card
- Dedicated share view: large QR, Lightning Address + copy, raw bech32
  LNURL + copy
- Download PNG (websites/social), download SVG (vector, print-sharp at
  any size), and a Print button that opens a print-ready payment card
  which auto-invokes the browser print dialog

Backend (FastAPI, session-authenticated):
- GET /api/nwc/wallets/{id}/lnurl -> alias, lightning_address, LNURL
- GET /api/nwc/wallets/{id}/lnurl-qr.png (?download=1, ?scale=)
- GET /api/nwc/wallets/{id}/lnurl-qr.svg (?download=1)
- GET /api/nwc/wallets/{id}/lnurl-qr/print (print-ready HTML card)
- Stdlib bech32 encoder (BIP-173, verified against the official LUD-01
  golden vector); LNURL is uppercased for QR alphanumeric-mode density
  per LUD-01 guidance; QR rendered with the existing qrencode package
- Resolves wallets by app id or pubkey; 404 unknown wallet, 503 when no
  Lightning domain is configured; all routes return 401 unauthenticated

No new system dependencies.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-28 23:47:37 +00:00
Sovran Systems 5ab2e432f1 Merge pull request #349 from naturallaw777/arena/019faad3-sovran-systemsos
Port-forward UX: remove tile Step 4 and misleading local port 'ready' status
2026-07-28 17:36:45 -05:00
naturallaw777andarena-agent 419680a847 Port-forward UX: drop tile Step 4 and misleading local 'ready' status
The Element Call tile still appended a synthetic 'Step 4: Router Setup
Needed' to the domain diagnostic checklist, and every port table carried a
'Sovran_SystemsOS Status' column with Ready / Not ready yet verdicts.

Both were misleading: port forwarding happens on the router, which this
computer cannot inspect. A local ss/firewall probe can neither prove nor
disprove that forwarding works — and the LiveKit TURN relay range binds on
demand, so it reported 'Not ready yet' even on a perfectly working system.

- server.py: add ROUTER_FORWARD_ONLY_UNITS ({livekit.service}); skip the
  local probe for those units, drop the step-4 append, replace extra_ports
  with router_ports (no status field), and exclude router-only ports from
  both tile health and /api/ports/health so they can't raise false alarms.
- helpers.js: new shared renderPortForwardGuideHtml() — one intro naming the
  internal IP, explicit instructions (same internal/external port, match the
  protocol, use port-range fields for 30000-40000), a colour-coded
  TCP / UDP / TCP+UDP badge per row, and a closing note that the only real
  test is loading the service from a phone on mobile data.
- features.js: enable-time modal uses the shared guide and now always lists
  every port to forward (the old local pre-filter hid ports the user still
  had to open).
- service-detail.js: tile port section uses the same guide; the SSH/non-domain
  branch keeps a small 'not open on this computer yet' hint, which is a real
  local fact, separate from router forwarding.
- onboarding.js: step 3 router note reworded to match (same number for
  internal/external, notes that Element Call adds UDP ports).
- domain-setup.css: styles for the protocol badges and instruction list.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-28 22:34:39 +00:00
Sovran Systems 0e3cab78f8 Merge pull request #348 from naturallaw777/arena/019faa9a-sovran-systemsos
Simplify port-forward UX (drop onboarding step 4) + run Njal.la DDNS on feature enable
2026-07-28 16:41:52 -05:00
naturallaw777andarena-agent a65d977489 Simplify port-forward UX (drop onboarding step 4) + run Njal.la DDNS on feature enable
- Onboarding: remove the redundant/error-prone 'Router Setup' step (5 steps -> 4).
  A compact 80/443 (+22 SSH) note now lives inside Domain Configuration,
  and the Element Call ports are only shown at the moment they matter:
  when enabling the feature, and afterwards on the service tile.
- Onboarding step 3: fix domain prefill bug (API returns {domains: {...}}),
  make /api/network fetch best-effort so it can never block the step.
- Enable-time port modal: streamline copy (one intro + table + pointer to
  the tile's live status view).
- Element Call tile detail: replace 5 repetitive prose blocks with 2 compact
  notes around the live-status port table.
- Njal.la DDNS: run njalla.sh immediately when a DDNS-backed feature is
  enabled (previously only ran on domain save or the 15-min cron tick).
- Harden njalla.sh handling: create the base script (shebang + IP lookup)
  if missing before appending curl lines; invoke via bash explicitly.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-28 21:38:07 +00:00
Sovran Systems 2077cbd0fb Merge pull request #347 from naturallaw777/arena/019faa75-sovran-systemsos
Show NWC manager in Wallet Connections modal
2026-07-28 15:45:48 -05:00
naturallaw777andarena-agent a4f848fc6d Show NWC manager in Wallet Connections modal
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
2026-07-28 20:44:44 +00:00
naturallaw777 1a6e456d06 Updated Contributing 2026-07-27 18:22:40 -05:00
naturallaw777 ccda5e3246 Updated Readme 2026-07-27 18:04:17 -05:00
naturallaw777 aef7582083 Added contributing and updated readme 2026-07-27 17:56:38 -05:00
Sovran Systems c51a762b42 Merge pull request #346 from naturallaw777/staging-dev
Merge into Main
2026-07-27 13:33:56 -05:00
Sovran Systems 91bb49ea13 Merge pull request #345 from naturallaw777/staging-dev
Merge staging-dev into main
2026-07-27 12:21:15 -05:00
naturallaw777 0f8c15e6a1 removed .tests, not needed 2026-07-27 10:35:54 -05:00
naturallaw777 586625f110 removed .github, not needed 2026-07-27 10:34:33 -05:00
naturallaw777 c78370647a removed nix directory, not needed 2026-07-27 10:33:32 -05:00
naturallaw777 e30610ddac removed docs/ai added not necessary 2026-07-27 10:29:34 -05:00
naturallaw777 1503060f7c fixed duplicate systemd lnd strings 2026-07-27 09:40:32 -05:00
Sovran Systems 016ceaa3df Merge pull request #344 from naturallaw777/copilot/fix-wallet-connections-cert-path
Complete Wallet Connections cert-path wiring for interactive `nwc-wallet`
2026-07-27 09:34:03 -05:00
copilot-swe-agent[bot] bfa4f2f9e4 Fix NWC wallet cert-path env wiring 2026-07-27 14:29:27 +00:00
copilot-swe-agent[bot] f557a6d2db Initial plan 2026-07-27 14:26:00 +00:00
Sovran Systems 69c236d9ca Merge pull request #343 from naturallaw777/copilot/fix-wallet-connections-integration
Use nix-bitcoin `services.lnd.certPath` for Wallet Connections LND setup
2026-07-27 09:18:40 -05:00
copilot-swe-agent[bot] d6546bfed8 fix: use nix-bitcoin lnd cert path for wallet connections 2026-07-27 14:16:21 +00:00
copilot-swe-agent[bot] d97615db8e Initial plan 2026-07-27 14:13:14 +00:00
Sovran Systems f3c05bf7db Merge pull request #342 from naturallaw777/copilot/fix-lnd-alby-hub-port-collision
Resolve deterministic LND/Alby Hub port collision and enforce loopback-only binding
2026-07-27 13:34:13 +00:00
copilot-swe-agent[bot] 004424cc2b Address validation feedback for Nix test and workflow permissions 2026-07-27 12:23:06 +00:00
copilot-swe-agent[bot] 37a15afa10 Fix deterministic LND/Alby Hub port collision 2026-07-27 12:21:09 +00:00
copilot-swe-agent[bot] fdd9d29db3 Initial plan 2026-07-27 12:13:11 +00:00
Sovran Systems c9458dd73b Merge pull request #341 from naturallaw777/copilot/require-unique-hostname-for-wallet-connections
Require unique hostname for Wallet Connections; add server-side conflict validation
2026-07-27 11:49:44 +00:00
copilot-swe-agent[bot] dc98a355e8 Fix redundant backslash escape in hostname regex character class 2026-07-27 11:41:44 +00:00
copilot-swe-agent[bot] 154988bbeb Address review feedback: fix hostname regex, use CSS class for NWC warning banner, clarify test comment 2026-07-27 11:39:40 +00:00
copilot-swe-agent[bot] 768f26027e Require unique hostname for Wallet Connections; add conflict validation and UI guidance 2026-07-27 11:36:02 +00:00
copilot-swe-agent[bot] 007bf1a4ce Initial plan 2026-07-27 11:29:47 +00:00
Sovran Systems 5a91fa4de5 Merge pull request #340 from naturallaw777/copilot/regenerate-alby-hub-patches
fix: regenerate Alby Hub v1.23.0 patches against exact upstream source
2026-07-27 10:58:10 +00:00
copilot-swe-agent[bot] c449044dcf fix: regenerate Alby Hub v1.23.0 patches against exact upstream source
Patch 1 (0001-private-route-hints.patch):
- Add missing RouteHints: hints, context line (v1.23.0 added this field
  between Expiry and Private; old patch lacked it, causing hunk failure)
- Fix amountMsat parameter name in hunk header (was 'amount' from older version)
- Only regular invoice Private field is changed; MakeHoldInvoice keeps
  Private: !hasPublicChannels unchanged

Patch 2 (0002-isolated-invoice-app-id.patch):
- Fix CreateInvoice interface: amountMsat uint64 (not amount uint64)
- Fix MakeInvoiceRequest: Amount/AmountSat/AmountMsat are *uint64 pointers
  (not Amount uint64) matching v1.23.0 struct layout
- Fix MakeInvoice call: 9 args → 10 args with appId as 8th positional arg
  (nil, nil trailing) matching v1.23.0 signature
- Fix function impl parameter: amountMsat uint64 throughout

Both patches dry-run verified against exact v1.23.0 source and apply cleanly
in sequence. Updated NixPatchContractTests to assert v1.23.0 context
(RouteHints, amountMsat, *uint64 pointer types, 10-arg MakeInvoice call).

All 233 Python tests pass. JS syntax checks pass.
2026-07-27 10:49:25 +00:00
copilot-swe-agent[bot] 614f750032 Initial plan 2026-07-27 10:42:29 +00:00
Sovran Systems 127a10fc17 Merge pull request #337 from naturallaw777/copilot/urgent-fix-alby-hub-api
Restore Wallet Connections to exact proven Alby Hub/LND contract and remove placeholder packaging
2026-07-27 05:17:10 +00:00
Sovran Systems 0fac2f9c68 Merge pull request #339 from naturallaw777/copilot/update-alby-hub-executable-logo
Port PR #338 blockers, switch Alby Hub to `lib.getExe`, and replace Wallet Connections branding with official NWC logo
2026-07-27 05:11:24 +00:00
copilot-swe-agent[bot] b9485c18dc Fix AlbyHub executable and switch Wallet Connections to official NWC logo 2026-07-27 05:10:07 +00:00
copilot-swe-agent[bot] 14c6861918 Initial plan 2026-07-27 05:00:37 +00:00
Sovran Systems 2c2b889f49 Merge pull request #338 from naturallaw777/copilot/copiloturgent-fix-alby-hub-api
fix(nwc): credential access, strict LNURL amount validation, partial-funding wording
2026-07-27 04:27:45 +00:00
copilot-swe-agent[bot] ecd83a8262 fix: address code review — restrict ReadOnlyPaths to unlock-password, clarify amount error message
ReadOnlyPaths for nwc-lnurl.service now lists only the specific file
/var/lib/albyhub/unlock-password (least-privilege) instead of the
whole /var/lib/albyhub directory.

Amount-duplicate error message changed to "A single amount parameter is
required" (clearer for 2+ values than "Exactly one").

Test assertion updated to match new message text.
2026-07-27 04:09:41 +00:00
copilot-swe-agent[bot] 2f744c0850 fix: resolve final PR #337 blockers — credential access, amount validation, wording
Blocker 1: run nwc-lnurl.service as albyhub user/group so it can read
/var/lib/albyhub/unlock-password (mode 0600, dir mode 0700).
Remove the now-unused nwc-lnurl user, group, and /var/lib/nwc-lnurl
state directory.  ReadOnlyPaths updated to allow the whole albyhub dir.

Blocker 2: require exactly one amount query parameter in the LNURL
callback HTTP handler.  Duplicate values now return a 400 protocol
error ("Exactly one amount parameter is required") before the helper is
called.  The missing-amount and non-integer paths are unchanged.

Blocker 3: partial-funding failure message now reads "was created
successfully" instead of "already exists" to avoid confusion with a
duplicate-name error, while retaining the warning not to recreate.

Tests added:
- LnurlHandlerAmountTests — HTTP-handler level tests for duplicate (2×,
  3×), single-valid, and missing amount parameters.
- test_create_partial_failure_message_says_created_successfully —
  asserts exact wording of the partial-funding message.
- test_nwc_lnurl_service_runs_as_albyhub — asserts the Nix service block
  sets User/Group to albyhub.
- test_nwc_module_no_separate_nwc_lnurl_user — asserts no standalone
  nwc-lnurl user/group is declared.

All 229 Python tests pass (1 skipped).  JS syntax clean.  No secrets.
2026-07-27 04:07:53 +00:00
copilot-swe-agent[bot] cca681979a Initial plan 2026-07-27 04:03:01 +00:00
copilot-swe-agent[bot] 82ee21f13f Align Wallet Connections with proven Alby Hub API contract 2026-07-27 03:41:41 +00:00
copilot-swe-agent[bot] f24be16b98 Initial plan 2026-07-27 03:34:02 +00:00
Sovran Systems 4213c20537 Merge pull request #336 from naturallaw777/copilot/replace-wallet-connections-scaffolding
Replace Wallet Connections scaffolding with real Alby Hub/LND backend
2026-07-27 03:27:03 +00:00
copilot-swe-agent[bot] 0e13779773 Address code review feedback: BOLT11 regex, error messages, PORT env var, gofmt, test helpers
- nwc_hub_manager.py: Fix BOLT11 regex to accept only valid prefixes (bc/tb/bcrt/tbs); add path to timeout error message; add early-exit to _get_app_pending_txs pagination
- nwc_lnurl_service.py: Allow NWC_LNURL_PORT env var override for port
- nwc-wallets.nix: Remove || true from gofmt so build fails on syntax errors
- test_wallet_connections.py: Extract _call_body() helper; use c.args[] access
2026-07-27 03:21:40 +00:00
copilot-swe-agent[bot] 5ebd51e5f9 Fix nwc-lnurl Python path, improve patches, expose hub package option
- sovran-hub.nix: Add nwc-lnurl launcher binary to sovran-hub-web derivation (same pattern as nwc-wallet); add options.services.sovranHub.webPackage to expose derivation to other modules
- nwc-wallets.nix: Use config.services.sovranHub.webPackage for LNURL ExecStart (fixes sys.path for sovran_systemsos_web imports); replace no-op placeholder patches with real unified diff format showing intended Go changes (Private: true, AppId *uint)
2026-07-27 03:18:30 +00:00
copilot-swe-agent[bot] dcc6d9fc1d Fix critical Authorization header bug and CodeQL stack-trace exposure
- nwc_hub_manager.py: Fix Authorization header to use real ****** (was hardcoded to literal asterisks due to display redaction)
- server.py: Use exc.args[0] instead of str(exc) in NWC error handlers to prevent CodeQL stack-trace taint flow to HTTP responses; update albyhub.service description key
2026-07-27 03:13:28 +00:00
copilot-swe-agent[bot] ccff377607 Replace Wallet Connections scaffolding with real Alby Hub/LND implementation
- Add nwc_hub_manager.py: AlbyHubManager with real Alby Hub API (setup, auth, CRUD, drain, delete, invoice)
- Add nwc_lnurl_service.py: dedicated loopback LNURL service on port 8181
- server.py: remove JSON scaffolding (state.json, fake invoice generator, fake NWC URI, LNURL routes); replace with real manager calls; update service maps to albyhub.service; remove LNURL auth-exempt paths
- nwc_wallet_cli.py: rewrite to use real AlbyHubManager instead of JSON state
- modules/nwc-wallets.nix: replace with albyhub user/service, nwc-lnurl service, LND macaroon, unlock-password generation
- modules/core/caddy.nix: proxy LNURL routes to port 8181 (dedicated service) instead of 8937 (Hub)
- modules/core/sovran-hub.nix: service tile points to albyhub.service
- docs/wallet-connections.md: document real architecture, Alby Hub pin/patches, backup sensitivity
- test_wallet_connections.py: replace scaffolding tests with 54 real manager tests using mocked Alby Hub
2026-07-27 03:10:13 +00:00
copilot-swe-agent[bot] 63c87c8fb4 Initial plan 2026-07-27 02:52:54 +00:00
Sovran Systems 9c3743724c Merge pull request #335 from naturallaw777/copilot/implement-hub-managed-wallet-connections
Add Hub-managed Wallet Connections (`nwc-wallets`) scaffold across Hub, Nix, domain, and LNURL paths
2026-07-27 02:32:03 +00:00
copilot-swe-agent[bot] 0e94ae58fc Implement wallet connections service-detail modal CRUD flow 2026-07-27 02:22:45 +00:00
copilot-swe-agent[bot] 9673f6733a feat: add hub-managed Wallet Connections scaffolding 2026-07-27 02:10:11 +00:00
copilot-swe-agent[bot] 5860f40e82 Initial plan 2026-07-27 01:58:07 +00:00
naturallaw777 72f4a7b39e updated nixpkgs and btc clients 2026-07-25 09:37:18 -05:00
Sovran Systems ba10f8956b Merge pull request #334 from naturallaw777/copilot/fix-manual-backup-rsync-failure
Fix rsync destination-directory failure: sync_tree with mkdir -p, mount re-verification, and path-safety guards
2026-07-21 00:06:54 +00:00
copilot-swe-agent[bot] 0961f89b9f Fix CodeQL false-positive: rename secret.key test fixture to bitcoin-key.txt 2026-07-21 00:04:58 +00:00
copilot-swe-agent[bot] 0c90c8a827 Fix rsync destination-directory failure: add sync_tree with mkdir -p, mount check, path-safety, stale-marker cleanup, and 19 behavioral tests 2026-07-21 00:03:12 +00:00
copilot-swe-agent[bot] 539d2bc48d Initial plan 2026-07-20 23:56:15 +00:00
Sovran Systems c61eabe21a Merge pull request #333 from naturallaw777/copilot/replace-manual-backup-implementation
Replace Manual Backup: tar+DB+LND → ext4+rsync mirror
2026-07-20 22:51:38 +00:00
copilot-swe-agent[bot] 228de54d62 refactor: rename CURRENT_DIR_NAME to BACKUP_SUBPATH for clarity 2026-07-20 22:47:53 +00:00
copilot-swe-agent[bot] 1a3c014183 feat: replace tar+DB+LND backup with ext4+rsync workflow
- Rewrite sovran-hub-backup.sh: rsync-based mirror to stable current/
  path, ext4 validation, no tar/pg_dump/mariadb-dump/LND orchestration,
  exit-24 nonfatal for /home, INCOMPLETE/BACKUP_COMPLETE markers, flock
- Update server.py: _is_supported_backup_fstype accepts only ext4
- Update support.js: require ext4, explain database limitations, update
  failure message from exFAT to ext4
- Update sovran-hub.nix: replace pkgs.gnutar with pkgs.rsync + pkgs.acl
- Rewrite test_manual_backup_workflow.py: 40 new tests covering rsync
  options, ext4 acceptance, exit-24 handling, no-delete, stable current/
  path, INCOMPLETE markers, behavioral rsync tests, and regressions
2026-07-20 22:45:04 +00:00
copilot-swe-agent[bot] 3d7f5410d4 Initial plan 2026-07-20 22:34:57 +00:00
naturallaw777 e0656b6cc0 nixpkgs update 2026-07-19 16:59:19 -05:00
Sovran Systems ec3397afa9 Merge pull request #332 from naturallaw777/copilot/fix-manual-backup-failure
Fix home.tar exit-1 failure on active desktop; harden full backup workflow
2026-07-18 18:21:07 +00:00
copilot-swe-agent[bot] c5982ccb14 Final polish: explicit LOCK_FD close, tar --list SIGPIPE comment, marker timing comment, _extract_bash_function docstring 2026-07-18 18:11:19 +00:00
copilot-swe-agent[bot] 9785b8717d Address code review round 2: Python regex for function extraction, bash 4.1+ comment, find -print0 for manifest, mktemp 0600 comment 2026-07-18 18:08:33 +00:00
copilot-swe-agent[bot] 9169e59780 Address code review: source allowlist function from script in tests, fix find exclusions for BACKUP_COMPLETE, simplify log message 2026-07-18 18:05:46 +00:00
copilot-swe-agent[bot] 98d17423ff Fix test assertion pattern and replace em dashes in new log messages 2026-07-18 18:02:41 +00:00
copilot-swe-agent[bot] 2d48bd8d77 Fix home.tar exit-1 failure: add tar tolerance, partial atomics, INCOMPLETE marker, flock, browser cache exclusions 2026-07-18 18:00:29 +00:00
copilot-swe-agent[bot] 28f1f4fe74 Initial plan 2026-07-18 17:44:54 +00:00
Sovran Systems a5a1afcd5a Merge pull request #331 from naturallaw777/copilot/fix-manual-backup-failure
Fix Manual Backup exit-code-127: add bash+gawk to service PATH, harden launcher
2026-07-18 16:40:29 +00:00
copilot-swe-agent[bot] 9f917d69da Address code review: compute stderr_text only when chunks are present 2026-07-18 16:30:38 +00:00
copilot-swe-agent[bot] 98f63fde73 Fix Manual Backup exit-code-127: add bash+gawk to service PATH, harden launcher 2026-07-18 16:29:10 +00:00
copilot-swe-agent[bot] a1d8d3d443 Initial plan 2026-07-18 16:23:49 +00:00
Sovran Systems 5da1b6d2ac Merge pull request #330 from naturallaw777/copilot/implement-manual-backup-workflow
Implement reliable exFAT Manual Backup with tar artifacts, DB exports, and lifecycle hardening
2026-07-18 15:31:38 +00:00
copilot-swe-agent[bot] 994a5ca1cd Refine backup validation and manifest details 2026-07-17 17:00:32 +00:00
copilot-swe-agent[bot] 9a76e71dd0 Address validation feedback for backup workflow 2026-07-17 16:58:34 +00:00
copilot-swe-agent[bot] 08f650cc35 Implement reliable exFAT manual backup workflow 2026-07-17 16:56:08 +00:00
copilot-swe-agent[bot] ce4a8f5a70 Initial plan 2026-07-17 16:49:35 +00:00
Sovran Systems 3f6c032ec9 Merge pull request #329 from naturallaw777/copilot/fix-nix-build-regression
Fix `sovran-hosts-update` ShellCheck build regression from `writeShellApplication`
2026-07-16 20:40:41 +00:00
copilot-swe-agent[bot] 24e3049b28 test: harden flake configuration detection 2026-07-16 20:38:48 +00:00
copilot-swe-agent[bot] d2c50038b3 test: derive nix helper build attr from flake 2026-07-16 20:37:48 +00:00
copilot-swe-agent[bot] e18ea204bf fix: group sovran hosts append redirection 2026-07-16 20:36:33 +00:00
copilot-swe-agent[bot] 233a6e17a8 Initial plan 2026-07-16 20:33:53 +00:00
Sovran Systems 32d6955ed6 Merge pull request #328 from naturallaw777/copilot/fix-sovran-hosts-update-runtime-dependency
fix(local-domain-loopback): replace raw /etc script with writeShellApplication, declare explicit runtimeInputs
2026-07-16 20:28:39 +00:00
copilot-swe-agent[bot] f9c42f4519 fix: convert sovran-hosts-update to writeShellApplication with explicit runtimeInputs
- Replace environment.etc raw script with pkgs.writeShellApplication
- Declare runtimeInputs: pkgs.coreutils, pkgs.gawk, pkgs.gnugrep
- Use awk -v for safe marker variable passing (no shell interpolation)
- Point systemd ExecStart and activation script at lib.getExe hostsUpdateScript
- Keep /etc/sovran-hosts-update.sh as a source symlink for operator discoverability
- Remove environment.systemPackages reliance
- Emit warning (not silently swallow) on activation failure
- Add structural regression tests (19 new tests, all passing)
2026-07-16 20:26:59 +00:00
copilot-swe-agent[bot] 907808823e Initial plan 2026-07-16 20:23:44 +00:00
naturallaw777 d99d7213ec updated readme 2026-07-15 15:25:14 -05:00
Sovran Systems 2df1805ce9 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] d91664fcb9 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] 19fc9fc9c9 Initial plan 2026-07-15 18:55:54 +00:00
Sovran Systems 79d902e341 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] aaf34de72a refactor: improve variable name in openRestartConfirmDialog for clarity 2026-07-15 16:51:52 +00:00
copilot-swe-agent[bot] 8869f16fec 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] 2bdedf80d7 Initial plan 2026-07-15 16:41:20 +00:00
Sovran Systems 6c722035c5 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] dcd699d31e 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] 5b3a6c4be8 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] b4ff21e1ce Initial plan 2026-07-15 15:14:53 +00:00
Sovran Systems 9d23acaf8d 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] f9a140f119 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] 49728fbc84 Initial plan 2026-07-14 15:51:34 +00:00
Sovran Systems 4b2fd7455c 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] cf53c870b6 style: fix spelling authorisation → authorization in test comment 2026-07-14 00:29:26 +00:00
copilot-swe-agent[bot] 3da6eff9fa 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] 92d09a6060 Initial plan 2026-07-14 00:26:18 +00:00
Sovran Systems ac9439535a 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] cc46098296 fix: apply hub-managed GRD credentials at boot 2026-07-13 23:54:08 +00:00
copilot-swe-agent[bot] 54bea9bb5c fix: tighten GRD boot credential setup 2026-07-13 23:48:12 +00:00
copilot-swe-agent[bot] bbcb5b8b47 Initial plan 2026-07-13 23:25:36 +00:00
Sovran Systems 75eeb69e9c 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] c22ff00abf feat(element-calling): universal LiveKit interface detection, JWT/Caddy fixes 2026-07-13 18:24:43 +00:00
copilot-swe-agent[bot] ac1aff9d7f Initial plan 2026-07-13 18:20:58 +00:00
naturallaw777 08f9bf9850 nixpkgs update and btc client update 2026-07-13 13:02:42 -05:00
naturallaw777 0988a3fb6d Merge remote-tracking branch 'github/main' into staging-dev 2026-07-13 12:46:51 -05:00
Sovran Systems a2999315d4 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] 4be0ea11ff Adjust update status docstring ordering 2026-07-13 17:41:36 +00:00
copilot-swe-agent[bot] 644baeb29b Polish staged-update test and log header formatting 2026-07-13 17:40:29 +00:00
copilot-swe-agent[bot] 47ecc7b9a4 Stage Hub full updates for next reboot 2026-07-13 17:39:14 +00:00
copilot-swe-agent[bot] 3cf55e53a5 Initial plan 2026-07-13 17:36:29 +00:00
naturallaw777 f9044466a1 Update nixpkgs 2026-07-04 11:25:52 -05:00
Sovran Systems e125c2747e 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] b7e79010cf 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] 0eba3c406b Initial plan 2026-07-03 23:33:55 +00:00
Sovran Systems 40fd142523 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] 84490e8d94 Fix TemplateResponse calls to use Starlette 1.1.0+ keyword-argument style 2026-06-30 17:37:21 +00:00
copilot-swe-agent[bot] f85633c9ba Initial plan 2026-06-30 17:35:17 +00:00
naturallaw777 4aaf10f3a0 Update nixpkgs 2026-06-30 12:14:53 -05:00
naturallaw777 83c9acb511 updated nixpkgs and btc clients 2026-06-25 16:26:43 -05:00
Sovran Systems 32f9c1a692 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 3b784cfd47 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] 346798b6c2 fix: simplify internal IP copy handling 2026-06-24 22:19:14 +00:00
copilot-swe-agent[bot] a440d199f1 fix: clarify router forwarding IP guidance 2026-06-24 22:17:13 +00:00
copilot-swe-agent[bot] 8c062dfc26 chore: use flock for ssh bootstrap repair 2026-06-24 22:16:44 +00:00
copilot-swe-agent[bot] 5875c971cf chore: serialize ssh bootstrap key repairs 2026-06-24 22:16:05 +00:00
copilot-swe-agent[bot] 9dd4568449 chore: harden ssh bootstrap script 2026-06-24 22:15:24 +00:00
copilot-swe-agent[bot] ece53d313d chore: log legacy ssh key regeneration 2026-06-24 22:14:42 +00:00
copilot-swe-agent[bot] 42a373e50e Initial plan 2026-06-24 22:14:35 +00:00
copilot-swe-agent[bot] d1bca416cf fix: repair legacy factory ssh key passphrases 2026-06-24 22:14:06 +00:00
copilot-swe-agent[bot] 4f7b752b3a Initial plan 2026-06-24 22:12:39 +00:00
Sovran Systems d6d3f219b2 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] 8452ea8fcf Align router setup wording and local port statuses 2026-06-24 19:06:12 +00:00
copilot-swe-agent[bot] 053c417c90 Initial plan 2026-06-24 19:03:36 +00:00
naturallaw777 bf9c6dd627 added onlyoffice 2026-06-24 13:19:41 -05:00
Sovran Systems 163a196203 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] 8fd076191d fix: deliver livekit config via LoadCredential to fix DynamicUser permission denied 2026-06-24 01:21:05 +00:00
copilot-swe-agent[bot] 9172339fbf Initial plan 2026-06-24 01:19:37 +00:00
Sovran Systems b21934f61e 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 bbf3ce436c 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 53fe416ac9 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] e2db7f95d8 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] e2ddf78c23 Initial plan 2026-06-23 00:40:49 +00:00
naturallaw777 1a8b18f5bc removed uneeded livekit-cli 2026-06-18 16:51:15 -05:00
naturallaw777 4329dde0a1 nixpkgs update 2026-06-18 16:05:02 -05:00
Sovran Systems d49ff5002a 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 33688f3015 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 c2a834923a 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] da03832c0e 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] 5194549060 Initial plan 2026-06-13 14:49:39 +00:00
Sovran Systems f0b9726a75 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] 2c5917e8d4 Remove orphaned ./branding.nix import from iso/common.nix 2026-06-10 18:41:49 +00:00
copilot-swe-agent[bot] 57b6d70894 Initial plan 2026-06-10 18:40:41 +00:00
naturallaw777 b93e8202d1 nixpkgs update 2026-06-09 11:03:40 -05:00
Sovran Systems e18c4714e1 Add files via upload 2026-06-05 15:15:19 -05:00
Sovran Systems 2e4bd9fa0f Add files via upload 2026-06-05 13:27:04 -05:00
Sovran Systems df645aaa56 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] 12a81708d4 Improve screenshot alt text for accessibility 2026-06-05 18:14:13 +00:00
copilot-swe-agent[bot] bc01e1e24e Add desktop screenshot placeholder and README embed 2026-06-05 18:13:46 +00:00
copilot-swe-agent[bot] dae409e106 Plan README changes with placeholder screenshot 2026-06-05 18:13:16 +00:00
copilot-swe-agent[bot] 7a3a390275 Initial plan 2026-06-05 17:56:09 +00:00
naturallaw777 6c48a9e239 sync and removed element-desktop and bitwarden desktop 2026-06-05 10:29:20 -05:00
naturallaw777 2903a61944 updated nix packages 2026-06-05 10:27:51 -05:00
naturallaw777 d9811d2375 updated stable branch 2026-06-05 10:04:13 -05:00
naturallaw777 7f185b0355 moved to new bitcoin-knots with bip110 2026-06-04 16:06:58 -05:00
Sovran Systems 58d34182b8 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] 00b5bf5166 Refine BIP110 matching and add regression coverage 2026-06-04 20:15:35 +00:00
copilot-swe-agent[bot] 732a7cd260 Fix BIP110 detection for reduced_data deployments 2026-06-04 20:11:58 +00:00
copilot-swe-agent[bot] 370786d1dc Initial plan 2026-06-04 20:07:44 +00:00
Sovran Systems f45141956a 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] 44737f5571 Fix docstring accuracy, extract _firstElementFromHtml helper, address all code review feedback 2026-06-04 19:49:01 +00:00
copilot-swe-agent[bot] bf56dfdb68 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] afb6a2f515 Add live BIP-110 deployment status: new helpers, endpoint, badge UI 2026-06-04 19:42:23 +00:00
copilot-swe-agent[bot] 9d212675bc Initial plan 2026-06-04 19:36:51 +00:00
Sovran Systems d2d5ced4ac 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] a652fd69d6 Address code review: fix whitespace and log migration exceptions 2026-06-04 19:18:28 +00:00
copilot-swe-agent[bot] e1ae63e8bc 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] 3ad09e478b Initial plan 2026-06-04 19:12:56 +00:00
Sovran Systems 4d7fdfda7c 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] e0bff2d640 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] 753e136384 Retire deprecated bip110 flake input; collapse Bitcoin node tiles to two 2026-06-04 18:52:28 +00:00
copilot-swe-agent[bot] 300dcb605e Initial plan 2026-06-04 18:47:49 +00:00
naturallaw777 2e9a2644d9 updated config for gnome 50 support 2026-05-27 11:15:09 -05:00
Sovran Systems 619e940ed3 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 92d7e062f4 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] 1237822954 Harden asset-version fallback hashing separators 2026-05-27 16:03:31 +00:00
copilot-swe-agent[bot] 48428c9a82 Add resilient Nix download/fallback settings for hub update flows 2026-05-27 16:02:37 +00:00
copilot-swe-agent[bot] ea02d3ef94 Initial plan 2026-05-27 15:59:57 +00:00
copilot-swe-agent[bot] 4fbfd18394 Add unified template asset cache-busting version 2026-05-27 15:59:50 +00:00
copilot-swe-agent[bot] 2a1eeff2f9 Initial plan 2026-05-27 15:56:18 +00:00
Sovran Systems 9da4a7d2ef Merge pull request #302 from naturallaw777/staging-dev
overall nixpkgs update
2026-05-27 09:38:22 -05:00
naturallaw777 85d1f5a0c2 overall nixpkgs update 2026-05-27 09:35:36 -05:00
Sovran Systems 3b7badfa47 Refine networking and security section in README 2026-05-23 15:46:24 -05:00
Sovran Systems f740244b75 Update README.md 2026-05-23 15:42:59 -05:00
Sovran Systems 18a8b0e088 Add Sovran Hub icon SVG referenced by README 2026-05-23 11:35:09 -05:00
Sovran Systems 12f9445dc8 README: use new Sovran Hub icon 2026-05-23 11:33:46 -05:00
Sovran Systems 2e2955be14 Add new Sovran Hub icon (v3) 2026-05-23 11:30:45 -05:00
naturallaw777 9e3a9fcdc3 update readme 2026-05-23 11:28:02 -05:00
Sovran Systems 502c842f72 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 b16094efce 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] 6a8d402d70 Initial plan 2026-05-22 03:34:50 +00:00
Sovran Systems 5736e9986a 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 ede8d660b5 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] b119b102e2 Initial plan 2026-05-22 03:21:40 +00:00
naturallaw777 c0eb237e6a overall nixpkgs update 2026-05-21 17:02:38 -05:00
Sovran Systems 399eafc859 Delete docs directory 2026-05-21 16:50:48 -05:00
Sovran Systems 5c23cbdad1 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 ce55caf66f 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] 6dfa153214 Initial plan 2026-05-21 14:28:00 +00:00
Sovran Systems 9b81573462 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 8f30e233e1 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 035e32028c 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] f5f23d3f47 Initial plan 2026-05-21 01:32:55 +00:00
Sovran Systems 1bdba77224 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 c11910c2bd 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] 19fa1fdff0 Initial plan 2026-05-17 14:05:39 +00:00
naturallaw777 690b5d5a8d nixpkgs update 2026-05-14 10:17:11 -05:00
Sovran Systems 167193008d 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 6d7f518228 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] a8683faf9e Initial plan 2026-05-09 14:58:55 +00:00
Sovran Systems 4fd83bd81e 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 b397e1bb20 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] 45a9b5c8b9 Initial plan 2026-05-08 20:53:48 +00:00
naturallaw777 eff48cafa2 blocked rxrpc kernal moduel 2026-05-08 15:20:18 -05:00
naturallaw777 c616cbed9b updated nixpkgs 2026-05-08 08:04:35 -05:00
Sovran Systems 81d838c950 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 696747dbf8 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] 72acbb795e Initial plan 2026-05-03 22:33:54 +00:00
Sovran Systems a949f2f7d8 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 78724d4ac1 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] 7151332de1 Initial plan 2026-04-30 17:44:53 +00:00
naturallaw777 970b9a6741 fix for gnome keyring 2026-04-30 11:42:35 -05:00
Sovran Systems e6d129f6ad 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 e58c645429 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] ecddc13b1b Initial plan 2026-04-30 16:31:14 +00:00
naturallaw777 64371c5572 fix for gnome keyring 2026-04-30 10:46:58 -05:00
Sovran Systems 50944b97a0 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 05d534d309 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] d11fb3f6f5 Initial plan 2026-04-30 14:26:43 +00:00
Sovran Systems 2becf04d80 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 25e00bd1f1 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] 9b591e29ac Initial plan 2026-04-30 13:48:25 +00:00
Sovran Systems b04a5b30b7 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 e64217be28 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] ba65a86432 Initial plan 2026-04-30 13:10:40 +00:00
Sovran Systems f8ce447fe7 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 bdb802d495 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] d87ba1879d Initial plan 2026-04-30 08:36:41 +00:00
Sovran Systems 54a211fd3e 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 afee1a82e9 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] b1b38c9325 Initial plan 2026-04-30 03:46:21 +00:00
Sovran Systems 8f32682acc hub: upgrade modal — Njalla is the only supported domain provider 2026-04-29 22:31:22 -05:00
Sovran Systems 2c95242140 hub: Zeus Connect — skip value/copy button when qronly is set 2026-04-29 22:24:24 -05:00
Sovran Systems 8db5638ffb hub: Zeus Connect — show QR code only, remove Connection URL text 2026-04-29 22:22:21 -05:00
Sovran Systems 923960d252 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 d3c29277e7 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 8076e2db96 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] 795537d7b2 Initial plan 2026-04-30 01:39:53 +00:00
copilot-swe-agent[bot]andnaturallaw777 04a90a3a38 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] 9cf8e49aaa Initial plan 2026-04-30 01:25:15 +00:00
Sovran Systems 9557d0ae3b 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 13c356e5b5 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] c36207e71e Initial plan 2026-04-30 00:51:11 +00:00
Sovran Systems ed8df2625c 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 2c2b2c6687 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] ee06747a81 Initial plan 2026-04-30 00:31:49 +00:00
naturallaw777 1aeab706b6 nix package update 2026-04-29 16:10:09 -05:00
naturallaw777 daacc83afc updated bitcoind for better launch 2026-04-29 16:00:27 -05:00
Sovran Systems dbd9c6db37 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 23b4c74c09 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] 7d5d0e04c4 Initial plan 2026-04-29 20:00:45 +00:00
Sovran Systems b02503bd53 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 27a3459de4 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] 72a156111a Initial plan 2026-04-29 19:48:47 +00:00
Sovran Systems 872ac28b60 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 33555f5b6f 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 f58e434588 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] ba3ab3566a Initial plan 2026-04-22 14:22:01 +00:00
Sovran Systems 40c233890b 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 5a504ac83f 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] b67555201b Initial plan 2026-04-22 12:34:17 +00:00
Sovran Systems f6b0445161 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 a16f61675a 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] febcf5fe06 Initial plan 2026-04-22 12:05:50 +00:00
Sovran Systems c15bb6a79c 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 4eee7c11ec 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] fe3d2d0a5e Initial plan 2026-04-22 11:58:07 +00:00
naturallaw777 18476d208f bitcon knots fix 2026-04-18 10:08:20 -05:00
naturallaw777 c275a9caa3 nixpkgs update 2026-04-18 09:45:57 -05:00
Sovran_Systems ed4125f82e 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 efd0e8d01a 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 9e2f6a4d78 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 98c0429a83 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] 207f0b4856 Initial plan 2026-04-18 13:44:03 +00:00
naturallaw777 4d55a98ca1 updated rpc bitcoinnd port 2026-04-17 18:05:24 -05:00
Sovran_Systems 053a7b62e1 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 d2a8d33cbf 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] 52c1c8ba70 Initial plan 2026-04-17 22:55:25 +00:00
Sovran_Systems ed86226003 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 7815f85be0 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] 44fdb72ffa Initial plan 2026-04-17 15:38:41 +00:00
Sovran_Systems ae0037ac23 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 135b9f4654 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] d391f1091b Initial plan 2026-04-17 13:05:11 +00:00
Sovran_Systems dff732e574 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 6d2e652f6b 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] 770ee125db Initial plan 2026-04-17 12:15:03 +00:00
Sovran_Systems cda0332004 Remove localhost from url_preview_ip_ranger_whitelist 2026-04-16 23:49:04 -05:00
Sovran_Systems 5c0a0f39ca 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 a188329e60 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] 01dbde7a2d Initial plan 2026-04-17 03:48:26 +00:00
Sovran_Systems 39e65d2041 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 e272ae99fa 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 91c385ff43 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 5357bfb43c 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] e684627183 Initial plan 2026-04-17 03:24:31 +00:00
Sovran_Systems 44babf2d33 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 71ae1e7da1 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] 0827e3d697 Initial plan 2026-04-16 17:50:02 +00:00
Sovran_Systems a75763ddc6 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 4434f1652f 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] 62ca6d6580 Initial plan 2026-04-16 17:40:57 +00:00
Sovran_Systems f5dd153a8e Remove server configuration from Bitcoin ecosystem 2026-04-16 12:21:19 -05:00
Sovran_Systems d3a8aa6dce 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 d533c3a288 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 ffea808106 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 e76b3b4c96 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] 10b63cf0e3 Initial plan 2026-04-16 17:04:06 +00:00
naturallaw777 c3c0a9035c updated hub to proper bitcoinnd port 2026-04-16 11:30:02 -05:00
naturallaw777 c3b6fc5e3f updated electrs and bitciond 2026-04-16 11:17:33 -05:00
copilot-swe-agent[bot]andnaturallaw777 87c478ab30 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 a6a004553d 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 a2c1e62129 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] 9a69815926 Initial plan 2026-04-16 14:58:54 +00:00
Sovran_Systems b1e856149b 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 738c8d7294 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] 365daa8989 Initial plan 2026-04-16 01:09:40 +00:00
naturallaw777 b4b62f18a8 updated electrs for localhost 2026-04-15 19:18:17 -05:00
naturallaw777 b7df44eb09 updated bitcoind and eletrs for localhost 2026-04-15 19:13:33 -05:00
Sovran_Systems 469569ab03 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 96205e8cdc 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 d4ef0b832f 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 af7486d52e 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] bbef199f1a Initial plan 2026-04-15 20:11:07 +00:00
Sovran_Systems f1f66f204d 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 99bac801c5 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] 742ee36909 Initial plan 2026-04-15 19:59:35 +00:00
Sovran_Systems 6c68cbf0bd 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 c53270f542 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] a1b1834e2a Initial plan 2026-04-15 18:55:17 +00:00
Sovran_Systems 9cb2d7d5ad 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 24cecc30e8 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 160d332284 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] a44a1d7901 Initial plan 2026-04-15 18:35:51 +00:00
copilot-swe-agent[bot]andnaturallaw777 65f367d3f1 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] 2d24b987ec Initial plan 2026-04-15 18:31:55 +00:00
Sovran_Systems 5cc5eaf3d9 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 e2442b558d 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 4ab2955f09 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 4c1b582835 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 32ab79cc00 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 f79b15cf73 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 f6995393af 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] de5f38f243 Initial plan 2026-04-15 18:15:23 +00:00
copilot-swe-agent[bot]andnaturallaw777 6028586da9 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] 95df489bdb Initial plan 2026-04-15 18:08:40 +00:00
Sovran_Systems 81c7858141 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 546ad69b58 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 31d4b8521f 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 0fc7f2b50d 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] 7ad5e28ce2 Initial plan 2026-04-15 18:00:21 +00:00
Sovran_Systems cc4d632b20 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 42b33592ba 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 e273e02f22 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] 847089b0bf Initial plan 2026-04-15 17:35:39 +00:00
copilot-swe-agent[bot]andnaturallaw777 5197ce9d74 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] 8787db3da4 Initial plan 2026-04-15 17:25:34 +00:00
Sovran_Systems 05aa06680b 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 f29b741bf7 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 205b8e9cf8 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 4bc3e65590 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 c454a35d5d 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 902ad793e9 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] 334682049a Initial plan 2026-04-15 17:05:40 +00:00
copilot-swe-agent[bot] 543223e2cc Initial plan 2026-04-15 17:04:40 +00:00
Sovran_Systems 3c2e986bec 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 838ae8aa65 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 15715258de 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 283420d43a 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 78fa162738 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 9ff7bdc9bc 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] e3962d3c88 Initial plan 2026-04-15 16:33:30 +00:00
copilot-swe-agent[bot] 7378909795 Initial plan 2026-04-15 16:31:03 +00:00
Sovran_Systems a50bf30241 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 dd581b4339 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 42c65589fd 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 f2d3c4aaa9 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 8e5e68850f 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 268411fe00 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 28adbf5a52 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 e5613e0826 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 c65b3ae1b9 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] 3c10aa3c1e Initial plan 2026-04-15 16:04:21 +00:00
copilot-swe-agent[bot] 72b2d78862 Initial plan 2026-04-15 16:01:35 +00:00
Sovran_Systems ab864d0801 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 a511ae4893 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] 9140f3d692 Initial plan 2026-04-15 15:32:38 +00:00
Sovran_Systems 514c4d8485 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 5cbe02420d 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 c474773f30 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] 4c00b8a7f9 Initial plan 2026-04-15 15:11:07 +00:00
copilot-swe-agent[bot]andnaturallaw777 9662395973 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] 6f46b1c7b2 Initial plan 2026-04-15 15:01:26 +00:00
Sovran_Systems ac833f725f 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 6e5dbb5bf5 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] 28daec91ad Initial plan 2026-04-15 12:34:39 +00:00
Sovran_Systems 8f676b0696 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 de981bd1a4 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] 40a2761f7d Initial plan 2026-04-15 01:43:37 +00:00
Sovran_Systems b551193368 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 e34997b49e 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] 51c75d102e Initial plan 2026-04-15 01:30:18 +00:00
Sovran_Systems 90b49766c3 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 1612e0956d 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] 9dbe8381d4 Initial plan 2026-04-14 22:00:01 +00:00
Sovran_Systems 7c33a23c73 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 d97cae01d0 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] d287e2b67e Initial plan 2026-04-14 21:42:15 +00:00
Sovran_Systems 8db0895d13 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 aec3b55b48 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] d0ae91e9ba Initial plan 2026-04-14 21:16:39 +00:00
Sovran_Systems cc63e796f5 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 f42fcd9afb 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] 317f3ed6c4 Initial plan 2026-04-14 21:03:53 +00:00
naturallaw777 b10c9d4ff6 updated elementcalling firewall typo 2026-04-14 13:30:27 -05:00
naturallaw777 5ee1c06106 updated elementcalling firewall 2026-04-14 13:28:35 -05:00
Sovran_Systems 9eb5851173 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 fe91e719f2 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] 592237fd1a Initial plan 2026-04-14 18:20:38 +00:00
naturallaw777 717cb43214 nixpkgs update 2026-04-14 08:50:54 -05:00
naturallaw777 19fa297b24 cleaned up code 2026-04-14 08:49:26 -05:00
Sovran_Systems 50bb5fa0bf 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 c97406a915 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] c191129a32 Initial plan 2026-04-14 13:44:07 +00:00
naturallaw777 b89ce496d3 update readme 2026-04-13 19:59:42 -05:00
Sovran_Systems 7e324fa552 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 91ca5a8c21 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] 1077242566 Initial plan 2026-04-14 00:43:21 +00:00
naturallaw777 a656c35776 update readme 2026-04-13 19:05:47 -05:00
Sovran_Systems de4a37e336 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 2b69fe73df 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] 64851553ec Initial plan 2026-04-13 23:59:21 +00:00
naturallaw777 61fc7595fb update readme 2026-04-13 18:50:04 -05:00
Sovran_Systems 21cd7723d2 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 a727a79bbf 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] dc8214566d Initial plan 2026-04-13 23:38:45 +00:00
naturallaw777 34fa8269e5 updated caddy icon 2026-04-13 18:24:58 -05:00
Sovran_Systems 9656c1a872 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 40d63439b7 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] b4fc08917d Initial plan 2026-04-13 23:15:32 +00:00
Sovran_Systems 80f0255a86 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 f2b041e51d 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 b6ac6174fe 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 aaef654553 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] 8b05aa7958 Initial plan 2026-04-13 22:54:20 +00:00
copilot-swe-agent[bot] 17e7c860e8 Initial plan 2026-04-13 22:52:35 +00:00
Sovran_Systems 8ace03487f 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 aff665c815 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] 3beb7d9cb2 Initial plan 2026-04-13 22:36:23 +00:00
Sovran_Systems 375e7273de 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 b2becf6d47 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] 0237faa8c6 Initial plan 2026-04-13 22:31:26 +00:00
Sovran_Systems aca16ed15f 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 6088288237 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] a4ff2f9dfd Initial plan 2026-04-13 22:24:18 +00:00
Sovran_Systems a0e0bc60f5 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 7351fdd3a1 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] 9684f1f253 Initial plan 2026-04-13 22:11:54 +00:00
Sovran_Systems 83b9c181b6 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 1a4cc54341 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] 146e62755f Initial plan 2026-04-13 21:58:15 +00:00
Sovran_Systems adfd77ea21 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 e35b9da9ed 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] 716e298f1e Initial plan 2026-04-13 03:57:26 +00:00
Sovran_Systems 741b4dcade 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 3492deb8c1 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] f9775ed64d Initial plan 2026-04-13 01:19:35 +00:00
Sovran_Systems 2546fb6970 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 3491b7d340 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] 44239488dd Initial plan 2026-04-13 01:08:24 +00:00
Sovran_Systems 8cc4ec360d 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 44888979ed 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] 57f9487e83 Initial plan 2026-04-13 00:59:53 +00:00
Sovran_Systems 0307213729 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 ebf11be1d3 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] 550c8a732c Initial plan 2026-04-13 00:50:55 +00:00
Sovran_Systems 8568f84eef 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 35e7bc46ab 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] a24c4368fd Initial plan 2026-04-13 00:25:41 +00:00
Sovran_Systems 18539ccab6 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 90447593df 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] 21ca80fc59 Initial plan 2026-04-13 00:14:35 +00:00
Sovran_Systems acce041c24 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 088fb0135b 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] ce26e4d3e0 Initial plan 2026-04-13 00:07:01 +00:00
Sovran_Systems 61ea105f74 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 47686017ba 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] 621109b0a5 Initial plan 2026-04-12 23:08:49 +00:00
Sovran_Systems b64a576977 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 5fa5d8cf41 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] 4ae7219b48 Initial plan 2026-04-12 21:35:00 +00:00
Sovran_Systems 31f4fced04 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 d503e0d095 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] 61f3faedce Initial plan 2026-04-12 20:50:10 +00:00
Sovran_Systems 913b3fe608 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 9851b0d62b 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] 2287adc415 Initial plan 2026-04-12 19:07:20 +00:00
Sovran_Systems 4e83b1e47f 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 300aaf4f6d 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] f9f747fb35 Initial plan 2026-04-12 18:47:12 +00:00
Sovran_Systems db909ddc6b 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 df51fc2a1b 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] df6b97620f Initial plan 2026-04-12 18:33:50 +00:00
Sovran_Systems eb59109a9a 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 41bc2b25ed 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] 1833c87327 Initial plan 2026-04-12 18:16:38 +00:00
Sovran_Systems b6c2c7e23c 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 36aa3eaee0 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] 19c8206c04 Initial plan 2026-04-12 18:06:28 +00:00
Sovran_Systems 17257c547e 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 25baee92a6 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 98eb24a880 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] 32f0a78bf4 Initial plan 2026-04-12 15:31:33 +00:00
Sovran_Systems 17e6af15d6 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 2b26472505 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] 57f495e65d Initial plan 2026-04-12 15:01:51 +00:00
naturallaw777 c7a7362d7a readme update 2026-04-12 09:26:15 -05:00
Sovran_Systems 81d69f07f6 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 29415029a7 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] 7658d07241 Initial plan 2026-04-12 13:45:19 +00:00
naturallaw777 31bd7c4afc readme update 2026-04-12 08:32:54 -05:00
naturallaw777 95f7b34ec5 readme update 2026-04-12 08:30:11 -05:00
Sovran_Systems 8609c2320c 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 8f3d2bb818 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] b666812661 Initial plan 2026-04-12 12:56:17 +00:00
Sovran_Systems dc27447a9e 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 45d8595366 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] 91f2f1b24d Initial plan 2026-04-12 12:24:20 +00:00
Sovran_Systems f60d788804 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 4c084ea2ad 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] 278dd5c7a8 Initial plan 2026-04-12 12:05:34 +00:00
Sovran_Systems fef12df2e5 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 991acfd143 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] b622cbf68d Initial plan 2026-04-12 11:45:49 +00:00
naturallaw777 800b772998 readme update 2026-04-12 00:11:46 -05:00
Sovran_Systems 80f8acb05d 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 512296022e 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] 0e73a36510 Initial plan 2026-04-12 04:58:42 +00:00
Sovran_Systems 164dd1124f 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 790f5126c6 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] 8d09d27472 Initial plan 2026-04-12 04:47:49 +00:00
Sovran_Systems 7d68fc9632 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 78add044c0 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 53472c894b 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 5f5acca535 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] eb3aad55f3 Initial plan 2026-04-12 04:28:13 +00:00
copilot-swe-agent[bot] ba439ee29c Initial plan 2026-04-12 04:26:11 +00:00
Sovran_Systems 0c76cdd678 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 62ac35591b 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] ecaf6012ce Initial plan 2026-04-12 04:09:58 +00:00
Sovran_Systems 43b38c1d7e 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 a22092ba5c 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] 21542780e4 Initial plan 2026-04-12 02:13:26 +00:00
Sovran_Systems 06b372c7d3 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 0e89e8f725 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] e73d5b2995 Initial plan 2026-04-12 01:43:05 +00:00
naturallaw777 27162616c0 nixpkgs update 2026-04-11 20:21:24 -05:00
naturallaw777 c6231a428f fixed icon color 2026-04-11 20:12:12 -05:00
Sovran_Systems c8c4a259e9 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 e05ce437f9 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] 65e84bcb3a Initial plan 2026-04-12 01:09:00 +00:00
naturallaw777 ce1d089193 cleaned up repo 2026-04-11 20:02:43 -05:00
Sovran_Systems b4cb04530b 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 98eed76d87 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] e3d8170287 Initial plan 2026-04-12 00:48:14 +00:00
Sovran_Systems 53da3d7615 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 df4839ecc5 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] e949c535e2 Initial plan 2026-04-12 00:14:43 +00:00
Sovran_Systems c70283c038 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 f9a23b6933 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] d26bd88dfc Initial plan 2026-04-11 23:27:26 +00:00
Sovran_Systems a538bb6db7 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 b120d19e38 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] c98f1bbfc3 Initial plan 2026-04-11 22:23:50 +00:00
Sovran_Systems 87a843a85b 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 ec014b4f9e 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] 1e46250c2b Initial plan 2026-04-11 21:37:04 +00:00
Sovran_Systems 85e3be20c2 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 6aff9a2038 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] 326c7f2dc9 Initial plan 2026-04-11 21:12:49 +00:00
Sovran_Systems 716ffceb78 Updated landing page to highlight ways to use Sovran_SystemsOS and added donation information. 2026-04-09 17:58:41 -05:00
Sovran_Systems 77b3a1e28a 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 bdfe9ed396 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] ee48963c23 Initial plan 2026-04-09 22:33:20 +00:00
Sovran_Systems 20e4c719f4 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 fbf9f928ad 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] c55dc15da1 Initial plan 2026-04-09 22:24:36 +00:00
Sovran_Systems 790fc1c1e3 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 b4dded64ff 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] 6128ece160 Initial plan 2026-04-09 22:11:49 +00:00
Sovran_Systems 8bdd4c10b1 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 5dc33b2c0a 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] 3d9cf819eb Initial plan 2026-04-09 19:45:43 +00:00
Sovran_Systems 33bdadde7e 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 4ed5936df4 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] bd06e1313d Initial plan 2026-04-09 19:30:28 +00:00
Sovran_Systems 4d2dfc7f39 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 914390259c 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] 0ab9d94f0e Initial plan 2026-04-09 19:22:49 +00:00
Sovran_Systems c49f421208 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 d01ec15050 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] 51ea07665e Initial plan 2026-04-09 19:09:56 +00:00
Sovran_Systems eb88684793 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 20e1206f1d 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] 433a276e43 Initial plan 2026-04-09 18:44:42 +00:00
naturallaw777 9c38baefcc key 2026-04-09 12:59:11 -05:00
Sovran_Systems 416661e291 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 62dd104c50 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] 2dbbd0fc73 Initial plan 2026-04-09 17:26:39 +00:00
Sovran_Systems 46cfcd998c 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 bb0467e1b5 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] 50898743b1 Initial plan 2026-04-09 17:17:07 +00:00
Sovran_Systems 999c6300c1 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 a8fcd007f3 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] ebb8bf70a4 Initial plan 2026-04-09 10:49:18 +00:00
Sovran_Systems 2c8bd4a1aa 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 1564be0b9c 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] dd34c0f923 Initial plan 2026-04-09 04:04:38 +00:00
Sovran_Systems 2022e99179 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 9ceedac415 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] 2aebcec712 Initial plan 2026-04-09 03:51:28 +00:00
Sovran_Systems ac172e44dc 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 c57a154fc6 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] 080d1b4a59 Initial plan 2026-04-09 03:43:01 +00:00
Sovran_Systems 60703904e2 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 efff6115b7 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] 1b617747ca Initial plan 2026-04-09 03:21:30 +00:00
Sovran_Systems 9a8fb3da91 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 881cdf9f02 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] 24a8be1d9c Initial plan 2026-04-09 02:58:35 +00:00
Sovran_Systems 2e80933092 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 ec47d2c2f8 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] 7bd81cbc25 Initial plan 2026-04-09 02:06:47 +00:00
Sovran_Systems 7d1766db9b 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 66271c9227 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] 099a0cc86a Initial plan 2026-04-09 01:49:35 +00:00
Sovran_Systems d0b08a9771 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 8e8b10dea2 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] d8defb74dd Initial plan 2026-04-09 00:16:48 +00:00
Sovran_Systems 41e3f9c98f 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 38982162b9 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 7a691c2280 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] 658bd307c2 Initial plan 2026-04-09 00:08:33 +00:00
copilot-swe-agent[bot]andnaturallaw777 6c8627ae25 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] f151f28e81 Initial plan 2026-04-09 00:02:10 +00:00
Sovran_Systems 11bdfae6fa 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 d8edde72e9 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] c261c9f3dd Initial plan 2026-04-08 22:10:31 +00:00
Sovran_Systems 54cb3e9a00 Merge pull request #158 from naturallaw777/copilot/revert-rdp-nix-to-8e31a32
revert: restore rdp.nix to pre-PR#147 working state
2026-04-08 16:30:59 -05:00
copilot-swe-agent[bot]andnaturallaw777 e83e227517 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] 7f338e9d1e Initial plan 2026-04-08 21:28:22 +00:00
Sovran_Systems f8e75151c0 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 e5416f9aaa 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] 942418a71b Initial plan 2026-04-08 21:00:10 +00:00
Sovran_Systems 1c4543839b 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 cfa2585b92 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] 97c6c403de Initial plan 2026-04-08 20:41:59 +00:00
Sovran_Systems dc7be493f1 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 5d75c16cb7 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] 19f1153250 Initial plan 2026-04-08 18:15:54 +00:00
Sovran_Systems 0b1b9023f2 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 e22fc91f47 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] b5b1389c20 Initial plan 2026-04-08 17:48:16 +00:00
Sovran_Systems 6c8891e762 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 7bcf54e99d 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 b74a6fe1b6 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] 28f00e3c6e Initial plan 2026-04-08 17:37:18 +00:00
naturallaw777 988af9ecc7 updated locals 2026-04-08 12:18:52 -05:00
naturallaw777 2e84f83d37 updated for user timezonez and languges and removed unsued code 2026-04-08 12:12:47 -05:00
Sovran_Systems 61aaabb8da 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 cf3115dc2d 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] 80f6e0436b Initial plan 2026-04-08 16:57:47 +00:00
Sovran_Systems 4f9c0af407 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 cfbfc0f921 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] 62ec738249 Initial plan 2026-04-08 16:49:49 +00:00
Sovran_Systems 7949b75d5e 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 f4bbae31b3 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] 1970e7efd4 Initial plan 2026-04-08 16:00:59 +00:00
Sovran_Systems 4a2a9336a6 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 672f32b85e 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 23094bfd7e 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 f965d88c9a 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 f51174e79f 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 bc7171b984 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] a58e9aa3f7 Initial plan 2026-04-08 14:43:14 +00:00
copilot-swe-agent[bot] a0a4967866 Initial plan 2026-04-08 14:36:01 +00:00
copilot-swe-agent[bot] 890b2b8fe5 Initial plan 2026-04-08 14:33:44 +00:00
Sovran_Systems 8e31a32ee1 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 663deaaeac 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] 4eb0d82740 Initial plan 2026-04-08 13:40:51 +00:00
Sovran_Systems 5ebba2a1aa 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 94f940c928 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 32b6d3dcf7 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 a6b68d4ebb 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] 8e909f5013 Initial plan 2026-04-08 13:13:41 +00:00
copilot-swe-agent[bot] 33b94b4526 Initial plan 2026-04-08 13:09:47 +00:00
Sovran_Systems a5081abbbe 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 9f816e41a8 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 aa5973f1c6 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 ef97eb359f 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 a45f6a6252 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 a18a1e0b02 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] 0928e0da1c Initial plan 2026-04-08 12:40:12 +00:00
copilot-swe-agent[bot] 152b83e8eb Initial plan 2026-04-08 12:39:45 +00:00
copilot-swe-agent[bot] 027f5c23bc Initial plan 2026-04-08 12:34:30 +00:00
Sovran_Systems f0f96d7471 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 a3d387ea35 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] e59d244418 Initial plan 2026-04-08 12:22:13 +00:00
Sovran_Systems f4cd4e4873 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 b4685256af 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] 5c6812b158 Initial plan 2026-04-08 02:46:49 +00:00
Sovran_Systems 9dac73cd9d 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 846e29f57e 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] 3c9f9e0c8e Initial plan 2026-04-08 00:51:40 +00:00
Sovran_Systems 81b70a6623 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 7b97109503 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 19c31f629e 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] 172e1752ff Initial plan 2026-04-08 00:38:42 +00:00
copilot-swe-agent[bot]andnaturallaw777 397abf3c89 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] 9fc3a95100 Initial plan 2026-04-08 00:33:37 +00:00
Sovran_Systems 250ff03135 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 7eb2664124 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] fe5ae3c5c0 Initial plan 2026-04-08 00:22:26 +00:00
naturallaw777 60b9347ebb updated installer.py 2026-04-07 17:59:53 -05:00
naturallaw777 f1539413a7 updated caddy.nix 2026-04-07 17:36:26 -05:00
naturallaw777 8b7779243d .iso update 2026-04-07 17:02:43 -05:00
Sovran_Systems f1f42a658b 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 21b71b3d89 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 b7959082c9 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] 5de0bf62cd Initial plan 2026-04-07 21:39:42 +00:00
copilot-swe-agent[bot]andnaturallaw777 6e9b165892 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] 7bd7a23c80 Initial plan 2026-04-07 21:28:36 +00:00
Sovran_Systems 4454edbf2b 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 32bdb20941 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] 35878160a7 Initial plan 2026-04-07 21:04:58 +00:00
Sovran_Systems 9974daeed0 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 1b77f4b699 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] 549e3ca233 Initial plan 2026-04-07 20:40:53 +00:00
Sovran_Systems d968a475fe 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 7beb1dbc72 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 6fb68bfa55 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 0eec097ecd 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] 645237fb94 Initial plan 2026-04-07 20:20:01 +00:00
copilot-swe-agent[bot]andnaturallaw777 f66443aa93 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] ef66497443 Initial plan 2026-04-07 20:16:57 +00:00
Sovran_Systems 9d9e5da628 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 8633b74f56 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] 38eb89538f Initial plan 2026-04-07 18:46:24 +00:00
Sovran_Systems 23c1c51ae2 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 4328eb3363 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 79e68e7d33 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] 01d009ec3b Initial plan 2026-04-07 18:25:47 +00:00
naturallaw777 a3cda9cc2e update flake 2026-04-07 13:14:21 -05:00
naturallaw777 9f7b9e8c7a update flake 2026-04-07 13:13:06 -05:00
naturallaw777 a6ca190dbf update flake and installer 2026-04-07 13:11:39 -05:00
Sovran_Systems df828d760a 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 40413118f8 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] 38cdd93ef0 Initial plan 2026-04-07 17:45:41 +00:00
Sovran_Systems 2c99827875 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 78b70eeada 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] 700476d823 Initial plan 2026-04-07 17:29:46 +00:00
Sovran_Systems 250bf83802 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 99b299ece3 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] 6dc591c053 Initial plan 2026-04-07 17:13:16 +00:00
Sovran_Systems 4f5d14ee12 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 38423730c8 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] 2871586048 Initial plan 2026-04-07 16:57:23 +00:00
Sovran_Systems ad0723c257 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 640a2c24a0 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 244b2763e1 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 ac2355f18e 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] 4419049ffa Initial plan 2026-04-07 16:42:51 +00:00
copilot-swe-agent[bot] b005d264ea Initial plan 2026-04-07 16:41:02 +00:00
Sovran_Systems 7a012f3cac 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 a30d554cf1 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] bf10bd06c0 Initial plan 2026-04-07 16:26:26 +00:00
Sovran_Systems 09e20b28ef 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 8e67e2b8bf 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] 475f1d27e8 Initial plan 2026-04-07 16:14:42 +00:00
Sovran_Systems c0f8081f60 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 5fb00bec41 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 52b1d059b8 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 500bb44e96 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] a5940dc020 Initial plan 2026-04-07 15:44:31 +00:00
copilot-swe-agent[bot] 1d35818d7c Initial plan 2026-04-07 15:43:22 +00:00
naturallaw777 d43687c3ad update flake 2026-04-07 10:13:24 -05:00
Sovran_Systems b45b7c9355 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 e2c1493eb2 removed erroniousfile 2026-04-07 10:05:37 -05:00
copilot-swe-agent[bot]andnaturallaw777 41b326ecf3 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] 0f648cc995 Initial plan 2026-04-07 15:02:58 +00:00
Sovran_Systems 0b04ae66c4 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 9d12952113 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] b3622fe099 Initial plan 2026-04-07 14:51:00 +00:00
Sovran_Systems 753fff982e 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 a32492d974 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] a90b496fed Initial plan 2026-04-07 14:44:38 +00:00
naturallaw777 c5bbb3ac40 updated nextcloud.nix 2026-04-07 09:35:23 -05:00
Sovran_Systems f0102a2321 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 575e6f85e4 updated nextcloud.nix 2026-04-07 09:27:25 -05:00
copilot-swe-agent[bot]andnaturallaw777 ca1dd18650 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 674787c2e1 updated nextcloud.nix 2026-04-07 09:11:13 -05:00
copilot-swe-agent[bot] 19d9d7a19c Initial plan 2026-04-07 13:58:07 +00:00
Sovran_Systems 1426fdec3c 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 97f37b8dfe 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] 52a48e8372 Initial plan 2026-04-07 13:02:55 +00:00
Sovran_Systems aaa9860a3f 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 203e3495f5 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] f35d15563d Initial plan 2026-04-07 12:44:22 +00:00
Sovran_Systems b3373ecf8c 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 e644057417 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] 69c47c8c7d Initial plan 2026-04-07 10:36:32 +00:00
Sovran_Systems 77961f3cc0 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 57d04682ac 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] 521559b51c Initial plan 2026-04-07 10:32:41 +00:00
naturallaw777 a797973408 updated branding for hub 2026-04-07 05:20:41 -05:00
Sovran_Systems 6fc6a144d8 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 816764ed67 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 2db157afb5 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] f722e25c74 Initial plan 2026-04-07 10:11:52 +00:00
Sovran_Systems 146579af3b 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 982032c1ea 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] 8216381f8d Initial plan 2026-04-07 10:07:15 +00:00
Sovran_Systems d7c46adcd2 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 2a804e070c 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] 2d44a744c1 Initial plan 2026-04-07 09:53:45 +00:00
naturallaw777 7c7efc008b updated branding for hub 2026-04-07 04:26:09 -05:00
Sovran_Systems 4bcf99af5a 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 28850a83ff 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 07ed88044c 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 43031445b3 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] 5f79e79387 Initial plan 2026-04-07 04:36:05 +00:00
copilot-swe-agent[bot] 34fa8b0f92 Initial plan 2026-04-07 04:33:21 +00:00
naturallaw777 1dfd935559 updated sparrow and bisq icon 2026-04-06 23:27:38 -05:00
Sovran_Systems f6654b8d46 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 eee76059f3 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 a16d3d0204 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] 90febbfadf Initial plan 2026-04-07 04:18:20 +00:00
copilot-swe-agent[bot]andnaturallaw777 bd4f1a11af 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] 8b4a8bcb36 Initial plan 2026-04-07 02:58:50 +00:00
naturallaw777 a991a25d28 updated hub-icon 2026-04-06 21:54:47 -05:00
Sovran_Systems bac1e066ba 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 beeb02f9b4 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] 954df93edf Initial plan 2026-04-07 02:47:51 +00:00
Sovran_Systems 4d720aaa64 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 7746bafefa 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] 084056ead1 Initial plan 2026-04-07 02:33:12 +00:00
Sovran_Systems 6c9313bbaa 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 5f85d5e585 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] a2c40a99b5 Initial plan 2026-04-07 02:25:28 +00:00
naturallaw777 1cad2a4e87 updated custom-template 2026-04-06 21:13:22 -05:00
Sovran_Systems 9386b6b54f 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 b502e54e49 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 16bdac2f5b 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 2fccdc5441 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] ab6aae5cdf Initial plan 2026-04-07 02:03:49 +00:00
Sovran_Systems 3a17c906b5 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 f678b9980f 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] 0d6740aae3 Initial plan 2026-04-07 02:01:07 +00:00
Sovran_Systems 3aa51a9b31 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 aa71e038c4 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] e12dbbf5b7 Initial plan 2026-04-07 01:50:23 +00:00
Sovran_Systems ebf33e9216 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 9d31c47ee6 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] d2d0a5c3d9 Initial plan 2026-04-07 01:42:45 +00:00
Sovran_Systems 6fbded52e2 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 1cad716274 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] d021489427 Initial plan 2026-04-07 01:33:08 +00:00
Sovran_Systems f32b432746 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 b51ea0fff4 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 cdc122a779 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] 5a3529a9f9 Initial plan 2026-04-07 01:22:39 +00:00
Sovran_Systems 33022a6bbf 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 e43ceb3492 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] eafd0a6237 Initial plan 2026-04-07 00:41:57 +00:00
naturallaw777 a0160477d1 fixed ssh at first boot 2026-04-06 18:40:17 -05:00
Sovran_Systems 2b666d66ee 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 f58f6a4c41 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] 3cf47589f9 Initial plan 2026-04-06 23:25:46 +00:00
Sovran_Systems c544617355 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 c21d11260a 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] 598a70e12b Initial plan 2026-04-06 23:17:29 +00:00
Sovran_Systems b92ed01edc 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 f1afa1b5c6 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] ae4316f55d Initial plan 2026-04-05 17:55:32 +00:00
Sovran_Systems 3818a71a50 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 fb4998e188 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 826b4a67f4 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 9381cad03a 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] 4c5efc4291 Initial plan 2026-04-05 17:48:34 +00:00
copilot-swe-agent[bot] 60f8bf0aed Initial plan 2026-04-05 17:48:12 +00:00
Sovran_Systems 1fa41ee84e 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 4a608ddca2 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 d4cd416f0e 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] 0c186c0763 Initial plan 2026-04-05 16:53:30 +00:00
copilot-swe-agent[bot]andnaturallaw777 ab42622dc4 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] 41102b87ed Initial plan 2026-04-05 16:51:21 +00:00
Sovran_Systems 14810945ea 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 9f43bb38f0 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] 0f523ed6d1 Initial plan 2026-04-05 16:22:43 +00:00
Sovran_Systems c3f0a66ec0 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 18e0111555 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] aef90f9036 Initial plan 2026-04-05 15:38:51 +00:00
Sovran_Systems c3ed66b065 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 b5b286eaac 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 b5070e2d78 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 e60d6dac03 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] 63697f9243 Initial plan 2026-04-05 15:17:54 +00:00
copilot-swe-agent[bot] 525346c2c5 Initial plan 2026-04-05 15:17:50 +00:00
Sovran_Systems 48a1c1a1af 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 ef1e2706f2 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] 79e8b79035 Initial plan 2026-04-05 15:03:10 +00:00
Sovran_Systems 3240e94f4b Merge pull request #75 from naturallaw777/copilot/revert-commit-ea30398
[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 3f508ca897 Revert commit ea30398: 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 e5f1449af8 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] 57240f2fa8 Initial plan 2026-04-05 14:42:36 +00:00
copilot-swe-agent[bot]andnaturallaw777 b701db897a 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] 520a14da3d Initial plan 2026-04-05 14:40:22 +00:00
Sovran_Systems ed041a8c71 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 ea30398f65 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] b057fea304 Initial plan 2026-04-05 14:27:55 +00:00
naturallaw777 d2bd55d166 syntax error 2026-04-05 09:27:38 -05:00
naturallaw777 1655b0bc10 closed unused ports 2026-04-05 09:16:03 -05:00
Sovran_Systems cd6c56be33 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 e4816bc54d 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] a052728042 Initial plan 2026-04-05 14:01:31 +00:00
Sovran_Systems 57ff11306b 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 8359eb6d34 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] 52ea2ecfa9 Initial plan 2026-04-05 13:35:12 +00:00
Sovran_Systems 760ebe7d24 Update README.md 2026-04-05 01:36:51 -05:00
Sovran_Systems 0d3a958e5c 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 783f85cb5c 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 65c3e68bda Update README.md with new content 2026-04-05 01:32:15 -05:00
copilot-swe-agent[bot] 3b10bc07b2 Initial plan 2026-04-05 06:28:05 +00:00
Sovran_Systems b5412a5c09 Update README.md 2026-04-05 01:09:30 -05:00
Sovran_Systems a529be7cdf Update README.md 2026-04-05 01:02:13 -05:00
Sovran_Systems e5d0e930c1 Create README.md file for Sovran_SystemsOS 2026-04-05 00:48:49 -05:00
Sovran_Systems 9811816c8d 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 6c52b42312 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] 822e4a40ed Initial plan 2026-04-05 05:26:49 +00:00
Sovran_Systems 632895e3a9 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 c6078b5f89 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] 524af8119f Initial plan 2026-04-05 05:17:23 +00:00
Sovran_Systems 7f969853a2 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 c10ba0d5a5 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] 1969173400 Initial plan 2026-04-05 05:06:55 +00:00
Sovran_Systems d6f03590c4 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 03164edb31 changed to static icon 2026-04-04 23:58:53 -05:00
copilot-swe-agent[bot]andnaturallaw777 80d35fc9e2 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] 6e81f4022c Initial plan 2026-04-05 04:53:41 +00:00
Sovran_Systems 0f67ab5b82 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 724cf0585d 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] 4158362aef Initial plan 2026-04-05 04:39:42 +00:00
naturallaw777 88ff60f3f5 updated update icon 2026-04-04 23:34:23 -05:00
Sovran_Systems 7243e93c0f 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 4649ff4294 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] 3777b936e6 Initial plan 2026-04-05 04:23:36 +00:00
Sovran_Systems b0181a5b3b 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 c98336e069 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] 2b840a4ad5 Initial plan 2026-04-05 04:07:09 +00:00
Sovran_Systems b4dc8c0b19 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 dd33cdcb00 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] ea3fc0559a Initial plan 2026-04-05 04:02:35 +00:00
Sovran_Systems 91824cb7db 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 5416744f5c 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] 9e237162fa Initial plan 2026-04-05 03:48:27 +00:00
Sovran_Systems 0249726908 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 f5b8402173 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] 4da486ce43 Initial plan 2026-04-05 03:36:18 +00:00
Sovran_Systems 70a74e5960 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 df9fc8672f 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] 9877e6a2f8 Initial plan 2026-04-05 03:18:02 +00:00
Sovran_Systems ec042eaa29 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 cacd1a8c00 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] 43f97bf6e3 Initial plan 2026-04-05 03:01:55 +00:00
Sovran_Systems 8ca126b90b Remove Credentials step from onboarding wizard (5 → 4 steps) 2026-04-04 21:58:42 -05:00
Sovran_Systems 61d582b3f9 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 0b54f5cf9d 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] 030e5e7f74 Initial plan 2026-04-05 02:45:46 +00:00
Sovran_Systems 21385b2a67 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 7fc0aefc6b 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] 7c5741ac2a Initial plan 2026-04-05 02:36:32 +00:00
Sovran_Systems 83600ad7b7 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 41773ed7dc 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] cb3df23a4e Initial plan 2026-04-05 02:29:21 +00:00
naturallaw777 e4f46ecfa0 updated missing icons 2026-04-04 21:23:04 -05:00
naturallaw777 f4eb079ccc updated logos 2026-04-04 21:08:29 -05:00
naturallaw777 0f9a63284c bigger logo 2026-04-04 18:58:17 -05:00
naturallaw777 0e2ca159a1 bigger logo 2026-04-04 18:55:16 -05:00
naturallaw777 df875b26a8 bigger logo 2026-04-04 18:48:46 -05:00
naturallaw777 bdb9ff79cc bigger logo 2026-04-04 18:43:12 -05:00
naturallaw777 03fb63347b bigger logo 2026-04-04 18:39:14 -05:00
Sovran_Systems 7c4eba1ff5 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 961636fc18 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] d6dbe72989 Initial plan 2026-04-04 23:19:54 +00:00
naturallaw777 6a044494d7 bigger logo 2026-04-04 18:09:13 -05:00
Sovran_Systems 1585104275 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 c678c85c1b 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] a5dbf269de Initial plan 2026-04-04 22:53:05 +00:00
Sovran_Systems 239e134ad0 Add missing service detail modal CSS styles (svc-detail, addon, domain, port, support, feature) 2026-04-04 17:27:35 -05:00
Sovran_Systems ea149ca134 Restore full style.css accidentally truncated in 14e728b; apply logo height fix (64px→36px) 2026-04-04 17:16:11 -05:00
Sovran_Systems 5912ed6cea 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 205eadbb46 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] 5f78ada2cf Initial plan 2026-04-04 18:28:29 +00:00
Sovran_Systems 14e728b5d2 fix: reduce header logo height from 64px to 36px to fit header bar 2026-04-04 13:12:58 -05:00
Sovran_Systems b685ec62f5 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 f37dcf4e3e 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] e4d234507b Initial plan 2026-04-04 16:43:32 +00:00
Sovran_Systems 838942a887 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 e0d47a17ee 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 22736d49e5 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 955e123a66 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 1c07540e6d 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] 415e7689f4 Initial plan 2026-04-04 15:46:48 +00:00
Sovran_Systems 49ed561b8e 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 19a0752672 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] b1d4f96112 Initial plan 2026-04-04 15:20:54 +00:00
Sovran_Systems ed457f30e3 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 b566fce52f 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] 2cbd0f2a76 Initial plan 2026-04-04 14:42:09 +00:00
Sovran_Systems e9befedee5 Delete .gitignore.txt 2026-04-04 09:31:46 -05:00
Sovran_Systems c5a756521c Update .gitignore 2026-04-04 09:31:19 -05:00
Sovran_Systems 3d986742a6 Delete path/to directory 2026-04-04 09:30:27 -05:00
Sovran_Systems 589ee3fc1b Update service data model to include requiresDomain and domain status fields. 2026-04-04 09:27:06 -05:00
Sovran_Systems 53d1de1803 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 f88c91bfe0 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] 975eb9a761 Initial plan 2026-04-04 13:15:17 +00:00
Sovran_Systems b18fbcf758 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 2203ddb79b 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] bb48932281 Initial plan 2026-04-04 01:25:16 +00:00
Sovran_Systems 3567985b88 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 14ddbac7da 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] 36dd935f88 Initial plan 2026-04-04 00:52:59 +00:00
Sovran_Systems bcfd66f554 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 7000435552 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] d27d5b7b34 Initial plan 2026-04-04 00:39:05 +00:00
naturallaw777 77b1f24cf9 bump 2026-04-03 19:31:57 -05:00
Sovran_Systems 92978a9868 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 4e70a267d8 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] 9de5fca56e Initial plan 2026-04-04 00:22:41 +00:00
Sovran_Systems d71b9a6aca 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 1c4a1b651f 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] ac417df803 Initial plan 2026-04-03 21:33:12 +00:00
Sovran_Systems 7aaae36781 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 08561ba816 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 1e6333097b 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 4c237f7aa7 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] fa06fb1ea2 Initial plan 2026-04-03 21:06:42 +00:00
copilot-swe-agent[bot] 941bc08ebf Initial plan 2026-04-03 21:06:27 +00:00
Sovran_Systems 1230e135ee 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 3e3039ee7b 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] 0b87eceb03 Initial plan 2026-04-03 20:51:06 +00:00
Sovran_Systems 60ce84bc50 Update onboarding Step 2 description to clarify Njal.la account/domain/Dynamic record flow 2026-04-03 15:44:41 -05:00
Sovran_Systems a3bcdd86bd Update onboarding Step 2 instructions to clarify Njal.la setup order 2026-04-03 15:43:04 -05:00
Sovran_Systems 0c14e85753 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 7c69ee8698 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] 0ae033267f Initial plan 2026-04-03 20:29:52 +00:00
Sovran_Systems 18241cbd52 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 1a26b5889b 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] e6984d300e Initial plan 2026-04-03 20:13:23 +00:00
Sovran_Systems c11a3e2c84 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 9ef4e80a3f 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] b56d154496 Initial plan 2026-04-03 19:45:32 +00:00
Sovran_Systems e1aebc0929 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 a3a8dd0c2f 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] f0d251e992 Initial plan 2026-04-03 19:06:27 +00:00
Sovran_Systems 97a5813bd6 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 0e9148d9a6 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] 30195df0f2 Initial plan 2026-04-03 18:47:00 +00:00
Sovran_Systems d7d435d469 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 5f948d7d5b 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] 99b25c8af9 Initial plan 2026-04-03 18:19:41 +00:00
Sovran_Systems 78a807cabb 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 c69b9fba60 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 4e2f4329ca 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] 4e4cd83c03 Initial plan 2026-04-03 17:53:29 +00:00
Sovran_Systems a19e21a9e5 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 48a325d630 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] cb02254c0d Initial plan 2026-04-03 17:45:02 +00:00
Sovran_Systems 8adbb7284e 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 e54e29ba2b 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 f0ada0770a 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] 09fe26d373 Initial plan 2026-04-03 17:23:13 +00:00
Sovran_Systems 92aa02ace0 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 8a31063ba8 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] 9af1fe50cd Initial plan 2026-04-03 16:51:31 +00:00
naturallaw777 441afc5c47 added awk command 2026-04-03 11:36:03 -05:00
Sovran_Systems a3c3c6f388 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 e1595b678e 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] 51fdd8e32d Initial plan 2026-04-03 16:28:25 +00:00
Sovran_Systems f29dd13447 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 9d359dd04e 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] 3d942de399 Initial plan 2026-04-03 16:09:01 +00:00
Sovran_Systems 47ffd78bd2 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 6047acd283 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] 6b9753c860 Initial plan 2026-04-03 15:53:39 +00:00
Sovran_Systems f67b6055c4 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 d7419ae62b 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] 37ea57cbe3 Initial plan 2026-04-03 15:42:53 +00:00
Sovran_Systems 60d22b2ac3 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 016549d1c3 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] 442bf67751 Initial plan 2026-04-03 15:26:58 +00:00
Sovran_Systems 69561aafac 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 18ab929049 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] 83db460f13 Initial plan 2026-04-03 15:19:46 +00:00
Sovran_Systems 4cc3df2910 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 784033bd21 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] 91a915b297 Initial plan 2026-04-03 14:50:04 +00:00
Sovran_Systems 3619a07771 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 7cc53e1763 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] c446ee7037 Initial plan 2026-04-03 14:24:14 +00:00
naturallaw777 91004c06b6 updated wiring for hub feature enable 2026-04-03 09:07:07 -05:00
naturallaw777 375e66701b updated wiring for hub feature enable 2026-04-03 08:37:21 -05:00
naturallaw777 02e8d0271b UX update for feature manager 2026-04-03 07:31:17 -05:00
naturallaw777 0a27b5d572 deeper fix for RDP regeneration 2026-04-03 07:16:01 -05:00
naturallaw777 45f19e536a deeper fix for RDP regeneration 2026-04-03 07:10:40 -05:00
naturallaw777 38f2d64cff fix for RDP regeneration 2026-04-03 07:08:09 -05:00
Sovran_Systems e74cb3c1e5 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 a853d31d27 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] 50a110a14f Initial plan 2026-04-03 01:38:04 +00:00
Sovran_Systems 1275ccf3c9 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 3a5e38d1ae 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] 5fd261ba09 Initial plan 2026-04-03 01:26:13 +00:00
naturallaw777 9b493ea84c fix for feature manager 2026-04-02 20:15:57 -05:00
naturallaw777 a56f3d09f6 fix for feature manager 2026-04-02 20:03:13 -05:00
naturallaw777 3c8429c448 fix for feature manager 2026-04-02 20:00:27 -05:00
naturallaw777 45b822b02a reverted to old file 2026-04-02 19:49:51 -05:00
Sovran_Systems d0fea597cf 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] 83846f2cb7 Initial plan 2026-04-02 23:50:44 +00:00
naturallaw777 322c74d04f updated Feature Manager 2026-04-02 18:42:00 -05:00
Sovran_Systems ba0c35aefa 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 5a838ae778 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 17a3ecec7c 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] 170ef43184 Initial plan 2026-04-02 23:15:35 +00:00
naturallaw777 66b026e27b updated public key 2026-04-02 17:39:51 -05:00
naturallaw777 5a41882a66 added public key 2026-04-02 17:25:21 -05:00
naturallaw777 210546c3d2 fixed service layout 2026-04-02 17:20:14 -05:00
naturallaw777 f62103d6f0 added service feature 2026-04-02 17:08:20 -05:00
naturallaw777 52a1b227c0 fixed RDP layout 2026-04-02 16:48:01 -05:00
naturallaw777 a3bb6abfad fixed RDP layout 2026-04-02 16:45:27 -05:00
naturallaw777 067d4b75f3 added RDP 2026-04-02 16:39:01 -05:00
naturallaw777 b06797b591 removed start stop toggles 2026-04-02 16:35:18 -05:00
naturallaw777 1c50858f80 added qr code 2026-04-02 16:23:45 -05:00
naturallaw777 3d72f51bf2 added qr code 2026-04-02 16:17:39 -05:00
naturallaw777 5412741a4b added qr code 2026-04-02 16:10:44 -05:00
naturallaw777 6e99dc4810 added lndconnet 2026-04-02 16:03:55 -05:00
naturallaw777 a0905cb477 frontend onion links 2026-04-02 15:55:57 -05:00
naturallaw777 c4e40de2bc frontend link fix 2026-04-02 15:50:31 -05:00
naturallaw777 474d9a4c5a frontend link fix 2026-04-02 15:46:23 -05:00
naturallaw777 bb81ff55e0 frontend link fix 2026-04-02 15:43:32 -05:00
naturallaw777 a0e563bf67 frontend uses weblinks 2026-04-02 15:37:07 -05:00
naturallaw777 7bd53631a3 updated BIP110 2026-04-02 15:24:24 -05:00
naturallaw777 df30ad9ff2 updated security 2026-04-02 15:14:15 -05:00
naturallaw777 5a14e3e070 added passwd fix for user account 2026-04-02 15:09:04 -05:00
naturallaw777 0c6f9f5364 added password tile 2026-04-02 14:55:32 -05:00
naturallaw777 123990fd35 fixed cache 2026-04-02 14:50:07 -05:00
naturallaw777 3e42dab311 fixed app.js 2026-04-02 14:25:04 -05:00
naturallaw777 d4ee1d3aea added info dialog for each tile 2026-04-02 14:20:06 -05:00
naturallaw777 b60b965421 fixed stale info 2026-04-02 14:07:47 -05:00
naturallaw777 4fb9af602c fixed reboot menu 2026-04-02 14:01:08 -05:00
naturallaw777 4bf3b76304 fixed the color of buttons 2026-04-02 13:54:03 -05:00
naturallaw777 2ce1f3f441 fixed updater 2026-04-02 13:42:24 -05:00
naturallaw777 cc61f10353 updated logging 2026-04-02 13:34:29 -05:00
naturallaw777 d6b9949988 updated logging 2026-04-02 13:27:25 -05:00
naturallaw777 b39d2d84ba updated logging 2026-04-02 13:21:33 -05:00
naturallaw777 7e0580292f updated logging 2026-04-02 13:15:19 -05:00
naturallaw777 88b16fbe19 updated logging 2026-04-02 13:09:07 -05:00
naturallaw777 d098462363 added new systemd update unit 2026-04-02 12:54:32 -05:00
naturallaw777 1c238577f6 fixed ssh access 2026-04-02 12:37:43 -05:00
naturallaw777 7b35dd8869 fixed for update button in web 2026-04-02 12:31:04 -05:00
naturallaw777 2015521395 changed port 2026-04-02 12:15:53 -05:00
Sovran_Systems b807cf6144 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 0828434488 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 c992aac8e5 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 3fc22aaf9a 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] 042a72e352 Initial plan 2026-04-02 16:50:57 +00:00
156 changed files with 32630 additions and 3654 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
custom.nix
role-state.nix
*.iso
*.zip
*.pma
__pycache__/
*.pyc
*.pyo
iso/secrets/enroll-token
iso/secrets/provisioner-url
result
-5
View File
@@ -1,5 +0,0 @@
custom.nix
role-state.nix
*.iso
*.zip
*.pma
+539
View File
@@ -0,0 +1,539 @@
# 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).
---
## [1.2.1] - 2026-10-18
### Added
- Mullvad: install the client on the Desktop Role only
- Password-model.nix: fix Nix string escaping in the generate fallback
- Password & keyring cleanup: one password, one truth, no prompts
- Update security details in SECURITY.md
- Enhance README formatting and emphasis on features
- Revise SECURITY.md for clarity on Hub operations
### Changed
- Update flake.lock (nixpkgs, Sovran_Bitcoin 1.1.1, Bisq2 2.1.14) and integration
### Fixed
- Fix README formatting and clarify service exposure
- Fix formatting and clarify server access instructions
[1.2.1]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.2.1
## [Unreleased]
### Added
- Mullvad: install the VPN app and daemon, as NixOS ships them, on the Desktop Role only
### Changed
- Updated `flake.lock` (`nixpkgs`, `nixpkgs-stable`, `nixvim`, `btc-clients` with Bisq2 `2.1.14`, and `Sovran_Bitcoin` `1.1.1` with RTL `0.15.13` and Nix-native `sovran-nwc` `2.0.0`)
- Updated fallback service version map (`app/sovran_systemsos_web/versions.json`)
- Fixed deprecated `pkgs.system` reference in `flake.nix` and guarded `config.networking.domain` in `modules/sovran-bitcoin-integration.nix` when `haven` and `nwc-wallets` are both enabled
---
## [1.2.0] - 2026-10-02
### Added
- Ssh: don't open port 22 for the loopback-only sshd
- Bitcoin: drop the stray UDP 3051 firewall rule
- Installer: raise generated password entropy from ~23 to ~33 bits
- Caddy: stop filtering the RTL and Mempool sites by client address
- Hub: serve the Hub on its own port instead of through Caddy
- Hub: answer the local network only, whichever way a client arrives
- Hub: make the login lockout that LOGIN_FAIL_MAX described
- Caddy: serve the Hub, RTL and Mempool sites to local clients only
- Docs, hub, installer: say Server + Desktop makes the home IP public
- Ddns: take the public IP from Njal.la only and give it to LiveKit
### Changed
- Updated nixpkgs and Sovran_Bitcoin update and the new Bisq 1.10.9
[1.2.0]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.2.0
## [1.1.7] - 2026-09-21
### Added
- Postgresql: drop per-database autovacuum ALTERs (rejected by Postgres)
- Synapse: performance tuning for 32 GB Server+Desktop hosts
- Nextcloud, postgresql: fix Nextcloud 35 DB warnings on 32 GB hosts
- Clean up flake.nix by removing comments and LiveKit override
### Changed
- Updated nixpkgs
- Updated to php85 and fixes
- Updated to proper syntax to prevent build errors.
- Updated flake lock which contains Bisq 1.10.8 and Bisq2 2.1.13
[1.1.7]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.1.7
## [1.1.6] - 2026-09-15
### Added
- Rename 'The Sovran Hub' to 'The Hub' in README
- Rename 'Sovran Hub' to 'The Hub' in README
### Changed
- Updated nix packages includes the new mempool version
### Documentation
- Update Sovran Hub screenshot to the v1.1.5 redesign
[1.1.6]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.1.6
## [1.1.5] - 2026-09-09
### Added
- Hub: don't claim the OS "keeps itself current" on the dashboard
- Hub: point RTL credentials at /rtl/ and bump dev version to 0.15.12
- Make dashboard cards uniform and symmetric; stop clipping update text
- Add boot splash; separate Systems Operational and Security icons
- Extend welcome dashboard background to the panel edges
- Add welcome dashboard as default view
- Monochrome updater glyph; fix oversized dialog header icon
- Merge Bitcoin categories into one; rename Self-Hosted Apps to Personal Apps
- Neutral graphite theme; branded updater icon
- Simplify Systems Operational, reword Domain Status, blue restart buttons
- Polish pass: sysops wording, QR size, NWC toolbar, logo, diagnostics placement
- Rework Update System dialog to the approved design; match rebuild dialog
- Redesign The Hub web UI: softer dark theme, sidebar nav, status widgets
- Nixpkgs update
- Switch LibreOffice from fresh to stable version
- Ui: simplify element-calling port modal — trim verbose steps and remove extra verification task
### Changed
- Updated nixpkgs
- Updated Sovran-Bitcoin
- Updated flake to build through the new sovran-bitcoin input
### Fixed
- Fix role fit-and-finish: Desktop-only router note, narrow-viewport layout
- Self-heal truncated/corrupt Nix downloads and keep failed updates retryable
[1.1.5]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.1.5
## [1.1.4] - 2026-09-02
### Added
- Element-calling: fix Nix string interpolation of LAN_CIDR echo
- Docs/hub: update Element Calling port guidance to the new port set
- Element-calling: fix Wi-Fi calls and tighten media/TURN ports
- Flake: pin LiveKit to 1.13.6 for rtc.advertise_internal_ip support
### Changed
- Updated nixpkgs
- Updated sovran-bitcon
- Dedupe NWC tooling — use Sovran_Bitcoin's sovran-nwc
- Update sovran-bitcoin flake input
- Extract bitcoin stack into Sovran_Bitcoin flake input
### Fixed
- Set lnurl domainFile for Hub-managed Lightning Address domain
- Correct disablewallet casing
- Fixed typo
[1.1.4]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.1.4
## [Unreleased]
### Changed
- Element Calling: pin LiveKit to 1.13.6 and advertise the LAN interface
(rtc.advertise_internal_ip) so Wi-Fi callers get a local ICE candidate and
calls no longer depend on router NAT hairpin
- Element Calling: use a single UDP media mux (7882); move the TURN relay to
40000-40099 and remove the unreachable 5349 TURN/TLS listener
- Hub: update Element Calling router-port guidance to the new port set
### Fixed
- Element Calling: TURN relay could not deliver media to the SFU on routers
without NAT loopback (turn.allow_restricted_peer_cidrs now permits the LAN
subnet derived from the primary interface)
## [1.1.3] - 2026-08-27
### Added
- Sparrow update
- Nixpkgs update with bisq and bisq2
- Albyhub: re-harvest hashes for fixed sovran tag
- Albyhub default.nix hash update
- Albyhub default.nix fix
- Albyhub: build from sovran fork tag (LND-only, no frontend)
- Vendor v1.24.0 LND-only, no-frontend build
- Ux: simplify domain and router setup guidance
### Fixed
- Evaluate local vendored package versions directly
- Use proxyVendor to preserve secp256k1-zkp cgo headers
[1.1.3]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.1.3
## [1.1.2] - 2026-08-21
### Added
- Nixpkgs update with Bisq update
- Unify public-IP detection into one privacy-first script
- Fix Element X discovery and harden federated calling
- Nixpkgs update
- Add tor-browser to the package list
### Changed
- Use pkgs.lndinit, drop local packages/lndinit
- Update RTL to 0.15.10
### Fixed
- Move system.activationScripts under the config attribute
- Reuse Hub external IP for LiveKit, drop egress service calls
- Derive Restart required from boot default vs running system
- Reconcile completed updates after polling stalls
- Migrate i2pd SAM settings for nixpkgs 26.11
- Order turn-setup after network-online to fix boot-time red dot
- Restore a Zeus-scannable LND REST connect QR
### Security
- Prevent LND admin macaroon exposure in curl argv
### Documentation
- Update desktop and Sovran Hub screenshots
[1.1.2]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.1.2
## [1.1.1] - 2026-08-15
### Added
- Update release-stable.sh
- Enhance release script with preflight checks and metadata
- Migrate dock/folder/mime entries from brave to brave-origin on upgrade
- Skip Brave Origin startup dialog in Hub launcher
- Switch default browser to Brave Origin (brave-origin)
- Add Bitcoin Core Tor IBD gossip control
- Replace Bitcoin Knots with Bitcoin Core
### Fixed
- Persistent browser profile so logout survives window reopen
- Recover from expired sessions and preserve logout
- Prevent Bitcoin Core switch from hanging the Hub UI
### Documentation
- Update README ISO download links to v1.1.0
[1.1.1]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.1.1
## [Unreleased]
### Changed
- Replaced Brave Browser with Brave Origin (the new `brave-origin` nixpkgs
package) as the desktop's default browser: installed package, dock
favorite, Browsers app-folder entry, `xdg.mime.defaultApplications`, and
the `$BROWSER` session variable now all point at Brave Origin, and the
Sovran Hub launcher runs it.
- On installs upgrading from the old Brave package, the desktop theme init
now migrates `brave-browser.desktop` → `brave-origin.desktop` in the dock
favorites, the Browsers app folder, and the user's `mimeapps.list`, so
the dock icon and default-browser handler survive the switch without a
manual re-pin.
### Fixed
- Zeus Connect QR in the Hub was invalid: the LND-only `lndconnect`
wrapper used flags Zeus/`lndconnect` do not understand (`--cert`,
`--macaroon`), read a non-existent onion path (`free/lnd.onion`),
and collided with LND's P2P hidden service. The wrapper now matches
the nix-bitcoin contract — dedicated `lnd-rest` onion on port 8080,
`--nocert` over Tor, admin macaroon in the URI — so scanning the Hub
QR in Zeus works again.
- The Hub launcher now uses a persistent per-user browser profile
(`$XDG_STATE_HOME/sovran-hub-browser`) instead of a throwaway `/tmp`
profile that was deleted on exit. The throwaway profile wiped the
`hub_manual_logout` marker cookie on every close, so `/auto-login`
silently logged the user straight back in after they signed out and
reopened the Hub window. The persistent profile makes explicit logout
stick (until the next password login) while still skipping Brave Origin's
one-time startup dialog.
- The Hub now redirects to the login page when an API request finds an
expired browser session, instead of leaving the dashboard tile grid in a
loading state while `/api/services` continues returning 401 every five
seconds.
- Explicitly logging out now suppresses the desktop launcher's local
auto-login after the Hub window is closed and reopened. A successful
password login clears that preference.
- 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).
- The update modal now reconciles persisted update state after a page/RDP
reconnect, bounds every status request with a timeout, prevents overlapping
async polls, and reports an explicit "status unavailable" state with a
Retry Status action instead of spinning forever. Verbose Nix logs are
rendered incrementally and bounded so they cannot stall the browser UI.
- Successful staged updates now record the exact NixOS generation. The Hub
keeps showing "Restart required" until that generation is active, then
clears the marker after reboot (with log-based recovery for older updates).
- Pending-reboot state is now derived from NixOS itself — the boot default
(`/nix/var/nix/profiles/system`) versus the running `/run/current-system`
— instead of from Hub-written marker files. Updates performed from a
terminal or support session (which never touch the Hub's status files)
previously left a stale `REBOOT_REQUIRED` marker the Hub could never
clear, pinning the "Restart required" badge forever even after many
reboots. The marker self-heals to `IDLE` on the next status read whenever
boot default and running system agree, and recording the informational
`.generation` marker can no longer fail an otherwise successful update.
---
## [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 Core, 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
@@ -0,0 +1,112 @@
# 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.
+889
View File
@@ -0,0 +1,889 @@
<div align="center">
<img src="iso/assets/sovran-hub-icon.svg" alt="Sovran Systems logo" width="160" />
# Sovran_SystemsOS
### Bitcoin sovereignty. Sovereign computing. One system.
Sovran_SystemsOS is a free and open-source operating system built for two
inseparable freedoms: **Bitcoin self-custody** and **sovereign computing**.
Hold your own keys, trade peer-to-peer, and verify your own money with your
own node. Then claim that same uncompromising ownership over the rest of your
digital life — your files, communications, passwords, and websites — all on
hardware you control, running auditable open-source software you can trust.
Every installation is a private [NixOS](https://nixos.org) desktop with
**[Sparrow Wallet](https://sparrowwallet.com)**, **[Bisq](https://bisq.network)**,
and **[Bisq 2](https://github.com/bisq-network/bisq2)** ready to use. Move beyond
custodial exchanges from the first boot, and grow into your own Bitcoin node,
Lightning infrastructure, private cloud, and communications platform when you
are ready.
[Visit the Website](https://sovransystems.com) ·
[Download the ISO](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.1.iso) ·
[Try it safely in a VM](#try-it-first-in-a-virtual-machine) ·
[Verify the Download](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.1.iso.sha256) ·
[Build from Source](#build-from-source)
<img src="assets/desktop-screenshot.webp" alt="Sovran_SystemsOS private Bitcoin desktop" width="800" />
*Bitcoin sovereignty from the first boot.*
</div>
> **📌 Active development on GitHub `main` / Gitea `staging-dev` — releases on
> [Gitea `stable`](https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/src/branch/stable).**
> See [Development workflow](#development-workflow) for details.
---
## Contents
- [Why Sovran_SystemsOS?](#why-sovran_systemsos)
- [Try it first in a virtual machine](#try-it-first-in-a-virtual-machine)
- [What is included](#what-is-included)
- [Three modes](#three-modes)
- [Use it your way](#use-it-your-way)
- [The Hub](#the-hub)
- [Install Sovran_SystemsOS](#install-sovran_systemsos)
- [For developers](#for-developers)
- [Development workflow](#development-workflow)
- [Build from source](#build-from-source)
- [Publishing a release](#publishing-a-release)
- [About Bitcoin wallet entropy](#about-bitcoin-wallet-entropy)
- [Security approach](#security-approach)
- [Acknowledgements](#acknowledgements)
- [License](#license) · [Contributing](#contributing)
---
## Why Sovran_SystemsOS?
Bitcoin lets you hold and transfer value without asking a bank, exchange, or
custodian for permission, but that freedom depends on the software and
infrastructure you choose. Your wider digital life works the same way: files,
messages, and passwords kept on someone else's servers are never fully yours.
Sovran_SystemsOS solves both with one operating system:
- **Hold your own keys** with **Sparrow Wallet**. Create and manage wallets,
connect hardware signing devices, use multisignature setups, build and
inspect transactions, and control UTXOs and coin selection.
- **Buy and sell Bitcoin peer-to-peer** with **Bisq** and **Bisq 2**. No
central company holds user funds, and no exchange account stands between
buyers and sellers.
- **Verify your own Bitcoin** with a full node: [Bitcoin Core](https://bitcoin.org) and
[Electrs](https://github.com/romanz/electrs), so your wallets connect to
*your* node instead of a stranger's.
- **Use Lightning** with [LND](https://github.com/lightningnetwork/lnd) and
[Ride The Lightning](https://github.com/Ride-The-Lightning/RTL).
- **Accept Bitcoin directly** with [BTCPay Server](https://btcpayserver.org),
with no payment processor in the middle.
- **Own the rest of your digital life** with
[Nextcloud](https://nextcloud.com) files and calendars,
[Matrix](https://matrix.org) communications, a
[Vaultwarden](https://github.com/dani-garcia/vaultwarden) password vault,
and your own [WordPress](https://wordpress.org) website.
- **Protect your network privacy** with [Tor](https://www.torproject.org)
integration across the Bitcoin stack.
- **Control everything from the Sovran Hub**, on the desktop or from any
browser on your local network.
Other projects solve one piece of this puzzle: a distribution that runs a
wallet, a node project that runs Bitcoin services, a self-hosting stack that
replaces a cloud app. Sovran_SystemsOS brings them together on a reproducible,
auditable NixOS foundation: Bitcoin tools from the first boot, one control
center, and hardware you own.
> Sovran_SystemsOS provides tools for self-custody and peer-to-peer Bitcoin
> use. Users remain responsible for protecting their keys, understanding their
> trades, following applicable laws, and maintaining secure backups.
---
## Try it first in a virtual machine
**Curious, but not ready to replace your current operating system?** Start with
Sovran_SystemsOS in a virtual machine (VM): a window on your existing Windows,
macOS, or Linux computer, using a virtual disk file instead of your internal
drive. Explore the desktop, Sovran Hub, Sparrow, Bisq, and the installation
experience before changing how you use any physical machine. Closing or
deleting the VM leaves your current operating system untouched, and installing
on a dedicated computer later is still open to you.
Use the same ISO with [VirtualBox](https://www.virtualbox.org), VMware,
QEMU/KVM, Proxmox, and similar x86_64 VM software. For a first look, select
**Desktop Only**, allocate at least **8 GB RAM**, and create a **256 GB or
larger dynamically allocated virtual disk**. Full requirements are in
[Installing in a virtual machine](#installing-in-a-virtual-machine-optional).
> **A VM is for evaluation and learning, not a substitute for a dedicated,
> hardened setup.** Do not hold meaningful Bitcoin, recovery phrases,
> passwords, or other sensitive data in a trial VM. Avoid attaching physical
> drives, and be deliberate about shared folders, clipboard sharing, and
> network settings. Review VM disk selections before confirming an install.
---
## What is included
Depending on the selected mode and enabled features, Sovran_SystemsOS brings
together a growing collection of private, open-source tools. The Sovran Hub
presents and manages the features available on your system.
### Your money — Bitcoin sovereignty
- Bitcoin Core, Electrs, and Tor integration
- LND and Ride The Lightning, with [Alby Hub](https://albyhub.com) for Nostr
Wallet Connect (NWC) connections
- BTCPay Server
- Sparrow Wallet, Bisq, and Bisq 2, with automatic wallet-to-node connections
- Optional: a self-hosted [Mempool](https://github.com/mempool/mempool)
explorer
Run your own Bitcoin infrastructure. Verify your own money. Trust no one.
### Your voice — private communications
- The [Synapse](https://github.com/element-hq/synapse) homeserver (Matrix) and
the [Element](https://element.io) client
- Optional Matrix-native Element audio and video calling, powered by
[LiveKit](https://livekit.io)
- Optional [Haven](https://github.com/bitvora/haven)
[Nostr](https://github.com/nostr-protocol/nostr) relay
Communicate without making Big Tech the owner of your identity or
conversations.
### Your cloud — self-hosting and storage
- Nextcloud files, calendars, and contacts
- Vaultwarden password vault
- WordPress websites
- [Caddy](https://caddyserver.com) with private service domains
- Optional remote desktop
Keep your files, passwords, calendar, contacts, website, and services on
hardware you control.
### Your desktop
- [GNOME](https://www.gnome.org) desktop
- [Brave Origin](https://brave.com/origin/) and
[Firefox](https://www.mozilla.org/firefox) browsers
- [Mullvad VPN](https://mullvad.net) desktop app and daemon, on Desktop Only
- File management, email, calendar, and office applications
- System monitoring and administration tools
---
## Three modes
Every mode shares the same foundation: the private NixOS and GNOME desktop,
the Sovran Hub, and Sparrow, Bisq, and Bisq 2. What changes is how much
Bitcoin and self-hosting infrastructure runs on the machine.
| Mode | Best for | What you get |
|---|---|---|
| **Desktop** | Everyday users and computers with modest hardware | Sparrow, Bisq, and Bisq 2 for self-custody and peer-to-peer Bitcoin use, plus the Mullvad VPN app |
| **Node** | People ready to verify and operate their own Bitcoin infrastructure | Everything in Desktop, plus the full Bitcoin stack: Bitcoin Core, Electrs, LND, Ride The Lightning, BTCPay Server, and wallet-to-node connections. **Publishes nothing unless you enable *Put BTCPay Server Online* or *Lightning Wallet Connections*:** [read this first](#server--desktop-and-your-home-ip-address) |
| **Server + Desktop** | Bitcoiners who want the same sovereignty over their communications, cloud, passwords, and web services | The complete Node stack, plus the private self-hosted services. **Makes your home IP address public:** [read this first](#server--desktop-and-your-home-ip-address) |
**Desktop: start with your keys.** Desktop is not a reduced or Bitcoin-free
edition: it is a complete, private everyday computer with a clean GNOME
desktop, Tor, the Sovran Hub, and the Mullvad VPN app and daemon — which no
other role installs — and a lower-hardware path to self-custody and non-KYC
Bitcoin tools from the first boot. Move to Node mode when your hardware,
storage, and needs are ready.
Mullvad is installed the way NixOS ships it and nothing in it is configured for
you: create an account at [mullvad.net](https://mullvad.net), sign in from the
app, and set the relay and the app's own switches to taste. It is the one thing
that reaches the network from this machine as an application rather than as a
service, so Server + Desktop and Bitcoin Node — built to serve — do not carry
it. On Desktop it is simply there; remove it with
`services.mullvad-vpn.enable = lib.mkForce false;` in `custom.nix` if you would
rather not have it.
**Node: verify your own money.** You operate the infrastructure that performs
the verification instead of asking someone else's server: your node verifies,
your wallet connects to it, your keys stay yours.
Node publishes nothing by default. Two Node features need a public domain and
make your home IP address public when enabled: **Put BTCPay Server Online** and
**Lightning Wallet Connections**. Everything else, Mempool and Ride The
Lightning included, stays on your home network or reaches you through Tor — see
[Server + Desktop and your home IP address](#server--desktop-and-your-home-ip-address).
**Server + Desktop: sovereignty beyond money.** The same principle, applied
to your data, communications, identity, and services.
### Recommended hardware
| | **Desktop** | **Node** | **Server + Desktop** |
|---|---|---|---|
| Processor | 64-bit Intel or AMD, ~2015 or newer | Intel or AMD x86, ~3 years old or newer | Same as Node |
| RAM | 8 GB | 16 GB | 32 GB |
| Storage | 256 GB SSD | 500 GB NVMe (OS) + 2 TB NVMe (timechain) | Same as Node |
| Network | Any broadband | Unmetered, ~200 Mbps down / 50 Mbps up | Same as Node |
| Also needed | — | — | A domain for publicly accessible services |
> **Before choosing Server + Desktop:** this mode makes services reachable from
> the public internet, which means opening specific ports on your home network.
> Before you start, confirm three things: you can log in to your router's admin
> panel, the panel includes a port-forwarding section, and your internet
> provider allows port forwarding.
### Server + Desktop and your home IP address
> **⚠️ Server + Desktop makes your home IP address public.**
> Public services need a domain name that points at your home internet
> connection. When you finish the guided domain setup, Sovran_SystemsOS puts
> your home's public IP address in a Dynamic DNS record at
> [Njal.la](https://njal.la) and keeps it up to date, and you forward ports 80
> and 443 on your router to this computer. From then on:
>
> - **Anyone can look up your domain and see your home IP address,** which
> reveals your internet provider and approximate location and ties everything
> you publish on that domain to your home connection. Domain privacy does not
> hide it: registrar privacy protects the registrant's identity, not the IP
> address in your DNS records.
> - **Your connection is open to the whole internet on those ports.** Scanners
> and bots constantly probe public IP addresses, so expect automated probing
> and login attempts against every service you publish.
> - **Your service names are discoverable.** Public HTTPS certificates are
> listed in Certificate Transparency logs, so hostnames such as
> `vault.yourdomain.com` can be found and resolved to your IP address, even
> if you never share them.
**Desktop** publishes nothing. **Node** publishes nothing unless you enable
*Put BTCPay Server Online* or *Lightning Wallet Connections* — see
[Three modes](#three-modes).
**If you do not want your home IP address to be public,** choose Desktop or
Node. Advanced users can put a VPS, reverse proxy, or tunnel in front of their
services so DNS points there instead of at their home; Sovran_SystemsOS does
not set that up, and the Hub's domain checks expect DNS to point at your home
IP address.
<details>
<summary><strong>What happens technically</strong></summary>
- You create a **Dynamic** DNS record at Njal.la and paste its update command
into the Hub, which only accepts `njal.la` update URLs.
- The `sovran-ddns-update` timer points your record at the address the request
came from: after you save a domain, two minutes after boot, then every 15
minutes. Element calling and the Hub use that same address. See
`modules/core/njalla.nix`.
- Once a service that needs a domain is on, the firewall opens TCP and UDP
ports 80 and 443 for Caddy, which requests public HTTPS certificates for the
domains you configure. The Hub lists any additional ports a feature needs.
See `modules/core/caddy.nix`.
</details>
---
## Use it your way
You do not have to replace the operating system on your current computer to
benefit from Sovran_SystemsOS.
### As your everyday computer
Install Sovran_SystemsOS on a desktop, laptop, or mini PC and use its clean
GNOME desktop as your daily operating system, with the Bitcoin software and
the tools of your selected mode already in place.
### As a private Bitcoin and home server
Prefer to keep using Windows, macOS, Linux, Android, or iOS? Install
Sovran_SystemsOS on a separate computer and let it run quietly on your local
network, with or without a monitor. From any other device on the same network,
open a browser, visit `http://sovransystemsos.local:8937`, and manage
everything from [the Hub](#the-hub).
---
## The Hub
### Your private infrastructure, controlled from any screen.
The Hub is the command center built into Sovran_SystemsOS: a local desktop
application and a private web interface served by your Sovran_SystemsOS
machine. The device opening it needs only a modern browser and access to the
same local network.
From one place, the Hub lets you:
- Open, monitor, start, and stop your services
- Configure system features and manage service domains and credentials
- Reach your Bitcoin tools, private cloud, and communications
- Perform supported system operations without everyday terminal commands
<img src="assets/sovran-hub-screenshot.webp" alt="The Sovran Hub welcome dashboard" width="800" />
*The Hub: your whole system at a glance — Bitcoin, Lightning, and your private apps.*
### Example home setup
```text
Your local network
│
┌──────────────────────┼──────────────────────┐
│ │ │
Windows laptop Phone or tablet Mac or Linux
│ │ │
└─────── Browser: sovransystemsos.local:8937 ─┘
│
▼
┌──────────────────────────┐
│ Sovran_SystemsOS │
│ │
│ • The Hub │
│ • Bitcoin node │
│ • Sparrow Wallet │
│ • Bisq and Bisq 2 │
│ • Lightning │
│ • Private cloud │
│ • Communications │
│ • Password vault │
│ • Hosted services │
└──────────────────────────┘
```
> **Local access:** the Hub is available at
> `http://sovransystemsos.local:8937` to devices connected to the same local
> network (not on Desktop, which publishes nothing). It is protected by
> authentication and answers only your local network.
### Tech Support
The Hub can give a Sovran Systems technician temporary access to your machine.
Support runs over SSH, and **Tech Support requires SSH Remote Access, which is
off by default.**
Turning SSH on makes the machine listen on all interfaces and opens **TCP port
22** in the firewall. Letting support in from outside your home means
forwarding port 22 on your router, which puts SSH on the public internet where
scanners and brute-force attempts will find it. Turn SSH off when the session
ends; the Hub prompts you to.
Support signs in as the restricted `sovran-support` account, not as root, with
one key that expires after 24 hours. Wallet paths stay locked unless you unlock
them for a limited time, and every session event is written to
`/var/log/sovran-support-audit.log`. See
[Restricted support access](SECURITY.md#restricted-support-access).
---
## Install Sovran_SystemsOS
Sovran_SystemsOS is free and open source: download the installer, verify it,
write it to a USB drive, and install it yourself, in control from the first
step.
An ISO is a complete installation image containing the operating system and
installer. It is not copied to a USB drive like a document; it must be written
with an imaging application such as [Balena Etcher](https://etcher.balena.io).
**Before you begin, you will need:**
- A compatible 64-bit computer, and a backup of anything important on it
- A USB drive that can be erased
- Another computer for downloading and preparing the installer
- An internet connection
> **Important:** Installing an operating system can erase the selected
> destination drive. Back up important files and review every disk selection
> carefully before continuing.
### 1. Download the ISO and checksum
- [Download Sovran_SystemsOS-1.2.1.iso](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.1.iso)
- [Download Sovran_SystemsOS-1.2.1.iso.sha256](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.1.iso.sha256)
The download may take some time. Do not rename or modify the ISO before
verifying it, and keep both files in the same folder.
### 2. Verify the checksum
A checksum is a digital fingerprint of a file. Verifying it confirms the ISO
downloaded completely and without corruption. The checksum produced from your
ISO must match the published one exactly.
<details>
<summary><strong>Linux</strong></summary>
Open a terminal in the download folder and run:
```bash
sha256sum --check Sovran_SystemsOS-1.2.1.iso.sha256
```
A successful comparison reports:
```text
Sovran_SystemsOS-1.2.1.iso: OK
```
</details>
<details>
<summary><strong>macOS</strong></summary>
Open Terminal in the download folder and run:
```bash
shasum -a 256 Sovran_SystemsOS-1.2.1.iso
```
Compare the value shown in Terminal with the value inside
`Sovran_SystemsOS-1.2.1.iso.sha256`.
</details>
<details>
<summary><strong>Windows</strong></summary>
Open PowerShell in the download folder and run:
```powershell
Get-FileHash .\Sovran_SystemsOS-1.2.1.iso -Algorithm SHA256
```
Compare the value under `Hash` with the published checksum.
</details>
**If the values do not match, do not install.** Delete the downloaded ISO,
download it again, and repeat the verification. Continue only after the values
match exactly.
### 3. Write the ISO to a USB drive
1. Download and install [Balena Etcher](https://etcher.balena.io), then
connect the USB drive.
2. Choose **Flash from file** and select `Sovran_SystemsOS-1.2.1.iso`.
3. Choose **Select target**, select the USB drive, and review your selection
carefully.
4. Choose **Flash** and wait for the writing and verification process to
finish.
> **Warning:** Flashing erases the selected drive. Verify that you selected
> the USB drive and not another storage device.
After flashing, your computer may report that it cannot read the USB drive or
may show several unfamiliar partitions. This is normal for a bootable Linux
installer. Do not format the drive.
### 4. Boot from the USB drive
1. Leave the USB drive connected and restart the destination computer.
2. Open the computer's boot-device menu. Common keys include `F12`, `F11`,
`F10`, `F9`, `Esc`, and `Delete`; the correct key is often shown briefly
when the computer powers on.
3. Select the USB drive and start the Sovran_SystemsOS installer.
If the normal operating system starts instead, restart and try the boot-menu
key again.
### Installing in a virtual machine (optional)
Sovran_SystemsOS can be tested in VirtualBox, VMware, QEMU/KVM, Proxmox, and
similar x86_64 virtual machines. Use production-like resources where possible:
- Allocate **8 GB RAM or more** for Desktop Only. Node and Server + Desktop
should follow the normal 16 GB / 32 GB recommendations.
- Create a **256 GB or larger virtual OS disk**. Thin-provisioned / dynamically
allocated disks are fine; they do not consume the full size immediately.
- Use NAT or bridged networking with internet access before opening the
installer.
- UEFI/EFI firmware is preferred: in UEFI VMs the installer avoids depending
on VM NVRAM boot-entry writes, and a VM that boots the ISO in legacy BIOS
mode automatically installs GRUB instead.
- For Node or Server + Desktop, attach a **second 2 TB virtual data disk**.
If you only attach one disk, choose **Desktop Only**.
- Present the install target as a normal virtual disk such as VirtIO, SATA,
SCSI, or NVMe. USB-attached disks are hidden by the installer so you cannot
erase the installer USB by mistake.
### 5. Install
Follow the on-screen installer. Before confirming:
- Verify that you selected the correct destination drive.
- Understand that existing partitions and data may be erased.
- Disconnect unrelated external drives if you are unsure which drive is which.
- Confirm that the computer is connected to reliable power.
When installation is complete, restart the computer, remove the USB drive when
instructed, allow Sovran_SystemsOS to start from the installed drive, and
complete the initial setup.
### 6. Open the Sovran Hub
Open the Hub directly from the Sovran_SystemsOS desktop, or from any other
device on the same local network at:
```text
http://sovransystemsos.local:8937
```
Sign in with your Sovran_SystemsOS credentials. Desktop does not publish the Hub
on the network, so in that mode open it from the desktop.
<details>
<summary><strong>If sovransystemsos.local:8937 does not open</strong></summary>
1. Make sure the Sovran_SystemsOS machine is powered on, and allow it a few
minutes to finish starting.
2. Make sure both devices are connected to the same local network, and that
you entered the full address `http://sovransystemsos.local:8937`,
including the `:8937`.
3. Avoid guest Wi-Fi networks, which may prevent devices from seeing one
another.
4. Temporarily disconnect any VPN that may interfere with local-network
access.
5. Try another browser or device on the same network.
</details>
### Prefer guided help?
Not technical? You do not have to figure everything out alone. Visit the
[Sovran Systems website](https://sovransystems.com) to learn about guided
setup, supported hardware, and Royal Membership.
---
## For developers
Sovran_SystemsOS combines [NixOS](https://nixos.org), the standalone
[Sovran_Bitcoin](https://github.com/naturallaw777/Sovran_Bitcoin) flake for
Bitcoin and Lightning, desktop packages from
[btc-clients-nix](https://github.com/emmanuelrosa/btc-clients-nix), and the
Sovran Hub. The Bitcoin and Lightning modules live in the Sovran_Bitcoin
repository and are consumed as a flake input. OS-specific Bitcoin customizations
(Second_Drive paths, operator user, Hub integration) are bridged by
`modules/sovran-bitcoin-integration.nix`. Legacy `nix-bitcoin.*` option names
and `/etc/nix-bitcoin-secrets` remain for compatibility.
### Development workflow
| Branch | Host | Purpose |
|---------------|---------------------------------------------------|-----------------------------|
| `main` | [GitHub](https://github.com/naturallaw777/Sovran_SystemsOS) | Active development & PRs |
| `staging-dev` | [Gitea](https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS) | Synced with `main` |
| `stable` | [Gitea](https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/src/branch/stable) | Tested, release-ready builds |
**Flow:** develop → sync `main` ↔ `staging-dev` → promote to `stable`
> ⚠️ `main` and `staging-dev` may contain unreleased or less-tested code. For
> stable, audited code, use Gitea
> [`stable`](https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/src/branch/stable).
### Technology
- [NixOS](https://nixos.org) and [Nix flakes](https://nixos.wiki/wiki/Flakes)
for declarative, pinned system configuration
- [Sovran_Bitcoin](https://github.com/naturallaw777/Sovran_Bitcoin) for the
Bitcoin and Lightning stack (consumed as a flake input)
- `modules/sovran-bitcoin-integration.nix` for OS-specific Bitcoin overrides
- [btc-clients-nix](https://github.com/emmanuelrosa/btc-clients-nix) for the
Sparrow, Bisq, and Bisq 2 packages
- [Python](https://www.python.org) and [FastAPI](https://fastapi.tiangolo.com)
for the Sovran Hub backend
- JavaScript, HTML, and CSS for the Hub interface
- [GNOME](https://www.gnome.org) desktop environment
- [Caddy](https://caddyserver.com) for local and public service routing
- [Tor](https://www.torproject.org) for Bitcoin network privacy
- [Mullvad VPN](https://mullvad.net), installed as the stock NixOS service on
the Desktop Role only (`pkgs.mullvad` daemon and `pkgs.mullvad-vpn` app)
- [AGPL-3.0](LICENSE) licensing
### Build from source
You need a system with Nix installed and flakes enabled.
Clone the **stable** branch from Gitea (recommended for builds):
```bash
git clone -b stable https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS.git
cd Sovran_SystemsOS
```
Or clone the **active development** line from GitHub:
```bash
git clone https://github.com/naturallaw777/Sovran_SystemsOS.git
cd Sovran_SystemsOS
```
Then build the installer ISO:
```bash
nix build \
.#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage
```
The resulting build output will be available through the `result` symlink.
### Publishing a release
Releases are managed with `scripts/release-stable.sh` and `scripts/upload-cdn.sh`:
1. Run the stable release script to bump version, tag, update changelog, and push/create releases:
```bash
./scripts/release-stable.sh [version]
```
2. Build the installer ISO:
```bash
nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage
```
3. Copy, checksum, verify, and optionally upload to CDN:
```bash
./scripts/upload-cdn.sh --upload
```
### Common development commands
Run these commands from the flake root.
```bash
# Build the installer ISO
nix build \
.#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage
# Build the system configuration
nixos-rebuild build --flake .#nixos
# Test without making the change permanent
sudo nixos-rebuild test --flake .#nixos
# Activate the new configuration
sudo nixos-rebuild switch --flake .#nixos
# Stage the configuration for the next boot
sudo nixos-rebuild boot --flake .#nixos
# Update pinned flake inputs (review and test before committing flake.lock)
nix flake update
# Roll back the last activated generation
sudo nixos-rebuild switch --rollback
```
### Repository map
| Path | Purpose |
|---|---|
| `flake.nix` | Declares flake inputs, the running system, and installer outputs |
| `flake.lock` | Pins dependencies for reproducible builds |
| `configuration.nix` | Base host, boot, desktop, user, security, backup, and system configuration |
| `modules/` | Core modules, self-hosted services, and optional features |
| `modules/sovran-bitcoin-integration.nix` | OS-specific Bitcoin overrides (Second_Drive, operator, Hub wiring) |
| `modules/core/` | Roles, Hub integration, Caddy, desktop, support, and other core behavior |
| `app/` | Sovran Hub backend, templates, static assets, scripts, and web interface |
| `scripts/` | Automated release, build, and CDN upload utility scripts |
| `iso/` | Installer configuration, installer code, and installer assets |
| `tests/` | Security and Nix integration checks |
| `assets/` | Documentation images |
| `custom.template.nix` | Template for local features and service overrides |
On installed systems, `flake.nix` also imports `role-state.nix` (the selected
mode), `custom.nix` (generated from `custom.template.nix`), and
`hardware-configuration.nix`. These are generated per machine, so they are
gitignored and not part of this repository.
### Architecture overview
Sovran_SystemsOS is assembled from a reproducible Nix flake.
```text
┌─────────────────────────┐
│ flake.nix │
│ │
│ Inputs and system │
│ build outputs │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ configuration.nix │
│ │
│ Host, desktop, users, │
│ boot, security, backup │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ modules/modules.nix │
│ │
│ Core modules, services, │
│ and optional features │
└────────────┬────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Sovran Hub │ │ Bitcoin │ │ Private │
│ and desktop│ │ ecosystem │ │ services │
└────────────┘ └────────────┘ └────────────┘
```
The Hub writes supported user choices into the local configuration. NixOS then
rebuilds the machine into the selected declarative state.
### Module overview
| Area | Modules |
|---|---|
| Core platform: roles, Hub, desktop integration, Caddy, domains, support, remote deployment | `modules/core/` |
| Shared credentials | `modules/credentials.nix` |
| Bitcoin and Lightning stack | [Sovran_Bitcoin](https://github.com/naturallaw777/Sovran_Bitcoin) flake input + `modules/sovran-bitcoin-integration.nix` |
| Automatic wallet-to-node connections | `modules/wallet-autoconnect.nix` |
| Matrix Synapse | `modules/synapse.nix` |
| Optional Element audio and video calling via LiveKit | `modules/element-calling.nix` |
| Optional Haven Nostr relay | `modules/haven.nix` |
| Nextcloud, Vaultwarden, WordPress | `modules/nextcloud.nix`, `modules/vaultwarden.nix`, `modules/wordpress.nix`, `modules/php.nix` |
| Optional remote desktop and public SSH | `modules/rdp.nix`, `modules/sshd.nix` |
| Mullvad VPN app (Desktop Role only) | `modules/mullvad.nix` |
Feature availability and defaults may change as Sovran_SystemsOS develops:
review the relevant Nix module before relying on a default in production.
---
## About Bitcoin wallet entropy
Wallet recovery words control the funds. Never share them with a website,
support technician, cloud service, or chat application.
For meaningful balances, prefer a well-reviewed hardware signer and follow its
verified backup process. A BIP39 passphrase is optional, advanced protection,
not a replacement for recovery words: back it up separately, because losing
either item can make the wallet unrecoverable.
Keep durable offline backups in separate secure locations, test recovery before
relying on a wallet, and begin with a small amount.
---
## Security approach
Sovran_SystemsOS uses layered controls:
- Pinned flake inputs and hash-pinned source archives
- Firewall enabled; SSH Remote Access and remote desktop disabled by default.
Enabling SSH opens TCP port 22 and the Hub then asks you to forward it
- Separate service users, systemd sandboxing, and loopback bindings where practical
- Tor enforcement for supported Bitcoin services
- Restricted, time-limited support access with scoped `sudo`
- Operator-controlled public service exposure (Server + Desktop Role or Node Role when enabled BTCpayserver and/or Lightning Wallet Connections
[makes your home IP address public](#server--desktop-and-your-home-ip-address)
once you set up a domain)
See [`SECURITY.md`](SECURITY.md) for the threat model, limitations, reporting,
and operator guidance.
---
## Acknowledgements
Sovran_SystemsOS stands on the work of exceptional free and open-source projects and contributors.
### NixOS
Gratitude to the [NixOS](https://nixos.org) team, the [Nixpkgs](https://github.com/NixOS/nixpkgs) maintainers, and the broader [Nix community](https://github.com/nix-community).
NixOS provides the reproducible, declarative foundation that makes Sovran_SystemsOS possible: its module system, package ecosystem, [flakes](https://nixos.wiki/wiki/Flakes), and generation-based system management allow an entire Bitcoin operating system to be described, audited, rebuilt, upgraded, and rolled back from source.
### nix-bitcoin
The in-repository Bitcoin stack began with code adapted from
[nix-bitcoin](https://github.com/fort-nix/nix-bitcoin), primarily from commit
[`360e30f`](https://github.com/fort-nix/nix-bitcoin/commit/360e30fee5ba32f9fecc89bc35628195d9d2dbbe).
It has since been narrowed to Sovran's supported services and is maintained in
this repository; nix-bitcoin is no longer a flake input or build dependency.
Its MIT notice is retained in [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
### Emmanuel Rosa and btc-clients-nix
Thanks to [Emmanuel Rosa](https://github.com/emmanuelrosa) for [btc-clients-nix](https://github.com/emmanuelrosa/btc-clients-nix), which packages the Bitcoin desktop software included in every installation:
- [Sparrow Wallet](https://github.com/sparrowwallet/sparrow)
- [Bisq](https://github.com/bisq-network/bisq)
- [Bisq 2](https://github.com/bisq-network/bisq2)
### The upstream Bitcoin ecosystem
Sovran_SystemsOS also depends on the work of the developers and communities behind:
- [Bitcoin Core](https://github.com/bitcoin/bitcoin)
- [Sparrow Wallet](https://github.com/sparrowwallet/sparrow)
- [Bisq](https://github.com/bisq-network/bisq)
- [Bisq 2](https://github.com/bisq-network/bisq2)
- [Electrs](https://github.com/romanz/electrs)
- [LND](https://github.com/lightningnetwork/lnd)
- [Ride The Lightning](https://github.com/Ride-The-Lightning/RTL)
- [Alby Hub](https://github.com/getAlby/hub) for Nostr Wallet Connect (NWC)
connections
- [BTCPay Server](https://github.com/btcpayserver/btcpayserver)
- [Tor](https://www.torproject.org)
- [NixOS](https://github.com/NixOS/nixpkgs)
- [GNOME](https://gitlab.gnome.org/GNOME)
- [Caddy](https://github.com/caddyserver/caddy)
- [Nextcloud](https://github.com/nextcloud/server)
- [Matrix Synapse](https://github.com/element-hq/synapse)
- [Element](https://github.com/element-hq/element-web)
- [LiveKit](https://github.com/livekit/livekit) for Element audio and video
calling
- [Vaultwarden](https://github.com/dani-garcia/vaultwarden)
- [WordPress](https://github.com/WordPress/WordPress)
- Every other upstream project included in the system
Sovran Systems did not create these foundations; our work is to bring them together into one operating system that helps more people use Bitcoin privately and independently. Thank you to every developer, maintainer, reviewer, tester, documentarian, and user who keeps this ecosystem alive.
---
## License
Sovran_SystemsOS is free and open-source software licensed under the
[GNU Affero General Public License v3.0](LICENSE). You may use, study, modify,
share, and self-host it; if you distribute a modified version, you must make
its corresponding source code available under the same license.
Because Sovran_SystemsOS includes the browser-based Sovran Hub, operators who
modify the covered software and make that modified version available for users
to interact with over a network must offer those users access to its
corresponding source code, as required by the AGPL-3.0.
Sovran_SystemsOS is provided **without warranty**, as described in the full
license. Individual upstream applications, packages, artwork, fonts, and other
components may have their own licenses and copyright holders, which the
AGPL-3.0 does not replace. Read [`LICENSE`](LICENSE) and
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
---
## Contributing
We welcome contributions! The
[GitHub repository](https://github.com/naturallaw777/Sovran_SystemsOS) is the
primary location for collaboration. Please read our
[Contributing Guidelines](CONTRIBUTING.md) before submitting a pull request.
<div align="center">
---
## Privacy. Sovereignty. Bitcoin.
[Visit Sovran Systems](https://sovransystems.com) ·
[Download Sovran_SystemsOS](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.1.iso) ·
[View the License](LICENSE)
</div>
+149
View File
@@ -0,0 +1,149 @@
# Security Policy
## Supported versions
| Release | Supported |
|---|:---:|
| Latest 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 for a trusted
local network and must not be port-forwarded to the internet. It uses HTTP, so
authentication does not encrypt local network traffic.
The Hub is served on port 8937 and is not fronted by Caddy. Server + Desktop and
Bitcoin Node Only open that port in the firewall, so local devices reach the Hub
at `http://sovransystemsos.local:8937`. On Desktop Only the Hub is not published
at all: reachable only from the machine itself, on localhost, with no TCP port
open in the firewall (UDP 5353 for mDNS only).
`sovran_systemsOS.hub.directPort = true` in `custom.nix` opens port 8937 if you
want to reach a Desktop Only Hub from another device.
The Hub checks every client before showing a login page. It runs as root, so it
answers only loopback, private, VPN and link-local addresses and turns everyone
else away. The check goes by the address a connection comes from, so it is a
second lock and not a reason to forward port 8937: don't. Addresses outside the
local ranges go in `sovran_systemsOS.hub.extraLanNetworks` in `custom.nix`;
`sovran_systemsOS.hub.lanOnly = false` turns the check off.
### Public services and your home IP address
Publishing public services on Server + Desktop points a DDNS record at your
home's public IP address, which anyone can look up, and lists your service
hostnames in Certificate Transparency logs. Desktop publishes nothing. Node
publishes nothing unless *Put BTCPay Server Online* or *Lightning Wallet
Connections* is on. See
[Server + Desktop and your home IP address](README.md#server--desktop-and-your-home-ip-address)
for what this means and the alternatives.
### Bitcoin stack
Bitcoin and Lightning modules live in the standalone
[Sovran_Bitcoin](https://github.com/naturallaw777/Sovran_Bitcoin) repository,
consumed as a flake input and bridged by
`modules/sovran-bitcoin-integration.nix`. The `nix-bitcoin.*` 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 and other inputs, upstream source
archives, and any configured binary cache.
The Hub integrity check verifies Nix store contents against a build from local
`/etc/nixos`. It does not authenticate the release publisher or protect against
an attacker who already controls root.
### Access and service isolation
- Firewall enabled by default
- SSH Remote Access 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 (this makes
your home IP address public)
SSH Remote Access is the only supported administrative port that faces the
network, and it is off by default. Enabling it listens on all interfaces and
opens TCP port 22; the Hub then asks you to forward port 22, which puts SSH on
the public internet. Key authentication only. Turn it off when you are done.
Tor reduces network exposure for configured Bitcoin services. It is not a
guarantee against every IP leak, application bug, or traffic-analysis attack.
### Mullvad VPN (Desktop Only)
The Mullvad VPN client — daemon and app — is installed on the Desktop Role and
on no other role: Server + Desktop and Bitcoin Node keep the one network
configuration their services need. `modules/mullvad.nix` is a role gate around
the stock NixOS service, with nothing configured by this system on top of it.
Two behaviours are the app's own, and worth knowing before you connect:
- While the tunnel is up, Mullvad's kill switch blocks everything outside it,
the local network included, so Remote Desktop and other local devices stop
answering until you switch local network sharing on in the app.
- Name resolution follows the tunnel for as long as the client is enabled: the
NixOS service turns on systemd-resolved and points NetworkManager at it.
Neither is set or restricted here; both are visible and changeable in the app.
The firewall rules, the Hub's exposure checks, the Bitcoin stack and Tor are
untouched by this module.
### Restricted support access
Tech Support runs over SSH, and the Hub refuses to start a session until SSH
Remote Access is enabled (see above). 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 but leaves SSH
enabled.
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.
+36
View File
@@ -0,0 +1,36 @@
# Third-Party Notices
## nix-bitcoin
The Bitcoin and Lightning modules were originally adapted from
[fort-nix/nix-bitcoin](https://github.com/fort-nix/nix-bitcoin), commit
[`360e30fee5ba32f9fecc89bc35628195d9d2dbbe`](https://github.com/fort-nix/nix-bitcoin/commit/360e30fee5ba32f9fecc89bc35628195d9d2dbbe).
They now live in the standalone
[Sovran_Bitcoin](https://github.com/naturallaw777/Sovran_Bitcoin) repository
and are consumed by Sovran_SystemsOS as a flake input. 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
@@ -0,0 +1 @@
1.2.1
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#1E3A5F"/><text x="32" y="28" font-family="sans-serif" font-size="12" font-weight="bold" fill="#F7931A" text-anchor="middle">BIP</text><text x="32" y="46" font-family="sans-serif" font-size="18" font-weight="bold" fill="white" text-anchor="middle">110</text></svg>

Before

Width:  |  Height:  |  Size: 362 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="83 78 333 357">
<g>
<path
d="m274.23,434.09 l4.37,-0.26 4.1,-0.42 4.1,-0.42 4.23,-0.64 4.23,-0.64 2.88,-0.53 2.88,-0.53 4.1,-0.95 4.1,-0.95 4.26,-1.18 4.26,-1.18 4.32,-1.45 4.32,-1.45 3.36,-1.29 3.36,-1.29 4.1,-1.77 4.1,-1.77 6.09,-3.11 6.09,-3.11 3.7,-2.2 3.7,-2.2 4.23,-2.82 4.23,-2.82 2.65,-1.95 2.65,-1.95 3.12,-2.48 3.12,-2.48 3.23,-2.82 3.23,-2.82 3.54,-3.45 3.54,-3.45 3.17,-3.44 3.17,-3.44 2.55,-3.04 2.55,-3.04 2.54,-3.39 2.54,-3.39 1.46,-2.83 1.46,-2.83 0.66,-1.85 0.66,-1.85 0.56,-2.63 0.56,-2.63 -0,-2.26 -0,-2.26 -0.63,-2.51 -0.63,-2.51 -0.67,-1.5 -0.67,-1.5 -1.13,-1.67 -1.13,-1.67 -1.37,-1.37 -1.37,-1.37 -2.01,-1.31 -2.01,-1.31 -2.22,-0.8 -2.22,-0.8 -1.69,-0.31 -1.69,-0.31 -2.65,0 -2.65,0 -2.08,0.44 -2.08,0.44 -1.89,0.68 -1.89,0.68 -1.48,0.74 -1.48,0.74 -1.83,1.25 -1.83,1.25 -1.85,1.59 -1.85,1.59 -5.56,4.14 -5.56,4.14 -4.1,2.92 -4.1,2.92 -5.42,3.71 -5.42,3.71 -6.35,4.09 -6.35,4.09 -5.69,3.45 -5.69,3.45 -5.56,3.31 -5.56,3.31 -2.91,1.46 -2.91,1.46 -2.91,1.19 -2.91,1.19 -3.04,0.98 -3.04,0.98 -3.18,0.75 -3.18,0.75 -2.51,0.41 -2.51,0.41 -2.65,0.31 -2.65,0.31 -4.89,-0 -4.89,-0 -3.57,-0.44 -3.57,-0.44 -3.42,-0.69 -3.42,-0.69 -3.54,-1.03 -3.54,-1.03 -3.1,-1.22 -3.1,-1.22 -2.66,-1.25 -2.66,-1.25 -2.9,-1.66 -2.9,-1.66 -7.28,-4.84 -7.28,-4.84 -5.03,-3.67 -5.03,-3.67 -4.01,-3.12 -4.01,-3.12 -4.11,-3.41 -4.11,-3.41 -3.41,-2.99 -3.41,-2.99 -2.23,-1.9 -2.23,-1.9 -3.46,-3.53 -3.46,-3.53 -2.19,-2.76 -2.19,-2.76 -2.05,-3.06 -2.05,-3.06 -1.07,-1.85 -1.07,-1.85 -1.61,-3.22 -1.61,-3.22 -1.3,-3.26 -1.3,-3.26 -0.84,-2.65 -0.84,-2.65 -0.76,-3.17 -0.76,-3.17 -0.42,-2.38 -0.42,-2.38 -0.39,-3.44 -0.39,-3.44 -0,-5.95 -0,-5.95 0.4,-5.29 0.4,-5.29 0.41,-3.44 0.41,-3.44 0.63,-3.97 0.63,-3.97 0.69,-3.57 0.69,-3.57 1.06,-4.23 1.06,-4.23 1.72,-6.61 1.72,-6.61 1.33,-5.03 1.33,-5.03 1.98,-7.14 1.98,-7.14 2.25,-7.54 2.25,-7.54 1.46,-4.63 1.46,-4.63 1.21,-3.7 1.21,-3.7 1.22,-2.38 1.22,-2.38 1.86,-1.88 1.86,-1.88 1.6,-0.81 1.6,-0.81 1.49,-0.43 1.49,-0.43 2.73,0 2.73,0 2.03,0.53 2.03,0.53 1.94,0.81 1.94,0.81 1.98,1.13 1.98,1.13 3.97,2.93 3.97,2.93 7.54,5.28 7.54,5.28 7.94,5.45 7.94,5.45 2.14,1.32 2.14,1.32 2.62,1.31 2.62,1.31 2.41,0.95 2.41,0.95 3.01,0.8 3.01,0.8 2.65,0.4 2.65,0.4 4,0.01 4,0.01 2.61,-0.42 2.61,-0.42 2.49,-0.67 2.49,-0.67 2.01,-0.79 2.01,-0.79 2.76,-1.45 2.76,-1.45 2.27,-1.56 2.27,-1.56 2.12,-1.7 2.12,-1.7 3.1,-3.14 3.1,-3.14 1.87,-2.25 1.87,-2.25 1.6,-2.37 1.6,-2.37 0.87,-1.74 0.87,-1.74 0.82,-2.33 0.82,-2.33 0.55,-2.7 0.55,-2.7 0.15,-3.17 0.15,-3.17 -0.32,-2.78 -0.32,-2.78 -0.4,-1.85 -0.4,-1.85 -0.67,-2.25 -0.67,-2.25 -1.18,-2.76 -1.18,-2.76 -1.21,-2.14 -1.21,-2.14 -1.26,-1.87 -1.26,-1.87 -1.66,-2.1 -1.66,-2.1 -2.8,-2.79 -2.8,-2.79 -2.51,-2.01 -2.51,-2.01 -2.91,-1.89 -2.91,-1.89 -4.1,-2.07 -4.1,-2.07 -3.17,-1.2 -3.17,-1.2 -2.8,-0.81 -2.8,-0.81 -3.02,-0.65 -3.02,-0.65 -3.7,-0.54 -3.7,-0.54 -4.76,-0.3 -4.76,-0.3 -3.44,0.17 -3.44,0.17 -3.17,0.27 -3.17,0.27 -4.37,0.66 -4.37,0.66 -4.17,0.92 -4.17,0.92 -3.65,1.03 -3.65,1.03 -2.89,0.97 -2.89,0.97 -3.7,1.39 -3.7,1.39 -4.1,1.8 -4.1,1.8 -4.57,2.33 -4.57,2.33 -3.1,1.78 -3.1,1.78 -3.57,2.23 -3.57,2.23 -2.91,2 -2.91,2 -4.07,3.03 -4.07,3.03 -3.21,2.66 -3.21,2.66 -3.44,3.06 -3.44,3.06 -4.37,4.37 -4.37,4.37 -3.9,4.67 -3.9,4.67 -3.25,4.32 -3.25,4.32 -2.93,4.37 -2.93,4.37 -2.61,4.37 -2.61,4.37 -3.08,6.09 -3.08,6.09 -1.76,4.16 -1.76,4.16 -1.73,4.76 -1.73,4.76 -1.43,5 -1.43,5 -1.08,4.76 -1.08,4.76 -0.78,4.87 -0.78,4.87 -0.4,3.57 -0.4,3.57 -0.3,3.7 -0.3,3.7 -0.08,8.6 -0.08,8.6 0.23,3.7 0.23,3.7 0.41,4.1 0.41,4.1 0.53,3.7 0.53,3.7 0.81,4.23 0.81,4.23 0.9,3.76 0.9,3.76 1.09,3.78 1.09,3.78 1.47,4.37 1.47,4.37 1.92,4.76 1.92,4.76 1.9,4.1 1.9,4.1 1.75,3.31 1.75,3.31 2.24,3.84 2.24,3.84 2.43,3.7 2.43,3.7 2.5,3.44 2.5,3.44 2.59,3.25 2.59,3.25 2.81,3.19 2.81,3.19 3.47,3.59 3.47,3.59 4.1,3.71 4.1,3.71 2.91,2.38 2.91,2.38 3.7,2.77 3.7,2.77 3.84,2.56 3.84,2.56 3.14,1.92 3.14,1.92 4.63,2.51 4.63,2.51 4.14,1.94 4.14,1.94 3.57,1.5 3.57,1.5 4.63,1.68 4.63,1.68 3.84,1.19 3.84,1.19 3.57,0.94 3.57,0.94 4.28,0.94 4.28,0.94 4.85,0.78 4.85,0.78 3.44,0.4 3.44,0.4 3.84,0.28 3.84,0.28 1.32,0.08 1.32,0.08 6.61,0.05 6.61,0.05zM266.31,350.59 L267.75,350.16 273.3,344.7 278.86,339.24 282.47,335.8 286.08,332.36 286.4,330.9 286.72,329.44 286.43,328.36 286.14,327.28 285.37,326.22 284.6,325.15 283.36,324.55 282.12,323.95 280.02,323.74 277.91,323.53 269.66,323.35 261.4,323.17 253.48,323.47 245.56,323.78 244.35,324.31 243.14,324.84 242.25,325.69 241.36,326.54 240.87,327.6 240.37,328.65 240.37,329.58 240.37,330.51 240.65,331.53 240.94,332.56 242.57,334.19 244.2,335.82 251.54,342.68 258.88,349.54 259.61,349.93 260.34,350.32 261.27,350.65 262.19,350.99 263.53,351 264.87,351.01zM215.35,299.54 L216.9,299.14 218.4,298.34 219.9,297.54 221.17,296.3 222.44,295.05 223.32,293.73 224.21,292.41 224.85,290.82 225.5,289.23 225.92,287.19 226.34,285.15v-2.76,-2.76l-0.4,-2.11 -0.4,-2.11 -0.65,-1.93 -0.65,-1.93 -1.08,-1.99 -1.08,-1.99 -1.32,-1.66 -1.32,-1.66 -1.64,-1.29 -1.64,-1.29 -0.95,-0.57 -0.95,-0.57 -1.83,-0.69 -1.83,-0.69 -1.98,-0.36 -1.98,-0.36 -1.99,-0Line truncated
fill="#25b135"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#F7931A"/><circle cx="32" cy="32" r="22" fill="none" stroke="white" stroke-width="2"/><text x="32" y="44" font-family="sans-serif" font-size="30" font-weight="bold" fill="white" text-anchor="middle">₿</text></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="1024" height="1024" viewBox="-34 -34 580 580"><filter id="b"><feColorMatrix in="SourceAlpha" result="matrixOut" type="saturate" values=".1"/><feGaussianBlur in="matrixOut" result="blur-out" stdDeviation="6"/><feColorMatrix in="blur-out" result="color-out" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0"/><feBlend in="SourceGraphic" in2="color-out"/></filter><circle cx="255" cy="255" r="200" fill="#fff"/><radialGradient id="a" cx="277.49" cy="196.441" r="34.397" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f9aa4b"/><stop offset="1" style="stop-color:#f7931a"/></radialGradient><path fill="url(#a)" d="m254.647 174.6-13.983 56.08c15.855 3.951 64.735 20.071 72.656-11.656 8.248-33.096-42.817-40.472-58.673-44.424" filter="url(#b)"/><radialGradient id="c" cx="261.915" cy="284.567" r="39.838" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f9aa4b"/><stop offset="1" style="stop-color:#f7931a"/></radialGradient><path fill="url(#c)" d="m233.608 258.984-15.425 61.832c19.04 4.729 77.769 23.584 86.448-11.296 9.072-36.376-51.984-45.784-71.023-50.536" filter="url(#b)"/><radialGradient id="d" cx="256.028" cy="256.003" r="255.988" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f9aa4b"/><stop offset="1" style="stop-color:#f7931a"/></radialGradient><path fill="url(#d)" d="M317.871 7.656c-137.12-34.192-276.024 49.28-310.2 186.44-34.208 137.136 49.256 276.048 186.36 310.24 137.16 34.199 276.063-49.265 310.256-186.408 34.192-137.152-49.264-276.08-186.416-310.272m50.936 211.872c-3.688 24.936-17.512 37.008-35.864 41.24 25.2 13.12 38.024 33.239 25.809 68.12-15.16 43.319-51.176 46.976-99.072 37.912l-11.624 46.584-28.088-7 11.472-45.96a1076 1076 0 0 1-22.384-5.809l-11.512 46.177-28.056-7 11.624-46.673c-6.561-1.68-13.225-3.464-20.024-5.168l-36.552-9.111 13.943-32.152s20.696 5.504 20.416 5.096c7.952 1.969 11.48-3.216 12.872-6.672l18.368-73.64.048-.2 13.104-52.568c.344-5.968-1.712-13.496-13.088-16.336.439-.296-20.4-5.072-20.4-5.072l7.472-30 38.736 9.673-.032.144c5.824 1.448 11.824 2.824 17.937 4.216L245.423 89.2l28.072 7-11.28 45.224c7.536 1.721 15.12 3.456 22.504 5.297l11.2-44.929 28.088 7-11.504 46.145c35.464 12.215 61.401 30.527 56.304 64.591" filter="url(#b)"/></svg>

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 2.3 KiB

+177 -1
View File
@@ -1 +1,177 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#F7931A"/><text x="32" y="44" font-family="sans-serif" font-size="36" font-weight="bold" fill="white" text-anchor="middle">₿</text></svg>
<svg width="512" height="512" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="512" height="512" image-rendering="optimizeQuality" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAMAAADDpiTIAAAAOVBMVEVHcEzSeg/Seg/Seg/Seg/S
eg/Seg8XFxf3kxrSeg/6lBrvjhnkhxdmQxiXXhjagBNBLhcrIhezbRlPwFBIAAAAB3RSTlMA3Kt2
JQxMlGKhdwAAIABJREFUeNrsXduW66gO3ImTtI0Nvvz/x46T9HQSBwlxMQgM8zDrrN070ydVlEpC
iH//DrGu18vlcrs1TXNe1+m+ulP3t9b/sa77H53XH7nd1h++Xv/VVQTud9ifoHfk9WTDkwqVCXlC
vyJ/B94Gdj0VHkRYeVC/05yg90dew4NVDyoNmGN/3/Vhof+mQWUBV+zXbd9FWJUFLMGPgv27FlQS
HBX8NymoJDgs+JUEydFPD/47CSoHoq516zMB/+UJqhBEE342W/9LCCoHDot+5UBFv3Jg77jPH/0X
BypeR0X/fw7cKgdCZnxZof/LgZobBtr8oQL/uK7hsfrf1f7987sef3r/uRoKytn8T9gfeD+XEM9/
tfD6JYM/E6oM+MHvtfl/gW+91i8Rqgwk0f6TB/S+yH/zwJkGp0oBF+132/zhsQ/BgjUpqJHAEn5H
7NvdlxsLKgXsQr8D+H0E8N+0wJ4E1QzsFPojg+9BgmoGwsOfBvx3ElQKJIP/vvXb5MtSCCoFAsGf
duu/L2EpBJUC/vCz2PpfQlApEAd+huhbc6BSYJv4nXJH35YDp6bWBezLPrzRt+TAqZaGnosK/5gD
/FYUON8q+v8utJp/NuhbceB0ProVIAb/zOB/LCIFDu0GacH/jr5oM1w0GTjwKdHlXOjmt6XApao/
CH/G6P9yYKwpobP3zx/+pwzUOODk/Yci4CdGgmPlAwT1H8uBn0qB44iA2fwVBj+RAgcRAfP2LxB+
GgUOYQaN279Q+GkUKF4ErrfTYayfU0ZQuAgYt3/Z8FMoUHQ6YNr+Y/HwU0pDp1LPCC+NKfi3B1km
CpQZBgylv+PATzgqLLBRwJT8DX17qGWwAsV5QYP7Gw8GPyEOlOUFcfk/lvpT40BBYcAg/0PfHnTh
caCYMIC7//Gw8JvjQBntYmj4P6r6E+NACUYALf4ce/sTRCD7ohAa/uv2N4tA5kbgeq7b31cEcg4D
aPiv2/9dBIpkABb+6/b/WAITgVyNAIZ/3f42IpAlA65NBttfCEZXjjARyM8KYvaPTelPDMvIiQFD
OQzA7B8X+RdtJ38m1TOiAJIQ5mUFEfz5yP+g5p91yTGPMJATAy4n9vIv2mX6ea5pYSQCSBjIxwoi
9n9gtv2fSw6cwkD2DIDx55P8r9H/bc1Tl0VdMA8G8Mdf9O/bn2EYyJoB8OkPH/mXP99LZREGTk2+
5R82+Hc6/JllA0OmBQEY/5Gf+9+uqcsiH+TMgAzw/w7/70agrQzYB382xd9e/SBrZsQAAVcEuDIA
wZ+1/XtngMrifJAnA0D8+XR+mfB/MEBUBgTGn03xfzTif2cAo4IA3CbCjwGF4M8tCmTDgGLwv5eE
KgMc6n/sq/8DFf9sNKDiH9L/f2aDIgMGMKoKg5OfRj5uygL/e0WI1XRyKBlgczIE4c+o9Vf9WK1p
ZKUBA++zQf74C6T+q1+sWkRADeDBgAzwX2zxXxnA6+IKxAAGfYIX/viPkzX+zFIBxgyA+n8Z4W9n
AHmmAiADUvcKX9njjxtACYvDNOTBgGvFHxeAbsYiPRIemNkAlgxo+OOPVYBkL0Q35RIEIAY07BIA
Vld/lSHXE2CLGLNqAMiAdCXBHPBHAsBvFyiSJEpe+EMVoVTlACABHFl9ZXAAePWAqmyCQDsySgaB
BJAV/i28u1/gInkis4IgyIAUySCQALAa/SKGiVLoQX5McSMAcDaYIBVochj9o2g5HmwD2PlAqEus
4WEAeeGP1IA3VR6hsmgPQjQgthG8ZIA/cglg3lwCgm3A3IlMGHBhYAB54Y8IgKLXCyU7CQDKATGN
INACymz2GywAOnOvcrgx+L9pTd0mmgX+sADoZB3OBPi5AEgDmrQGkN3sRztjB2YC7BIBkAGxjKDe
AHCb/QpvaQBR0AcqhgTQG8FINiAL/LHMTtgdG0xDPgy4JjMA7PAHNzSIJ/Q3+J0IPH7bVJ3imRgA
MKTPoKKDqaBkOdm8T2MD9EcADId/S/tOL1ACONpAwAjuHQT0FYCR3ZcD54BIVgdKgGJJAP3J4M65
4C2Txx/EqICF7WZIAnjGAMAI7hoE9EcAHL8eAS4H48DwQACxAZdqAILXDpjGgOg24Fb66y9Q8UBy
feFoiNokqg0ARb3+BNlAlrUg0AbsFQS0AaCw178AG8izFgQyYKcg0Bzg+S8oBnA1AYANaKIFgNKe
fxPjnJcJAGzApQaAwDFgYCsBsYLAbd8KgEi8/n4RlZkJAKoBt9wCwJB29aY84M0E9H3fmotKpQWB
865HAGKY0q5XjAdqQfL1qyq5LqWWbhyeTGB5KBC4Q3DvADDYT/EJul7FXsAEvBjy9wPzPK1EWMY+
+Qu0WhsQNAhou8BCZgCpCfCSeCARfCsFff7AnQZq/S6ScmDY2wc2e58Bu83xCbik4UDo1RMgFs2f
TnJVgoQcGPctBugcYNgMEH3KJcb62+FAJeDtMjFwZLgKQToO6IJAOB94Pe9fAkIec4prAqATwRcB
RpCqqw4MiR6c2DUI3OI0AXVJGbAYXODLJAzY7zmpLo0M7OgDdQ5wlxLgKDm4wNZAAJNfmeWSggL9
fhLQRDsDSMkAaTgPkhaGdY0E8Skw7OUDtQ5wp6OYIR0D/hJ9ncv/PA4i/JLTEv/wYC8feI7YBZiQ
Aa80QO/yXwQQpNnzMvpz1NogsM9FkP0OgdMx4EUA/WnA1AsrAqwU6CJnBMMeF0Wu58iHwGOiXOBV
6QEIMNgSYLWDcW+U6IoB3j7wFrsLRCTKBl+FgNFEgMXi7ZmocWAInwpqU8Cd/28sM28CtIvFp8YV
gfAS0KS4B6KKUYDYItCHTgWvpxRtgElOhvZRgEcJKWJGOAROBZskbYBJbMCfCRRdUAV4pAPRGKDz
gU3YGlCMPmCRIAi8AA6WBbyFgUwloEl0E1wkCAKmQpAPAWK+SR5SAnQCEKcPXMTPBF6VPr3C2xeC
Po1ALAb0ASUglQAk8YHGw6DeL0BFmzwfTgLSCUAr4kuAEngWancYlJAB4SSgSXkTLLoEmBpCpPev
FosBQyAJSCgACRKBVxYINPwowgMjPBgQSgKSCoDbe68+SUCP14GoLWEGoynykQCNAES9CiqM+2wO
eDPo9YIM5D4oTaFMcgFNNchBAprEd8HN9Ta1z+VAhVeK/VJUlY0LSC0AhBgQcmiDMFm894shcwiv
yV0CUgsAwWzvMrUDivBvZ0H9ouTkSoI470/4S4CmDyD6MAiVhACLeUSIEOs3rKSbF4wyb0ojAZZ9
Abf002CMUrsPAWhDgoQQ/bA4cSDKtJnBszXoykAAjG57DwKASb7mP3YXgkVax4Iow0Z8JYCBAJjz
7V0I0NkNixV9p2xlIMobNJ4SwEAAzDX3XUKAso/coy0FYgQBnQR45YAJ5sGZqsE7EEC4zAsXKwVm
dkFg8MkEGxYTwRMQoHV9MaCz8gIxMoHeIxPkIQDGWiAKSg8stwhg3LOitxpvoHhLwI3HkwCmPBD9
Fh+jvDQLNWCg76Rs2dFCBGL4wN7ZBupywDYFATp3AoDhA2UN+LcoDwhbTbmJcSbgnAlqroMmmQhr
KgSgBFhcXgADq88k1yYEPQxEkADNC8PEq6INk0ehTMMDUQJAYo69/gI/NUeEy+JCQxoJaDKygOZK
EG4Cpb2aw3+nJ4uWZJQIONrAhstMcC8CgOEcFl/YdNJNO50BEWoBvZMEaCxgokcB/AgAOkhlLQBW
D0dSo0CMcuDgYgO5WEBz9yW+L+G3gwE4YQGwworaLxLjKbrexQZysYCtsSUEJwDyergt3azUWlD7
mWMUgxxiwIWNAPgSAIwBegmA646Wfo3aNp7IBl6tq4DJnoUxDYwybCEYB2VnOGwzNmJHe4QYIDQ2
8GZ9EJzuXSgvBUC2tParV+GAIgaBNAcC53wigCcBWnhPa1wdXHaWDtI1cckDettSACML6E0AZFNv
bR0St10O72kSMI9JbOAtjyJACALAu/rLgKmg+5QmAYn6QtBSwHcESPkyoC8BkL8vqTV8N5RoEqCS
dIeiMaBh9TSoJwGwIQMbXGXoQN3NTEyARgIaqwjQ50sA9AM+iwEgU1xlmlQLiHNHxCYGaCJAmzMB
sIl+H1e13doHnAxobBfY2sQAXhHAnwBYUW7+rO8Ab4W5FmtIJwJzonuCTSYRIIACYHOHp0911/Zz
SddfnTZEIM5VYXoMYBYBAhAArctvzgQ0DHBv3KIlgirKwBB6DGAWAUIoACrFnzN7xDcDfPq2JBsC
kGMAtwgQggD4Z2wmtmzJ4jXUiUIAGedrpJ4HcIsAQQiA95ZvtvhnV69XpY5UCpJxNhg1BtyYRYAg
BMATsnlBKoJejbucCDDQzgOuDbMIEIgAqB3b7vK3nk6/Mg0nAvQ0E3DlFgECEQBPyTfJYPvXhOJ5
VMOJAJoYcM0hAgQigOFjNlOb/tqQPG9usCLAQOoNZRcBQhHA0KK1ZcDzip/vWFdWBCDFgO8kcCyE
ACYwtgy4JwPe09yE5ESAkVAMZJcEhiOA6YO2aItOehdpe04EICWC/CxAOAKY+nS3TrD1BobWGy4j
fZGCkgjyswDhCGA8nAv+qhOnswCaCdBYgLYcArS9MvXnhX3oWZBagqIRoDWbgG8LMJREAOOr5IHf
9aK9LbhEI8BgNAE3fhEgJAEIsxvCPvBJ6giKR4DeaAIadklgWAIQenRCvuhC8oDzmPBF0Ya/BQhL
AMKmDDjKndQQNEV8VNhkAjhagMAEIGRmUygrSLsXICOKrMkEcLQAoRXAaATDWUHaDXEpIn6XBhPA
0QKEJgDp3nYQI0BLAiMmAUYTwNICBCcAaYBPgLfeRU+7HNpFJIDBBLC0ADsQoCW060+LbxggToiY
+pgEwE3AhaMF2IMAFAbM3mGAOCAi7peJ9gQ0hyEAaaCv7CIIQFQLYDoOYGkBdiEALT5PPtkAzQFE
rQLoTQDmAYdSCUBkwCzdLwURBwXKyN/mgLhAnh5wHwJQk3TXohCh2hD7IMDsAlmWgQIfBlmVBJ9F
ITeNJs6JlH1kAmCloIalBQhJgOU956ZuUukgAuSXhZWI/W0ipaDSCSC6aXJhgL0IkF8MiG0BdS6w
Ye4Bw3UF3/F+v+otqAywFgHq58YXAMwFMvWAwQjw/Bz5wQDiQF87ESDzKoUAIC7wmwB9UQT4Dcsf
BT4yA2xEgPyZcwIBEN8u8AIlAWNJBHiF5Q0D1E9YEaBzKn4KoHeBN94eMAwB3mH5LPLTn3mj1QTo
+EevARhcIFMPuMN8gM/ZcHQGEESAHv9jHwMhLpB3EhCGAB8nM9uDPvpTjyYRoL8WlcYBYmkAVw+4
y4ygDQMWMgNwEbB4MTByIwhWC/xNA0omwHdhbjMazOLNZ1gERGvxMbNK9n0CBLgx9YAh5gRqAvOG
ATZbFxABq1eDf1S67QWkAU3BBNClepuTXovgrb9EamP/EmWAeBrQMPWAAWYF6+/ofg4AtcFv1m3f
zgr/IR3+3y6wYZ0EBCCAIl0BIpeEpqX3k/8Yr4Zb54HlEgC+pP/FABKImi4hm+wvOf5AHsg2CfAn
gCJeAxSkZuHvPkEr9x/28mGgNKBoAmBTOra5uDkdlN1XEigGK/n/kWn3P5QHss0CvRVgoRdjhcCt
nCYDFHbujwH+QB5YLAF6q6eg0GRAUwOydH+J/T9GgKZUAlg8G4gjqrkqZL39A4+gCVkIYFsG8CWA
7WOgAjgb0lwWtN7+oYdQhSwEnAslgMNz4For+H1d2Hr7J7f/WCHgWigB4DldyGnstxXUJX+9muy2
P4fwDxYCvgnQF0EA8C/j7TgbK/jt/hy2P4vwr88DVwLwLQN4EeA/9q5FOW4Vhm6SZmODwY///9g6
j816bQkQIJDZ0E7nztxu0okOR0cHIfAruu7JPI/Z/Vi5kbc/ZCCIMgIaBQBqAvjbMe6u4GHr+uyC
fBfMKgJgbAMAJr4f70cKHpV7xPZfOkHxPzYGrwCQ6wOlAAC1gQOGM6rvE57DCPHu5NsfMgJerq0C
AHOBgvqx1GDmQ/Wn9HLi7I9bgW9tAsAmNWSuwT7s3RUURPEvbfvDVqBcIzBJA5jEsRxqd1pIPPhd
a3952x+2ApsEACYBYq/kUA9+M8yaKwaA1yYBgAxrju3IJJ/8DCLDD3rBbQIA6fCJHSw2N8D+zwUA
m+9OjiKqP6ns/1wAMBEuMEb/UxvsD1uBrxe5TnACAJBP0jMAkf4lsz/iBTcJAKwIIGcAIv2LZv/n
AsCc5Vo2zfsVzv7PBADkyR6iBKCZP+LZ/6kYAD4JID4zRkr/EPsrpf4AUAcASzoASHd+IfZXalzE
5QQAAH2TAEj2gUmX/iD2/+odmBZhaeE4Ku7Si+0HSdAAS7IN1E9p7N8t5lsZjJIQoAIA0AQDgEbg
TCDkcPk3W+DOcDf+lo+ySsPnYAAFAyC4CqTIP5D9h23nmCwSeGYGCAUAIf5Qx/exc0gQCbTEADMX
A4RP/F33NnRraIb+4h8DVK8CwgBAmPgNib8Bu18ooxx46iogCADB8Yd2tatvVIhP/CwMMMd1hIeX
/9DEAIj9H+TiHwOUYgDQCg7wAULjD25/X9+giBEBT8IA4GGQ3wkMjT84McBvHQi4JQwwwEt7VQDW
D+CdKhUWf2D7f14mmoNcg7oIUM9yFgADwGSJ/5QyMaB2NRDAAE0AAP6k52J4WPwTB4ZUFgLP0hCC
HAaMKrX+g8bFEbvGqwqBJwfAohLjDzE49cpw1XmBTwMAuCPEdMSk4Q8deWJA3ZFRAABemwTASOsK
DYo/lL5pbwXUR4B+losh8EfRHBAyMB4W8ORroz9fqxICWroZNEdcD8dyQEAQsV1Lnhl5Q1MVH+BZ
roZhUwLhef0hT747tLta6DLgcwDRHwMwAgC5GQB/xt//56ze6aPD6mUBAABtTgjBVB0gA/GJgtvt
6p5KOpwFAcPTjIjpbKBwCAieP1RRQqBGLRACgCaGRKF5/UAB/gIw5ASHPEMIVySsCxgSdW0UANig
wN2nlHfrhlq3PR0B5V1hYEzc9aVJAOBPxj0+GrjM2YI00oVA8VckgUGRpx0VO3uO97FRkQ9egFcA
Ug7vIqRg4XeEoVGxrQ6LRt3drR2o+pxdXEobuh1QlAKeaVo4/mrggw50SoDZdow3yisIQQgArT4Y
4ajvbaALTHfrIhBgdE0AgC+GNPJmkMJPeLZJAP9bMW6t6qgImG1BCgDfjm0VAI4Ev7VgUATEuvVU
BJRMAuDr0c0+G+d4OG5b2yEIiD6tUVQEFEwCQ9C7gc08HevQ+FZ7EnfKaR35TcFiFAA+HHnWp2ND
kqdx6HsnAlLir0LaS6IGF3AYgS0/Hu2igIdtd0DAnGTQkP2AYjqwPxqBDT8f3zlbvR7O4nYImBMN
OurxcCkKgJ+PbxkArrP+3WnsFgHJlzcCegxqUMARAGsVKNgISNcAStngJm/Hc4ERCOhngRQA2gCn
BUDQtnGm491B3+1EP88R3TLLowAYAHKNgAwAwA8Fjwj4ae7MdEhPE4JlOgNAG0CwEZAFAJ0N/rmr
z56OXMYcUQYU8QJGGADXcwIg0ER3JoF5R/f9lK9RT5GSgCnx8wRtAMFlQB4GcG/FHQLGvtg/v/yJ
AFwFNg8A91bcj23JGAZaJWDrAUBsGZAJAG4ZwKm/KJZwgSMhpAiQeyCcRQN4vxDf6CaSDiyQA8DD
YMllQDYG8FizfBxgRdUBSBEAlQG6LQD4un/ZEEChAPY6ACsCLserAX1zAFDuaxtsU1soz45w3xHA
NCBUBgwnAACtmc5TlfMgQFG8gJlZBBxnA/wCQGoZkJEBvAjguamJXlCr0RiEFgFAGTC2BwDf8QzT
XV0rxwkYsSIAKgN0gwDw9OyzIICSA5hVoEaLAKAM6E8AAHJDvfL063JMbKBYAcxWkAb7wUSrwMwM
0Pn6dTkQQDgQYO4KwTWgWBWYHwDlEaCMlDLAoQGlqkAGAHgQkF+Jq3AVOPOOjHFoQKkqMLMG+AlI
WQ4gqEDeOvA4KP5tAwCZKpCDAXzVYO4oUM6EywJgowFXESBSBfIAQC9FOWCcZRgBLg0IqcCxJQAo
kiOUFQFqEAKA0aUBhYqAbBpg2J2zlPQEhQDAZQOJFQG5WsIGY6shQAoDHDXg9QEAIq2gXACwh7PW
cmeDSogGcEsAoSIgZ1fwvvdzKdQhIgUAHglweZcoAvJogB8vjoiAbN0ZvQgA+CSATBGQhQF+C/Ed
r3sQkGuCI+U4kBMAHgkgUwRkAcD9izxqO+VBQKZiEHm5ovS8MJ8EkCkCcgBguwNJCMjkzBPOAjid
QJ8EgERA/RyQQwM8fI1Hj8fTsZfndJZyGsgHAPdBACYChgYYYBfj3VtNbgRkadAg9AMwngYOXgkA
iYCxhRRg3NO/nNcGM+xIUlcoWz+AGr0SABQB+vQAOJ7F7RHgOh3OcFuLchjI1xGk/RIAOg7oZQMg
SAMY7wRIFwLSkwBFA05sN0NCJIBEEZA+JxCy4R4R4J7qmJ4ESFeDyhWBVwAA7+JEQHoKCJkC7Pou
qUmAIgEY28JHcDyc/ByQPCsY+fHvOMB1dTh1YiRlTIytmwEE5oBUDYC6cDt97+jdTy3NCBKAzwYI
KQJFFoKpDIB+fv+zduzTpMRMygB8VWBIEQg3h1fOAakPRqAnsfuoOrR6EgUQDgIYq8BjBniF4w+I
gOHUALChHT+uUXIpFECaE8ZWBIRmAHk5IFEDoB+3AFlM+SlA0SYF2uoZADAD6+aAxEejMBMOqu0c
sTJlCIBNAwIZ4P1yjhzA9G4gWNrhflC0F0AjADYJMAQWgQJzQBoAsE/DAXUkgUhuJr4bwiYBwjOA
uByQpAFU0NvB21owd2hoLwdZARlAWg5geT0cE3X4ho2TgcQnI9iaAUZCBgBzgD5rCjDE/Yyn7JjN
SX02iCsDaEoGAHPAcE4AoCacQ2ybfMER83DYQMoA0HlAPRno2URuDYDtZ4fYRikgxqQlPh1YMANc
XfEHckA9GaiSGMCSj/fw70cv0Rchz8ceJaAzA4DnAfVygJ7iAYAE07nTUOvecMefrSN8CD4HcOQA
XS0FxAMA+6yz7Qr9hrSLYvQn5NlcIE3NAFBfUDUK8ADArQGQIsyjtWwGEaA0Of7ZLqIdfoJ9WC+Q
2woYz8gANkZrYTKQ4tNT6z/WVgCaCYDNjq8lA303q50AMDFUu0Ju3q7pZ5lgAKhumcjxZ+sGBCTg
1QsAOVaAz0tzAAAT9N6fdL/8sN44Dt9LD3pdwdufTv+MNSDVBEBzQB0K8JymOTUAkj28O1mp7z8f
VuhTYkrHbH9GAojJAKAVMFQCQLR5hpAH6zBWpXszx8S/IAH4TADsnnAlGWjjAWAL371R6+6PCz9b
CRAnAREZWIcCTG4AGK7oqyE6/HzvxR0JoL8GAQCQgVUowOMEOzWAKTaCY42+7u30Eb2WYr2AQRIQ
loE1KkFvUz0eTgWbyNmT7adAHHobvflZO4F0LAGIcQN9HRWO/QxjZ85ouKrv4C9mSok+21NF8RJQ
DgV4b1U4GADGTobZb7fCUA/jYlODz3ofDCCAt9D4y6gEtfUO1drX6x4HwRwCSVmf/ySth3XXr6Ff
Y58c/MJXwgkEAMrA0hTg76mxw25pTxGwoYw1ivtlv39hy6xRX+P+6RV/ZFslh4J4D4Ldh8LlKcA7
Y/XXqr+tXzpFAHCn2xVcMxbJ+et3kVV0LNTLlQAAARRAuli5D7Bx51vSrV3G+DPOhYuvAcVQAPlU
dWP0Gvf/J7Zscy3L98Mb4mtAIRRAeXHxkFBh+XD//2qRAACr5RIAWAmWpQB6Brg7/bAP9AsApayA
+HM+Fz+k1IBoJViUArSJ+Im6U/z9Jy4BAGYoORyeUgPKoICILH0v84fZU3PbpuOfgwCqU0BEiH7L
fKSXzCQIzFPFPwsB1KWACAm49Xl6DwC0aVf/5SIAkALKnQjEcLT1iHyrhABg5o0/cAoQQwBVKSCG
AO4+kHAAzEtX9pnwOAKoSgFRAdoAwMMQuqYROPXM9VMuAgApoExrUJxRdzdWPQDoKgJgNqNiZs9c
BFCNAlQcQ8++syAJDMCc/uEEEEsA1SjAfiQBQHlPg6tpANN3qjsNAcAUwK4Do09qfBrA1AbAZAfu
8OclAJgCmJNAzOXKQA1Q1weYC2x/yANKIIAaFKDig+NlgKpO4LRoxZ89MxMASAHMOjDepvcZQTXP
AkqwP1wCJhEA2BfAqwMTTurDAVD6NHCyoyoRf0gBUvsAAlqDGJNAUqeG9TT8bBtCCoe/KxJ+KAEQ
G4ECKUCzxT+lQDed+zRwA4ByLWHzSv6Fwg8qwFQCAK+KclFAWvy9DSGbnsFSTaGzWQZVKPywAkwl
AHBwHJcOTIv/veMHuxq4lDUC1s3f63LhBxXga3r8wVKQQQeqLrVT09cUujEKlCkQ/ZKbH1GAaSWg
qxTMngRihqvhFG88XjCzCvyKflc2/KACfMsR/8u/EklgSN+T93s22HyI+z965FOBk7F98egjCeA9
CwDAUnDMvP9zcLLPCtzcxOMRAfMa/GXQqnj04QTQX/PEHywFMyeBIceO9BkB26t4ma2g+TP2686v
E3wkAeQiAEQH5kwCKgsA7kYAUuZtREAuJ+BzmuQa+mWNfVcr+IgFkEcB4jpwzAoAk2FZ7WH4zXGQ
NpN7rZG9/T6sr+mhn9/QLss4aP04naDGGtkUoEMHZk0CGlrd4Zd7dR6zf3sdXw/fv2jrNkD067vt
51LUW7wJAPMDa78u7/SUPnwiQHXdbiooalBs/vvrbxGGh9arAF6uOeMP68BRbPyxOdNWdS2ukZ0A
YB1Y+W1p554wPiegoQUlgJwKEDcDBCeBiBdjzrqgBNBfc8cfSQJCEYA+3N1gDtAlEsDpkgDmBDSY
A8okADQJSEWAfpYcAMU/qwXgSQJSZQB23mcaiz8oADgSAJoEpMqAkfxyaOcYIHoqAcCTAM6WBLB1
VHaZAAAGHElEQVTzPpcM1Nig0P5UCeDKFX8kCQznygEOCujn26DQ2++fPxZ1ovhzJQA0CciUARHP
h6NNAtMgEwCgAOBLAGgSkCkDsGnj6Hw+/FKCUOUIC4DrhXW99Wc5FECP/DEKQAlgFpoBxnIVoOdg
WKYMwAKKqAAHAWiRACguAFwyQCIC0IiChYBjgrg9T/x5BYAjCYgUgtgkGFjT2Y9TSUBQALInAPSq
kEghiF4CBua0O0ZTWon7HxSABRIALgMEIgBldeBEAO8Tn0Z1mvj/uxRZ17PIAJQCDrLOcS9dJAHA
AuB6KbRgGTCchwL2us6RAEQSwFBPAJwMAQs6rlOFJQCRBIDE/70cAGAZILAUQCP7aAbgCUBiCQAX
AKUEgMsNkCcE8dxut57hFDB7TrgAfCkaf0wICkSA3991TSYU2EKGxP96KbxgGSAOAbi8u8kA181k
gR1kcPxLCkCnHyQQAajDd5MBjskU9iT7v5ADFNIdIq4UcGzwLzfANZnOyFOAowABeAMALATFnQ07
JN6KANdkMoEJAI7/S434o0JQnh1gHdP7HQWAwAQAGwDlBeDZEGDwOX5TwNg54fHvq8X/8g6XAtIQ
EPUInUAPGIn/2+XyhwA3AGJGEE/LWeL/fqmJgNdzIIA8E2oWJwCQ+L9WjT96KiANAeSxcKeJ/79L
5YUUg9IQQBxEKU4AIvF/qR7/syCA9hipOAdIcPzRYlAaB/RTe/G/Xi6SESDLEwwvBYywAlALj39r
CBBnAIzS44+dDYs7GwxCgLj9j8S/qgHUMAL+4t8yArQXAdL0Hx7/d1kAeG+DA/7inx8BsnqF/7d3
rruNwkAYxdxU1zQ07/+yaxwiJd1gj42BudGfu1JXOme++WyiTfybyW647n++Jzr8YwaguhCI3Afc
b7j+D+it4z9O/mQM+Jp/tt7/0bj+x8o/ZgCuIvD5vQCV6x+8/MkY8Okr6u7I1v92/UPMn44B/31J
qY9/5X/ofQC2KvhWBO7Y4n97/Rvk/L0BhkYVfFkDvzdHh3+D/hkNjSr4ZdcQ+LHfVOIf0/sfBgZM
SwjgG3/q/GMGICsCPgT89qcS/2T4b39KbDEAVwg4dOPPgf/2Z4URf78A9vhH8PnfSgYg/rI5xPFP
jH/sSgjdGqAQ/xb98f+DAUbXQK34N/T4ewMihwFdA1nxT6n+AY+DGgLw8SfLP14FNQSA40+u/oEN
0BCAjD9p/tF3Q94ADYHH+Mfin2L9AxcBDYHU+NNd/8A1oE0gtv2px//TgM5qCJSMv+1Y8I/fCYm+
GHTR8ae//l+KQHQNSC2DU3T827Fh9MSLgMg9EE9/JusfeB4UuAfi6c8q/mFrQNgemISNf/INsaw9
kMBvGY5/ECB+KSRGgcTyZ3H5U9gFJVSBxPLnGv+gDwkIUCCJ34xDw/tJhsDMVwGXWv7Mxx92IGSr
QBq/6YZGwpMMAY4KpPHLGH9gE+CmAAA//+3//oYwqQCjOgjBL2f8nyGQ3ANc7gWSzZ/dm596IcDg
gjg9/HLKX34ZpF4GHAS/lZb+mXuArgJA/OPQiH2GAbIHaDoAou/Tv29kPz1LBRwQfysdP3gPUHIA
SF9m9+evQAb+QdnnVQH8DoDpiz36bToAVgCvA3D62v3K2yBWBzLoK/4KCiwOoJHA5dBX/LUUsDOK
IPDwM+gr/qoKXB0EeaOv+A9RIASBuwZ+Hn3FDz4RtNailqAAvj/3K354CozZCoR1cEIn8OynueAf
p9c+2beDxpZIsFjgDmRfAt8axX9GGfhjgauJvpi9rv59m6DUgVWD3WmwkC9HH4Zf8e/aBN0OBVYP
HiLkqODcCn7e+ctNp9m/fxO0ex14ihBUWG349Dz+dN7PXYe/bgz0XRUF/gjx9lP/0eGvaEDxoeCq
R2s/tkZ4Ln2N/qNWAX4HFvo6/FIdUPqUjgVKn3If6AwqCYzv/Er/7CsiLEHgR187/1VBcLUEC3wd
/Utb4XUSKHxUEhiFL16Cc6LAtAofsQVHZoFR9iQseISBqY9e2RPToIIHgbyfekVP1AO/FYII3gS4
C8vfbQN4Jc9HhD6osLiw2vDigwnMF+ieesDeD0LA/wO925C+jjTE3wAAAABJRU5ErkJggg==
"/>
</svg>

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 13 KiB

+63 -1
View File
@@ -1 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#51B13E"/><text x="32" y="44" font-family="sans-serif" font-size="28" font-weight="bold" fill="white" text-anchor="middle">PAY</text></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xml:space="preserve"
width="1024"
height="1024"
version="1.1"
id="svg7"
sodipodi:docname="BTCPay Server Icon_Version2 (1) (1).svg"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.360203"
inkscape:cx="512.21117"
inkscape:cy="512.21117"
inkscape:window-width="2346"
inkscape:window-height="1355"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="svg7" />
<defs
id="defs1">
</defs>
<path
d="M 395.38615,1007.3428 A 57.385688,57.385688 0 0 1 337.99353,949.95705 V 86.24698 a 57.39955,57.39955 0 0 1 114.78524,0 v 863.71007 a 57.385688,57.385688 0 0 1 -57.39262,57.38575"
style="fill:#cedc21;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.93064"
id="path2" /><path
d="M 395.42245,1007.3558 A 57.39955,57.39955 0 0 1 370.7771,898.10115 L 716.85161,733.85888 361.3445,471.95004 a 57.385688,57.385688 0 0 1 -12.15634,-80.2568 57.385688,57.385688 0 0 1 80.236,-12.15634 l 432.75601,318.83017 a 57.371827,57.371827 0 0 1 -9.43953,98.04775 L 419.99156,1001.7974 a 57.177769,57.177769 0 0 1 -24.56911,5.5584"
style="fill:#51b13e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.93064"
id="path3" /><path
d="M 395.43261,667.85437 A 57.385688,57.385688 0 0 1 361.34773,564.25518 L 716.84791,302.33942 370.78033,138.09021 A 57.385688,57.385688 0 0 1 343.53599,61.638322 c 13.59791,-28.6374 47.83527,-40.842254 76.45187,-27.237411 L 852.74387,239.77654 a 57.385688,57.385688 0 0 1 9.4326,98.05468 l -432.74908,318.8371 a 57.177769,57.177769 0 0 1 -33.99478,11.18605"
style="fill:#cedc21;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.93064"
id="path4" /><path
d="M 452.77605,396.7388 V 639.46363 L 617.42723,518.15666 Z"
style="fill:#1e7a44;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.93064"
id="path5" /><path
d="M 452.77896,574.75277 H 337.99334 V 294.96882 h 114.78562 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.93065"
id="path6" /><path
d="M 395.38615,28.855342 A 57.385688,57.385688 0 0 0 337.99353,86.24796 V 808.52449 H 452.77877 V 86.24796 A 57.385688,57.385688 0 0 0 395.38615,28.855342"
style="fill:#cedc21;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.93064"
id="path7" />
</svg>

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 2.9 KiB

+93 -1
View File
@@ -1 +1,93 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#1F2937"/><path d="M20 18h24v6H20z M20 28h24v6H20z M20 38h24v6H20z" fill="#22D3EE"/><circle cx="42" cy="21" r="2" fill="#4ADE80"/><circle cx="42" cy="31" r="2" fill="#4ADE80"/><circle cx="42" cy="41" r="2" fill="#FBBF24"/></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="96.767509"
height="92.983162"
viewBox="0 0 96.767509 92.983162"
version="1.1"
xml:space="preserve"
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round"
id="svg19"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs19" />
<g
transform="translate(-51.987986,-84.712078)"
id="g19">
<g
id="Icon"
transform="matrix(0.99999699,0,0,1.0000031,-3876.6653,-1644.4859)">
<g
id="g3">
<g
transform="matrix(-0.191794,-0.715786,0.715786,-0.191794,4329.14,4673.64)"
id="g1">
<path
d="m 3901.56,610.734 c -8.03,-0.473 -15.5,-2.634 -22.36,-5.857 -6.96,-3.269 -13.16,-7.784 -18.4,-13.244 -2.09,-2.176 -4.04,-4.484 -5.83,-6.924 -1.77,-2.428 -3.4,-4.976 -4.84,-7.643 -4.24,-7.842 -6.92,-16.685 -7.24,-26.198 -0.32,-7.547 0.75,-14.813 3.05,-21.561 2.43,-7.104 6.14,-13.611 10.89,-19.258 l -1.04,-0.954 c -5.4,5.445 -9.77,11.886 -12.89,19.03 -3.06,7 -4.87,14.656 -5.22,22.743 -0.34,10.523 1.83,20.557 6.11,29.438 1.48,3.074 3.24,5.998 5.22,8.743 2,2.757 4.23,5.341 6.68,7.693 6.06,5.826 13.31,10.448 21.34,13.358 7.63,2.767 15.93,3.959 24.53,3.452 z"
style="fill:#1f88c0"
id="path1" />
</g>
<g
transform="matrix(-0.191794,-0.715786,0.715786,-0.191794,4329.14,4673.64)"
id="g2">
<path
d="m 3875.69,496.573 c 3.93,-2.035 8.11,-3.676 12.51,-4.787 4.29,-1.082 8.76,-1.662 13.36,-1.754 2.26,0.098 4.47,0.3 6.65,0.656 8.92,1.459 16.98,5.126 24.1,9.995 3.82,2.611 7.28,5.652 10.5,8.936 4.28,4.361 8.08,9.197 11.04,14.613 4.35,7.965 7.11,16.954 7.47,26.636 0.29,7.88 -0.86,15.477 -3.44,22.454 -1.79,4.847 -4.18,9.431 -7.22,13.516 -3.44,4.623 -7.7,8.589 -12.39,11.931 -4.61,3.286 -9.74,5.85 -15.18,7.709 -0.72,0.243 -1.49,0.327 -2.16,0.689 -0.51,0.281 -0.79,0.687 -1.24,1.057 l 0.68,2.165 c 0.61,0.043 1.1,0.184 1.7,0.085 0.79,-0.13 1.48,-0.591 2.21,-0.908 7.71,-3.35 14.54,-8.211 20.29,-14.138 2.45,-2.525 4.68,-5.254 6.74,-8.109 2.28,-3.151 4.35,-6.466 6.12,-9.971 4.04,-8.008 6.58,-16.926 6.93,-26.48 0.38,-10.357 -1.7,-20.24 -5.88,-29 -2.91,-6.093 -6.76,-11.705 -11.53,-16.39 -3.58,-3.516 -7.69,-6.488 -12.11,-8.916 -7.96,-4.37 -16.97,-6.802 -26.47,-7.333 -2.25,-0.125 -4.51,-0.175 -6.81,-0.075 -4.69,-0.094 -9.26,0.365 -13.67,1.243 -4.59,0.912 -9,2.286 -13.18,4.128 z"
style="fill:#1f88c0"
id="path2" />
</g>
</g>
<g
id="g6">
<g
transform="matrix(-3.37109,-0.514565,0.514565,-3.37109,4078.07,1806.88)"
id="g4">
<path
d="m 22,12 c 0,-1.097 -0.903,-2 -2,-2 -0.579,0 -1.103,0.251 -1.47,0.649 C 18.202,11.006 18,11.481 18,12 c 0,1.097 0.903,2 2,2 1.097,0 2,-0.903 2,-2 z"
style="fill:none;fill-rule:nonzero;stroke:#1f88c0;stroke-width:1.05px"
id="path3" />
</g>
<g
transform="matrix(-5.33921,-5.26159,-3.12106,-6.96393,4073.87,1861.55)"
id="g5">
<path
d="m 10.315,5.333 c 0,0 -0.567,0.588 -1.285,1.34 C 7.768,7.995 6.054,9.805 6.054,9.805 L 6.237,9.86 c 0,0 1.808,-1.783 3.123,-3.089 0.747,-0.743 1.329,-1.327 1.329,-1.327 z"
style="fill:#1f88c0"
id="path4" />
</g>
</g>
<g
id="Padlock"
transform="matrix(3.11426,0,0,3.11426,3938.31,1737.25)">
<g
id="g7">
<path
d="m 9.876,21 h 8.286 C 18.625,21 19,20.625 19,20.162 V 11.838 C 19,11.375 18.625,11 18.162,11 H 5.838 C 5.375,11 5,11.375 5,11.838 v 4.92"
style="fill:none;stroke:#22b638;stroke-width:1.89px;stroke-linecap:butt;stroke-linejoin:miter"
id="path6" />
<path
d="M 8,11 V 7 c 0,-2.194 1.806,-4 4,-4 2.194,0 4,1.806 4,4 v 4"
style="fill:none;fill-rule:nonzero;stroke:#22b638;stroke-width:1.89px"
id="path7" />
</g>
</g>
<g
id="g10">
<g
transform="matrix(5.30977,0.697415,-0.697415,5.30977,3852.72,1727.97)"
id="g8">
<path
d="M 22,12 C 22,11.659 21.913,11.337 21.76,11.055 21.421,10.429 20.756,10 20,10 c -1.097,0 -2,0.903 -2,2 0,1.097 0.903,2 2,2 1.097,0 2,-0.903 2,-2 z"
style="fill:none;fill-rule:nonzero;stroke:#1f88c0;stroke-width:0.98px"
id="path8" />
</g>
<g
transform="matrix(4.93114,2.49604,1.11018,5.44847,3921.41,1726.72)"
id="g9">
<path
d="m 8.902,6.77 c 0,0 -1.667,1.483 -2.875,2.596 -0.684,0.63 -1.208,1.136 -1.208,1.136 l 0.701,0.662 c 0,0 0.501,-0.527 1.126,-1.213 C 7.749,8.739 9.219,7.068 9.219,7.068 Z"
style="fill:#1f88c0"
id="path9" />
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 5.1 KiB

+72 -1
View File
@@ -1 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#3B82F6"/><text x="32" y="44" font-family="sans-serif" font-size="36" fill="white" text-anchor="middle">⚡</text></svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 85.3 85.3" style="enable-background:new 0 0 85.3 85.3;" xml:space="preserve">
<style type="text/css">
.st0{fill:#083766;}
.st1{fill:#FFFFFF;}
.st2{fill:none;stroke:#1250A9;stroke-width:1.9869;stroke-miterlimit:10;}
.st3{fill:none;stroke:#1859D1;stroke-width:1.9869;stroke-miterlimit:10;}
.st4{fill:none;stroke:#1853FF;stroke-width:1.9869;stroke-miterlimit:10;}
</style>
<g>
<path class="st0" d="M84.7,41.6L84.7,41.6l-3.6-2.2c0-0.4-0.1-0.7-0.1-1l3.1-2.9c0.3-0.3,0.4-0.7,0.4-1.1c-0.1-0.4-0.4-0.8-0.8-0.9
L79.8,32c-0.1-0.3-0.2-0.7-0.3-1l2.5-3.4c0.3-0.3,0.3-0.8,0.1-1.2c-0.2-0.4-0.5-0.7-0.9-0.8L77,24.9c-0.2-0.3-0.3-0.6-0.5-0.9
l1.8-3.8c0.2-0.4,0.1-0.8-0.1-1.2c-0.2-0.4-0.6-0.6-1.1-0.5l-4.2,0.1c-0.2-0.3-0.4-0.5-0.7-0.8l1-4.1c0.1-0.4,0-0.9-0.3-1.2
c-0.3-0.3-0.7-0.4-1.2-0.3l-4.1,1c-0.3-0.2-0.5-0.4-0.8-0.7l0.2-4.2c0-0.4-0.2-0.8-0.6-1.1C66,7,65.5,6.9,65.2,7.1l-3.8,1.7
c-0.3-0.2-0.6-0.3-0.9-0.5l-0.7-4.2c-0.1-0.4-0.4-0.8-0.8-0.9c-0.4-0.2-0.9-0.1-1.2,0.1l-3.4,2.5c-0.3-0.1-0.7-0.2-1-0.3l-1.5-3.9
c-0.1-0.4-0.5-0.7-0.9-0.8c-0.4-0.1-0.9,0.1-1.1,0.4L47,4.3c-0.3,0-0.7-0.1-1-0.1l-2.2-3.6C43.5,0.2,43.1,0,42.7,0
c-0.4,0-0.8,0.2-1.1,0.6l-2.2,3.6c-0.3,0-0.7,0.1-1,0.1l-2.9-3.1c-0.3-0.3-0.7-0.4-1.1-0.4c-0.4,0.1-0.8,0.4-0.9,0.8L32,5.5
c-0.3,0.1-0.7,0.2-1,0.3l-3.4-2.5c-0.3-0.3-0.8-0.3-1.2-0.1c-0.4,0.2-0.7,0.5-0.7,0.9l-0.7,4.2c-0.3,0.2-0.6,0.3-0.9,0.5l-3.8-1.7
C19.8,6.9,19.3,7,19,7.2c-0.4,0.2-0.6,0.6-0.6,1.1l0.1,4.2c-0.3,0.2-0.5,0.4-0.8,0.7l-4.1-1c-0.4-0.1-0.9,0-1.2,0.3
c-0.3,0.3-0.4,0.7-0.3,1.2l1,4.1c-0.2,0.3-0.4,0.5-0.7,0.8l-4.2-0.1c-0.4,0-0.8,0.2-1.1,0.5c-0.2,0.4-0.3,0.8-0.1,1.2L8.9,24
c-0.2,0.3-0.3,0.6-0.5,0.9l-4.1,0.7c-0.4,0.1-0.8,0.4-0.9,0.8c-0.2,0.4-0.1,0.8,0.1,1.2l2.5,3.4c-0.1,0.3-0.2,0.7-0.3,1l-3.9,1.5
c-0.4,0.1-0.7,0.5-0.8,0.9c-0.1,0.4,0.1,0.9,0.4,1.1l3.1,2.9c0,0.3-0.1,0.7-0.1,1l-3.6,2.2h0C0.2,41.8,0,42.2,0,42.7
c0,0.4,0.2,0.8,0.6,1l3.6,2.2c0,0.3,0.1,0.7,0.1,1l-3.1,2.9c-0.3,0.3-0.5,0.7-0.4,1.1c0.1,0.4,0.4,0.8,0.8,0.9l3.9,1.5
c0.1,0.3,0.2,0.7,0.3,1l-2.5,3.4c-0.2,0.3-0.3,0.8-0.1,1.2c0.1,0.4,0.5,0.7,0.9,0.7l4.1,0.7c0.2,0.3,0.3,0.6,0.5,0.9l-1.7,3.8
c-0.2,0.4-0.1,0.8,0.1,1.2c0.2,0.4,0.6,0.6,1.1,0.5l4.2-0.1c0.2,0.3,0.4,0.5,0.7,0.8l-1,4.1c-0.1,0.4,0,0.8,0.3,1.1
c0.3,0.3,0.7,0.4,1.2,0.3l4.1-1c0.3,0.2,0.5,0.4,0.8,0.7L18.4,77c0,0.4,0.2,0.8,0.6,1.1c0.3,0.2,0.8,0.3,1.2,0.1l3.8-1.7
c0.3,0.2,0.6,0.3,0.9,0.5l0.7,4.1c0.1,0.4,0.4,0.8,0.7,0.9c0.4,0.2,0.8,0.1,1.2-0.1l3.4-2.5c0.3,0.1,0.7,0.2,1,0.3l1.5,3.9
c0.1,0.4,0.5,0.7,0.9,0.8c0.4,0.1,0.9-0.1,1.1-0.4l2.9-3.1c0.4,0,0.7,0.1,1,0.1l2.2,3.6c0.2,0.4,0.6,0.6,1.1,0.6
c0.4,0,0.8-0.2,1.1-0.6l2.2-3.6c0.3,0,0.7-0.1,1-0.1l2.9,3.1c0.3,0.3,0.7,0.5,1.1,0.4c0.4-0.1,0.8-0.4,0.9-0.8l1.5-3.9
c0.3-0.1,0.7-0.2,1-0.3l3.4,2.5c0.4,0.2,0.8,0.3,1.2,0.1c0.4-0.2,0.7-0.5,0.7-0.9l0.7-4.1c0.3-0.2,0.6-0.3,0.9-0.5l3.8,1.7
c0.4,0.2,0.8,0.1,1.2-0.1c0.4-0.2,0.6-0.6,0.5-1.1l-0.2-4.2c0.3-0.2,0.5-0.4,0.8-0.7l4.1,1c0.4,0.1,0.9,0,1.2-0.3
c0.3-0.3,0.4-0.7,0.3-1.1l-1-4.1c0.2-0.3,0.4-0.5,0.7-0.8l4.2,0.1c0.4,0,0.8-0.2,1.1-0.5c0.2-0.4,0.3-0.8,0.1-1.2l-1.7-3.8
c0.2-0.3,0.3-0.6,0.5-0.9l4.1-0.7c0.4-0.1,0.8-0.3,0.9-0.7c0.2-0.4,0.1-0.9-0.1-1.2l-2.4-3.4c0.1-0.3,0.2-0.7,0.3-1l3.9-1.5
c0.4-0.2,0.7-0.5,0.8-0.9c0.1-0.4-0.1-0.8-0.4-1.1L81.1,47c0-0.3,0.1-0.7,0.1-1l3.6-2.2c0.4-0.2,0.6-0.6,0.6-1
C85.3,42.2,85.1,41.8,84.7,41.6z M62.4,66.6c-1.1,0.9-2.4,1.8-3.6,2.6c-4.7,2.9-10.3,4.5-16.3,4.5c-3.4,0-6.7-0.5-9.8-1.6
c-0.1,0-0.2-0.1-0.4-0.1c-1.1-0.4-2.1-0.8-3.2-1.2v0C28.1,70.3,27,69.7,26,69c-1.2-0.8-2.4-1.6-3.5-2.5c-0.9-0.7-1.7-1.5-2.5-2.3
l-0.1,0l-0.1,0c-1-1.1-2-2.2-2.9-3.4c0,0,0.1,0,0.1,0c-3.4-4.6-5.5-10.1-6-16.1h0c0-0.2,0-0.3,0-0.5c0-0.2,0-0.3,0-0.4
c0-0.1,0-0.3,0-0.4c0-0.3,0-0.5,0-0.8v-0.1l0,0c0-0.8,0-1.6,0.1-2.4c0-0.2,0-0.4,0.1-0.7h0l0.2-0.1c0.1-1.2,0.3-2.4,0.6-3.5
c0.4-1.6,0.8-3.2,1.4-4.8c0.5-1.3,1.1-2.6,1.8-3.9L15,27c2.3-4,5.5-7.5,9.2-10.1c0.1-0.1,0.3-0.2,0.4-0.3c4.4-3.1,9.6-5,15.3-5.4
c0.8-0.1,1.7-0.1,2.5-0.1c0.8,0,1.5,0,2.2,0.1c12.3,0.9,22.6,8.8,26.9,19.8c0.6,1.6,1.1,3.2,1.5,4.8c0.5,2.2,0.7,4.4,0.7,6.7
C73.9,52.1,69.4,60.9,62.4,66.6z"/>
<circle class="st1" cx="42.7" cy="42.7" r="33.5"/>
<g>
<ellipse class="st2" cx="42.7" cy="42.8" rx="27.2" ry="13.6"/>
<ellipse transform="matrix(0.8957 -0.4447 0.4447 0.8957 -14.4551 23.4064)" class="st3" cx="42.7" cy="42.5" rx="13.6" ry="27.2"/>
<path class="st4" d="M30.6,67.1c6.7,3.3,17.6-4.9,24.2-18.3s6.6-27-0.1-30.4"/>
<ellipse transform="matrix(0.4447 -0.8957 0.8957 0.4447 -14.6426 61.9687)" class="st4" cx="42.7" cy="42.8" rx="27.2" ry="13.6"/>
<path class="st3" d="M30.6,18.2c6.7-3.3,17.6,4.9,24.2,18.3s6.6,27-0.1,30.4S37.2,62,30.5,48.6"/>
<path class="st2" d="M69.8,42.8c0,7.5-12.2,13.6-27.2,13.6s-27.2-6.1-27.2-13.6"/>
</g>
<g id="Layer_x0020_1_3_">
<g id="_1421344023328_3_">
<path class="st0" d="M50.1,40.8L50.1,40.8c0.3-2.2-1.3-3.3-3.6-4.1l0.7-2.9l-1.8-0.4l-0.7,2.8c-0.5-0.1-0.9-0.2-1.4-0.3l0.7-2.9
l-1.8-0.4l-0.7,2.9c-0.4-0.1-0.8-0.2-1.1-0.3v0L38,34.6l-0.5,1.9c0,0,1.3,0.3,1.3,0.3c0.7,0.2,0.8,0.6,0.8,1l-0.8,3.3
c0,0,0.1,0,0.2,0.1c-0.1,0-0.1,0-0.2,0l-1.2,4.7c-0.1,0.2-0.3,0.5-0.8,0.4c-0.1,0-1.3-0.3-1.3-0.3l-0.9,2l2.3,0.6
c0.4,0.1,0.8,0.2,1.3,0.3l-0.7,3l1.8,0.4l0.7-2.9c0.5,0.1,0.9,0.3,1.4,0.4l-0.7,2.9l1.8,0.4l0.7-2.9c3,0.6,5.3,0.3,6.3-2.4
c0.8-2.2,0-3.5-1.6-4.3C49,43.1,49.9,42.4,50.1,40.8z M46.1,46.5c-0.5,2.2-4.3,1-5.5,0.7l1-3.9C42.8,43.6,46.6,44.2,46.1,46.5z
M46.6,40.8c-0.5,2-3.6,1-4.6,0.7l0.9-3.5C43.9,38.2,47.2,38.7,46.6,40.8z"/>
<path class="st0" d="M50.1,40.8L50.1,40.8c0.3-2.2-1.3-3.3-3.6-4.1l0.7-2.9l-1.8-0.4l-0.7,2.8c-0.5-0.1-0.9-0.2-1.4-0.3l0.7-2.9
l-1.8-0.4l-0.7,2.9c-0.4-0.1-0.8-0.2-1.1-0.3v0L38,34.6l-0.5,1.9c0,0,1.3,0.3,1.3,0.3c0.7,0.2,0.8,0.6,0.8,1l-0.8,3.3
c0,0,0.1,0,0.2,0.1c-0.1,0-0.1,0-0.2,0l-1.2,4.7c-0.1,0.2-0.3,0.5-0.8,0.4c-0.1,0-1.3-0.3-1.3-0.3l-0.9,2l2.3,0.6
c0.4,0.1,0.8,0.2,1.3,0.3l-0.7,3l1.8,0.4l0.7-2.9c0.5,0.1,0.9,0.3,1.4,0.4l-0.7,2.9l1.8,0.4l0.7-2.9c3,0.6,5.3,0.3,6.3-2.4
c0.8-2.2,0-3.5-1.6-4.3C49,43.1,49.9,42.4,50.1,40.8z M46.1,46.5c-0.5,2.2-4.3,1-5.5,0.7l1-3.9C42.8,43.6,46.6,44.2,46.1,46.5z
M46.6,40.8c-0.5,2-3.6,1-4.6,0.7l0.9-3.5C43.9,38.2,47.2,38.7,46.6,40.8z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 218 B

After

Width:  |  Height:  |  Size: 6.3 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Logo Mark">
<rect width="47.86" height="48" rx="23.93" fill="#0DBD8B"/>
<g id="Union">
<path d="M21.3075 9.42871C20.3396 9.42871 19.5549 10.214 19.5549 11.1828C19.5549 12.1516 20.3396 12.9369 21.3075 12.9369C25.9321 12.9369 29.6811 16.689 29.6811 21.3175C29.6811 22.2863 30.4657 23.0716 31.4337 23.0716C32.4016 23.0716 33.1863 22.2863 33.1863 21.3175C33.1863 14.7515 27.868 9.42871 21.3075 9.42871Z" fill="white"/>
<path d="M38.4591 21.3174C38.4591 20.3486 37.6745 19.5633 36.7065 19.5633C35.7386 19.5633 34.9539 20.3486 34.9539 21.3174C34.9539 25.9459 31.2049 29.698 26.5804 29.698C25.6124 29.698 24.8277 30.4833 24.8277 31.4521C24.8277 32.4209 25.6124 33.2062 26.5804 33.2062C33.1408 33.2062 38.4591 27.8834 38.4591 21.3174Z" fill="white"/>
<path d="M28.3329 36.8173C28.3329 37.786 27.5482 38.5714 26.5803 38.5714C20.0198 38.5714 14.7015 33.2486 14.7015 26.6826C14.7015 25.7138 15.4862 24.9285 16.4541 24.9285C17.4221 24.9285 18.2067 25.7138 18.2067 26.6826C18.2067 31.3111 21.9557 35.0632 26.5803 35.0632C27.5482 35.0632 28.3329 35.8485 28.3329 36.8173Z" fill="white"/>
<path d="M9.40112 26.6827C9.40112 27.6514 10.1858 28.4368 11.1537 28.4368C12.1217 28.4368 12.9064 27.6514 12.9064 26.6827C12.9064 22.0542 16.6553 18.3021 21.2799 18.3021C22.2478 18.3021 23.0325 17.5167 23.0325 16.548C23.0325 15.5792 22.2478 14.7939 21.2799 14.7939C14.7194 14.7939 9.40112 20.1167 9.40112 26.6827Z" fill="white"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+23 -1
View File
@@ -1 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#8B5CF6"/><text x="32" y="44" font-family="sans-serif" font-size="28" font-weight="bold" fill="white" text-anchor="middle">N</text></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<!-- Background circle -->
<circle cx="128" cy="128" r="120" fill="#5B21B6"/>
<!-- Outer signal arc (top-left) -->
<path d="M60 128 A68 68 0 0 1 128 60" stroke="#A78BFA" stroke-width="14" stroke-linecap="round" fill="none"/>
<!-- Outer signal arc (top-right) -->
<path d="M128 60 A68 68 0 0 1 196 128" stroke="#A78BFA" stroke-width="14" stroke-linecap="round" fill="none"/>
<!-- Middle signal arc (top-left) -->
<path d="M82 128 A46 46 0 0 1 128 82" stroke="#C4B5FD" stroke-width="12" stroke-linecap="round" fill="none"/>
<!-- Middle signal arc (top-right) -->
<path d="M128 82 A46 46 0 0 1 174 128" stroke="#C4B5FD" stroke-width="12" stroke-linecap="round" fill="none"/>
<!-- Inner signal arc (top-left) -->
<path d="M104 128 A24 24 0 0 1 128 104" stroke="#EDE9FE" stroke-width="10" stroke-linecap="round" fill="none"/>
<!-- Inner signal arc (top-right) -->
<path d="M128 104 A24 24 0 0 1 152 128" stroke="#EDE9FE" stroke-width="10" stroke-linecap="round" fill="none"/>
<!-- Center antenna dot -->
<circle cx="128" cy="128" r="14" fill="#F5F3FF"/>
<!-- Antenna mast -->
<rect x="122" y="128" width="12" height="52" rx="4" fill="#DDD6FE"/>
<!-- Base platform -->
<rect x="96" y="176" width="64" height="12" rx="6" fill="#7C3AED"/>
<rect x="108" y="188" width="40" height="10" rx="5" fill="#6D28D9"/>
</svg>

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 1.4 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#0DBD8B"/><text x="32" y="44" font-family="sans-serif" font-size="28" fill="white" text-anchor="middle">EC</text></svg>

Before

Width:  |  Height:  |  Size: 217 B

+4 -1
View File
@@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#7B3FE4"/><polygon points="36,8 20,36 30,36 28,56 44,28 34,28" fill="white"/></svg>
<svg width="280px" height="280px" style="background-color:#ffffff00" version="1.1" viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg">
<path id="Ellipse" d="m7 140c0-74 60-134 134-134s134 60 134 134-60 134-134 134-134-60-134-134z" fill="#7e1af7"/>
<path d="m161 52c-8 21-16 43-25 65 0 0 0 3 3 3h65s0 2 2 3l-96 106c-2-2-2-3-2-5l33-72v-6h-67v-6l81-89h5z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 382 B

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#2D3348"/><rect x="14" y="38" width="8" height="12" rx="1" fill="#5E35B1"/><rect x="24" y="30" width="8" height="20" rx="1" fill="#7C4DFF"/><rect x="34" y="22" width="8" height="28" rx="1" fill="#B388FF"/><rect x="44" y="14" width="8" height="36" rx="1" fill="#E1BEE7"/></svg>
<?xml version="1.0" encoding="utf-8" standalone="yes"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny-ps" viewBox="0 0 241 241" xml:space="preserve"><title>Mempool Holdings S.A. de C.V.</title><g><g><path fill="#2E3349" d="M241.37,211.23c0,16.56-13.43,29.99-29.99,29.99H30.36c-16.56,0-29.99-13.43-29.99-29.99V30.21 c0-16.56,13.43-29.99,29.99-29.99h181.02c16.56,0,29.99,13.43,29.99,29.99V211.23z"></path><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="120.8689" y1="68.6556" x2="120.8689" y2="301.1491"><stop offset="0" stop-color="#AE61FF"></stop><stop offset="1" stop-color="#13EFD8"></stop></linearGradient><path fill="url(#SVGID_1_)" d="M0.32,120.99v90.24c0,16.56,13.49,29.99,30.14,29.99h180.82c16.64,0,30.13-13.43,30.13-29.99 v-90.24H0.32z"></path></g><g><path fill="#FFFFFF" fill-opacity="0.3" d="M212.72,209c0,3.7-2.53,6.7-5.65,6.7h-31.24c-3.12,0-5.65-3-5.65-6.7V32.44c0-3.7,2.53-6.7,5.65-6.7h31.24 c3.12,0,5.65,3,5.65,6.7V209z"></path></g></g></svg>

Before

Width:  |  Height:  |  Size: 374 B

After

Width:  |  Height:  |  Size: 1.0 KiB

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#0082C9"/><ellipse cx="32" cy="34" rx="18" ry="12" fill="white" opacity="0.9"/><ellipse cx="22" cy="36" rx="10" ry="8" fill="white"/><ellipse cx="42" cy="36" rx="10" ry="8" fill="white"/></svg>
<svg width="32" height="32" version="1.1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><rect width="32" height="32" rx="5" ry="5" fill="#0082c9"/><g transform="matrix(.12 0 0 .12 .64 8.32)" fill="none" stroke="#fff" stroke-width="22"><circle cx="40" cy="64" r="26"/><circle cx="216" cy="64" r="26"/><circle cx="128" cy="64" r="46"/></g></svg>

Before

Width:  |  Height:  |  Size: 291 B

After

Width:  |  Height:  |  Size: 353 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<!-- Shield -->
<path d="M128 20L32 60v68c0 55.2 40.8 106.8 96 120 55.2-13.2 96-64.8 96-120V60L128 20z" fill="#1565C0"/>
<path d="M128 32L44 68v60c0 49.6 36 95.6 84 108V32z" fill="#1E88E5"/>
<!-- Key head (circle) -->
<circle cx="128" cy="108" r="32" fill="#FFC107"/>
<circle cx="128" cy="108" r="16" fill="#1565C0"/>
<!-- Key shaft -->
<rect x="122" y="136" width="12" height="56" rx="2" fill="#FFC107"/>
<!-- Key teeth -->
<rect x="134" y="160" width="16" height="8" rx="2" fill="#FFC107"/>
<rect x="134" y="176" width="12" height="8" rx="2" fill="#FFC107"/>
</svg>

After

Width:  |  Height:  |  Size: 677 B

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<!-- Monitor body -->
<rect x="28" y="40" width="200" height="140" rx="12" fill="#1A73E8"/>
<!-- Screen -->
<rect x="40" y="52" width="176" height="108" rx="4" fill="#E8F0FE"/>
<!-- Screen content - window bars -->
<rect x="52" y="64" width="100" height="12" rx="2" fill="#4285F4"/>
<rect x="52" y="84" width="152" height="8" rx="2" fill="#DADCE0"/>
<rect x="52" y="100" width="152" height="8" rx="2" fill="#DADCE0"/>
<rect x="52" y="116" width="120" height="8" rx="2" fill="#DADCE0"/>
<!-- Cursor arrow -->
<path d="M168 88l24 56-12 4-8-18-14 16-6-6 14-16-18-8z" fill="#34A853"/>
<!-- Stand -->
<rect x="108" y="180" width="40" height="20" fill="#1A73E8"/>
<!-- Base -->
<rect x="80" y="200" width="96" height="12" rx="6" fill="#1A73E8"/>
</svg>

After

Width:  |  Height:  |  Size: 864 B

+22 -1
View File
@@ -1 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#16A34A"/><text x="32" y="43" font-family="sans-serif" font-size="32" font-weight="bold" fill="white" text-anchor="middle">RTL</text></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="610px" height="524px" viewBox="0 0 610 524" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
<title>RTL-Logo-Single</title>
<desc>Created with Sketch.</desc>
<defs>
<polygon id="path-1" points="0.451852397 0.2573 52.6803 0.2573 52.6803 52 0.451852397 52"></polygon>
</defs>
<g id="RTL-Logo-Single" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="BY_-_RTL_logo_wht" transform="translate(58.000000, 54.000000)">
<g id="Group-3" transform="translate(0.000000, 0.737000)" fill="#FFFFFE">
<path d="M360.6201,52.8608 C362.5721,57.4638 363.3541,61.1008 363.2471,63.1018 C355.6871,58.5468 344.3161,54.6748 338.1131,53.5648 C334.0751,52.8408 347.2931,49.6338 360.6201,52.8608 M371.6051,222.1978 C373.9431,221.7028 376.2931,221.2298 378.6451,220.7688 C381.1041,220.2858 383.5691,219.8228 386.0431,219.3858 C384.0591,218.1358 382.0681,216.8978 380.0641,215.6788 C354.6091,200.1908 327.4751,187.0398 300.6651,174.3118 C283.7451,166.2788 266.5131,158.2938 248.9661,151.0968 C264.2721,130.9078 283.8701,113.7738 304.8171,99.0968 C304.8351,99.0848 304.9201,99.0258 305.0521,98.9328 C309.6371,105.1898 315.4331,109.4478 318.3391,111.0478 C324.0201,114.1778 331.1751,117.1028 337.7531,117.9968 C350.6891,119.7558 361.3701,119.4848 373.1871,117.4478 C374.2521,117.2648 375.3251,117.1348 376.3931,116.9568 C383.9711,115.6898 388.8201,115.4978 393.2591,115.4148 C399.1381,115.3048 402.0501,116.5498 402.7421,116.7298 C404.8421,117.2728 406.6271,118.1718 407.6191,119.3778 C408.7711,120.8458 410.0021,122.2308 411.3211,123.4988 C415.2771,127.4358 419.2051,129.6158 424.9041,130.2408 C432.2391,131.0448 437.4311,128.1758 441.1021,123.4128 C442.8361,121.1638 442.0011,117.6318 441.8111,116.9388 C440.8481,113.3958 438.6251,108.2418 437.0161,103.4438 C436.5991,102.2038 434.5781,98.3918 432.1461,95.8048 C431.3431,94.9488 430.5351,94.0948 429.7281,93.2418 C425.9511,89.2468 422.1441,85.2818 418.3701,81.2898 C403.3111,65.3718 386.9861,50.7158 370.1561,36.7028 C368.4121,35.2508 366.6651,33.7948 364.9151,32.3418 C363.6461,31.2868 362.3721,30.2378 361.1001,29.1868 C360.4531,28.6528 359.8071,28.1198 359.1581,27.5868 C357.7891,26.4608 356.4161,25.3428 355.0401,24.2258 C354.4971,23.7858 353.9521,23.3488 353.4081,22.9098 C352.5511,22.2198 350.5951,20.5728 348.9481,19.2538 C354.4071,13.8118 358.8121,10.4238 366.6311,5.3008 C367.7521,4.5668 373.7921,1.3448 373.5791,0.5268 C373.4511,0.0328 363.5341,0.1448 352.1711,1.6088 C348.1791,2.1228 325.8041,5.4268 310.4411,8.7748 C294.0871,12.3378 276.8181,17.3188 260.9121,22.3258 C214.9671,36.7898 171.0961,56.3918 130.9221,83.0318 C107.8341,98.3418 86.2221,115.2368 65.3621,133.4398 C55.6411,141.9208 45.9791,150.5378 36.5451,159.3928 C35.0671,160.7798 33.6011,162.1788 32.1351,163.5788 C34.1441,163.9548 36.1521,164.3478 38.1561,164.7598 C54.7691,168.1718 71.2001,172.7318 87.1751,177.2978 C106.7131,182.8818 126.4791,188.9018 145.5921,196.2918 C134.7011,206.6528 124.0761,217.2688 113.9611,228.5028 C94.9141,249.6538 77.1581,271.9988 60.5891,295.1368 C38.9991,325.2848 20.5681,357.7988 4.5021,391.3058 C2.9801,394.4758 1.4761,397.6528 0.0001,400.8388 C2.9721,398.4598 5.9621,396.1018 8.9741,393.7648 C34.9841,373.5938 62.4081,354.9428 89.7891,336.9238 C128.3171,311.5688 169.6651,290.6218 212.5711,273.7668 C264.2991,253.4468 317.1251,233.7338 371.6051,222.1978" id="Fill-1"></path>
</g>
<g id="Group-24" transform="translate(172.000000, 352.737000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Clip-23"></g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 3.8 KiB

+111
View File
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Created with Vectornator (http://vectornator.io/) -->
<svg stroke-miterlimit="10" style="fill-rule:nonzero;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;" version="1.1" viewBox="8.789 8.791 32.422 32.418" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Untitled">
<path d="M38.8519 23.6353L38.3818 24.2217L41.2105 25.0446L41.2105 24.8132L39.3844 23.4968L38.8519 23.6353Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M37.9843 25.2648L38.7188 25.2376L40.119 25.1551L41.2106 25.0446L38.3819 24.2217L37.5381 23.9764L37.9843 25.2648Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M38.8519 23.6353L38.3818 24.2217L37.5381 23.9764L38.0403 23.846L38.8519 23.6353Z" fill="#ababab" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M37.9843 25.2648L33.5662 25.2648L36.0929 24.3388L37.9843 25.2648Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M38.0403 23.846L37.145 23.3355L36.8724 23.18L38.7803 21.9829L39.3844 23.4968L38.8519 23.6353L38.0403 23.846Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M37.0092 23.5521L36.7613 23.8298L36.3913 23.3554L36.3959 23.3517L37.0092 23.5521Z" fill="#ffffff" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M36.8724 23.1801L37.1451 23.3355L37.0092 23.5521L36.3959 23.3517L36.6823 23.1455L36.8724 23.1801Z" fill="#ffffff" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M38.7803 21.9829L36.8724 23.18L36.2058 20.8919L38.7665 21.949L38.7803 21.9829Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M36.7613 23.8298L37.0092 23.5521L37.1451 23.3355L38.0403 23.846L37.5381 23.9764L36.7172 23.8792L36.7613 23.8298Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M36.3913 23.3554L36.7613 23.8298L36.7172 23.8792L35.9653 23.6626L36.3913 23.3554Z" fill="#808080" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M36.7172 23.8792L37.5381 23.9764L36.103 24.3351L36.069 24.3278L36.0681 24.3271L33.4046 23.7318L35.9653 23.6626L36.7172 23.8792Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M36.3958 23.3517L36.3913 23.3554L36.1535 23.0497L36.6823 23.1455L36.3958 23.3517Z" fill="#efefef" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M36.1535 23.0497L36.3913 23.3554L35.9653 23.6626L35.9139 23.2419L36.1535 23.0497Z" fill="#d4d4d4" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M36.2058 20.8919L36.8724 23.1801L36.6823 23.1454L36.1535 23.0497L35.6779 22.9627L36.1342 20.8956L36.2058 20.8919Z" fill="#d8d8d8" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M36.1342 20.8956L35.6779 22.9627L34.4972 20.9745L36.1342 20.8956Z" fill="#bcbcbc" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M36.103 24.3351L37.5381 23.9764L37.9843 25.2648L36.0929 24.3388L36.103 24.3351Z" fill="#d8d8d8" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M36.0928 24.3388L33.5661 25.2648L33.5092 25.2648L33.3183 23.734L33.4046 23.7318L36.0681 24.3271L36.069 24.3278L36.0928 24.3388Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M33.5092 25.2648L35.7366 27.2015L35.3005 27.5949L35.2785 27.6664L35.2638 27.6745L31.5591 26.1025L33.5092 25.2648Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M34.4972 20.9745L35.6779 22.9627L33.3202 23.7046L34.4972 20.9745Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M35.2748 27.6789L34.8397 29.0874L34.0712 31.1582L31.3848 29.8823L35.2639 27.6745L35.2748 27.6789Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M35.2638 27.6745L31.3847 29.8823L31.5591 26.1025L35.2638 27.6745Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M33.2366 23.6942L31.0303 23.4143L32.3322 22.3697L32.705 22.0706L34.4972 20.9744L33.3202 23.7045L33.2366 23.6942Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M34.0712 31.1582L33.9959 31.2459L33.995 31.2459L29.0407 31.307L29.0398 31.307L29.0306 31.2916L29.0003 31.2399L31.3848 29.8823L34.0712 31.1582Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M33.9949 31.2459L32.3276 33.1745L30.6869 33.9126L29.0407 31.307L33.9949 31.2459Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M33.5661 25.2648L37.9842 25.2648L37.112 25.8822L36.5152 26.4988L35.7366 27.2015L33.5092 25.2648L33.5661 25.2648Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M33.3183 23.734L33.5092 25.2648L31.5591 26.1024L31.0303 23.4143L33.2366 23.6942L33.3155 23.7119L33.3183 23.734Z" fill="#bcbcbc" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M33.3183 23.734L33.4046 23.7318L33.3156 23.712L33.3183 23.734Z" fill="#3a3a3a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M35.6779 22.9627L36.1535 23.0497L35.9139 23.2419L35.9653 23.6626L33.4046 23.7318L33.3156 23.7119L33.2366 23.6942L33.3202 23.7046L35.6779 22.9627Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
<g opacity="1">
<path d="M34.0372 36.0387C34.0712 36.1212 34.0023 36.4077 34.0023 36.4077L33.5773 36.8313L33.6938 36.4622L33.8655 36.1594L33.6598 35.7359L32.671 35.3941L32.9253 35.7904L33.1998 36.4077L32.8904 36.6L32.4305 37.0795L32.5682 36.628L32.8904 36.2972C32.8904 36.2972 32.705 35.9834 32.671 35.8729C32.6361 35.7631 32.0394 35.3661 32.0394 35.3661L31.8677 35.1186L32.8106 35.0124L32.8115 35.0124L34.14 35.3116L34.14 35.8729L33.9683 35.5038L33.3706 35.3388L33.8655 35.6526C33.8655 35.6526 34.0023 35.9554 34.0372 36.0387" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M32.8106 35.0124L31.8677 35.1186L30.2958 34.3251L32.4994 34.9425L32.8106 35.0124Z" fill="#7f7f7f" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M31.9016 15.918L32.4305 17.3559L30.3894 21.9646L29.6981 23.5248L29.9561 20.9605L30.0994 19.5358L30.2352 18.1869L30.2958 18.2348L30.2728 18.1354L31.9016 15.918Z" fill="#585858" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M32.4305 17.3559L31.9016 20.8647L31.0303 23.4143L30.3894 21.9645L32.4305 17.3559Z" fill="#545454" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M31.9016 20.8647L32.3322 22.3697L31.0303 23.4143L31.9016 20.8647Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M31.9016 15.918L30.2728 18.1354L29.89 16.4808L29.89 16.4801L29.935 14.4225C30.3986 14.7769 30.6943 15.0111 30.7219 15.0539C30.8724 15.2851 31.9016 15.918 31.9016 15.918" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M31.8677 35.1186L30.8935 34.9145L29.5613 34.4467L30.2958 34.3251L31.8677 35.1186Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M31.5592 26.1024L31.3848 29.8823L27.8912 29.3739L27.1521 28.1289L31.5142 26.123L31.5592 26.1024Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M31.4894 26.0891L31.5141 26.123L27.1521 28.1289L29.3363 26.07L29.3896 26.0199L31.4894 26.0891Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M31.3848 29.8823L29.0003 31.2399L27.8912 29.374L31.3848 29.8823Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M30.7898 36.0659L30.9275 35.5428L31.8677 35.1186L32.0394 35.3661L31.1331 35.6806L31.0303 36.0107L31.202 36.4077L30.7898 36.0659Z" fill="#7c7c7c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M30.3894 21.9645L31.0303 23.4143L29.6981 23.5248L30.3894 21.9645Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.5282 24.895L29.6981 23.5248L31.0303 23.4143L29.3987 25.9462L29.5282 24.895Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M30.6869 33.9126L30.2958 34.3251L29.5613 34.4467L28.6496 34.0754L30.6869 33.9126Z" fill="#7c7c7c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M30.159 37.7234L29.7669 37.162L29.2179 36.804L30.7181 37.0648L30.9615 37.2445C30.9615 37.2445 31.236 37.5584 31.2709 37.6961C31.3048 37.8339 31.3737 38.1477 31.3737 38.2582C31.3737 38.368 30.8247 38.7098 30.8247 38.7098L31.0992 38.396L30.9275 37.7234L30.2269 37.327C30.2269 37.327 30.5703 38.038 30.6043 38.1477C30.6392 38.2582 30.4675 38.5168 30.3298 38.5448C30.1976 38.5713 29.7936 38.8564 29.7679 38.874L30.2958 38.2582L30.159 37.7234Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M30.7181 37.0648L29.2179 36.804L29.127 36.1167L29.1353 36.1211L29.5952 36.3252L30.3298 36.7768L30.7181 37.0648Z" fill="#7c7c7c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M30.2352 18.1869L30.0994 19.5358L22.4568 15.5216L20.4039 14.2052L26.5673 16.175L30.181 18.1449L30.1828 18.1456L30.2352 18.1869Z" fill="#9c9c9c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M30.181 18.1449L26.5673 16.175L22.5045 13.9599L20.9189 12.4372L20.2533 11.7653L20.2533 11.3137L20.6444 11.0935L22.353 11.9966L30.181 18.1449Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M30.0993 19.5358L29.9561 20.9605L23.1912 17.2491L23.1224 17.2344L20.9189 15.918L20.816 15.2741L21.1383 15.0811L22.4567 15.5216L30.0993 19.5358Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.935 14.4225L29.89 16.4801L29.89 16.4808L29.8881 16.5773L22.4788 10.1284L29.935 14.4225Z" fill="#9c9c9c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.935 14.4225L22.4788 10.1284L21.6874 9.07799L21.9417 8.79141L22.5596 8.95646C22.5596 8.95646 27.8866 12.8571 29.935 14.4225" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M22.5247 10.1904L22.4789 10.1284L29.8882 16.5773L29.89 16.4808L30.2728 18.1353L30.2352 18.1869L30.1829 18.1457L30.181 18.1449L22.3531 11.9966L21.0218 10.5042L20.884 9.97009L21.3789 9.63932L22.5247 10.1904Z" fill="#585858" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.6871 23.5197L23.9258 19.2617L23.1564 18.6171L22.7598 18.069L22.7607 18.069L29.9561 20.9605L29.6981 23.5248L29.6871 23.5197Z" fill="#545454" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.6981 23.5248L29.5282 24.895L26.0605 21.9771L29.6871 23.5197L29.6981 23.5248Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.6871 23.5197L26.0605 21.9771L24.1452 20.3851L23.5485 19.658L23.5135 19.1792L23.9258 19.2617L29.6871 23.5197Z" fill="#3a3a3a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M31.4894 26.0891L29.3896 26.0199L29.3987 25.9462L31.0303 23.4143L31.5592 26.1024L31.5141 26.123L31.4894 26.0891Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.3896 26.0199L29.3364 26.07L29.2399 26.067L29.3896 26.0199Z" fill="#a3a3a3" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.3896 26.0199L29.2399 26.067L23.1637 25.8792L24.8265 22.8566L29.3896 26.0199Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.3363 26.07L27.152 28.129L24.237 29.1773L24.0626 29.0491L22.7451 28.9386L22.1823 28.6955L25.8198 27.1434L29.2399 26.0671L29.3363 26.07Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.2399 26.0671L25.8199 27.1434L23.1637 25.88L23.1637 25.8792L29.2399 26.0671Z" fill="#c8c8c8" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M28.9985 36.4902L28.118 35.5996L29.127 36.1167L29.2179 36.804L28.8268 37.2998L28.7579 37.7786L29.1353 38.3407L28.5862 38.01L28.4357 37.3661L28.6551 36.997L28.9985 36.4902Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.1261 36.1108L29.127 36.1167L28.118 35.5996L28.0234 34.832L29.1261 36.1108Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M28.6147 34.0769L28.5743 34.071L29.0398 31.307L29.0407 31.307L30.6869 33.9126L28.6496 34.0754L28.6211 34.0776L28.6156 34.0769L28.6147 34.0769Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.0398 31.307L28.5743 34.071L22.8819 33.268L29.0398 31.307Z" fill="#a2a2a2" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.0398 31.307L22.8819 33.268L26.2331 30.0856L29.0398 31.307Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M29.0398 31.307L26.2331 30.0856L27.8912 29.374L29.0003 31.2399L28.9984 31.2407L29.0306 31.2916L29.0398 31.307Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M28.6211 34.0776L28.6496 34.0754L29.5613 34.4467L28.0234 34.832L28.6211 34.0776Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M28.6211 34.0776L28.0234 34.832L28.118 35.5996L28.0234 35.5038L27.2889 34.9425L28.6211 34.0776Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M28.6211 34.0776L27.2889 34.9425L26.8639 35.0361L25.9411 34.0864L28.5735 34.0769L28.6147 34.0769L28.6156 34.0769L28.6211 34.0776Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M28.5743 34.071L28.5734 34.0769L25.9411 34.0864L25.9383 34.0864L23.2858 34.0953L23.2849 34.0953L22.8819 33.268L28.5743 34.071Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M27.1521 28.129L27.8912 29.374L26.2331 30.0856L27.1521 28.129Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M26.0862 35.1288L26.8639 35.036C26.8639 35.036 24.7972 38.9285 24.3619 39.7816L26.0862 35.1288Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M22.5045 13.9599L26.5673 16.175L20.4039 14.2052L20.1156 13.6711L20.2533 13.2858L20.7472 13.2858L22.5596 14.0129L22.5045 13.9599Z" fill="#7c7c7c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M26.2331 30.0856L22.8819 33.268L23.5135 31.1309L24.2232 29.2148L24.2407 29.2192L26.2331 30.0856Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M26.0862 35.1288L24.3619 39.7816C24.3142 39.8759 24.2867 39.9326 24.283 39.9437C24.2481 40.0535 23.5485 40.5603 23.5485 40.5603L22.848 40.5876L26.0862 35.1288Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M25.9383 34.0864L25.9411 34.0864L26.8638 35.0361L26.0861 35.1288L24.5565 35.3116L25.9383 34.0864Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M25.9383 34.0864L24.5565 35.3116L24.1076 35.0559L23.6164 34.7767L23.2849 34.0961L23.2858 34.0953L25.9383 34.0864Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M25.8199 27.1434L22.1823 28.6955L22.1814 28.6955L22.0445 28.6358L21.5368 28.1784L23.1637 25.88L25.8199 27.1434Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M24.4198 22.5745L24.8265 22.8566L21.0998 22.6128L19.7033 22.5214L19.6308 22.506L16.4063 19.8805L22.6845 21.415L24.4198 22.5745Z" fill="#bcbcbc" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M24.5565 35.3116L22.69 40.7334C22.5403 40.8661 22.3227 41.0399 22.2162 41.0399C22.0445 41.0399 21.1732 41.1769 21.1732 41.1769L20.8014 41.1865L24.5565 35.3116Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M24.5565 35.3116L20.8014 41.1865L19.986 41.2086L19.3121 41.1496L19.1809 40.9906L24.1076 35.0559L24.5565 35.3116Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M24.0552 20.3608L24.4197 22.5745L22.6844 21.415L19.0744 19.0024L24.0552 20.3608Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M24.2407 29.2192L24.2233 29.2148L24.237 29.1772L27.1521 28.1289L26.2331 30.0856L24.2407 29.2192Z" fill="#8d8d8d" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M24.1452 20.3852L26.0605 21.9771L29.5283 24.895L29.3988 25.9463L29.3896 26.02L24.8265 22.8567L24.4198 22.5745L24.0552 20.3608L24.1452 20.3852Z" fill="#595959" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M24.1076 35.0559L19.1809 40.9905L18.5777 41.0399L17.8092 40.8078L17.5347 40.2848L24.1076 35.0559Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M24.1076 35.0559L17.5347 40.2848L23.2849 34.0961L23.6164 34.7767L24.1076 35.0559Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M23.2858 34.0953L23.2849 34.0961L23.2849 34.0953L23.2858 34.0953Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M23.2849 34.0953L23.2849 34.0961L16.3603 39.3853L19.0036 36.8313L22.8819 33.268L23.2849 34.0953Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M23.2849 34.0961L17.5346 40.2848L16.869 40.3673L16.2373 40.0535L16.3062 39.4369L16.3585 39.3861L16.3603 39.3853L23.2849 34.0961Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M24.8265 22.8567L23.1637 25.8792L23.1628 25.8792L23.138 25.8674L21.0998 22.6128L24.8265 22.8567Z" fill="#8c8c8c" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M23.1628 25.8792L23.1637 25.8792L23.1637 25.88L21.5368 28.1784L21.4817 28.129L20.4039 28.157L19.9788 27.9368L19.5187 27.5677L19.1754 27.1434L19.2699 27.1132L23.1628 25.8792Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M23.138 25.8674L23.1628 25.8792L19.2698 27.1132L20.3441 24.5385L23.138 25.8674Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M22.8479 40.5876C22.8479 40.5876 22.7818 40.6524 22.69 40.7334L24.5565 35.3116L26.0861 35.1288L22.8479 40.5876Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M22.7102 18.0005L22.6762 17.2071L23.1224 17.2344L23.1913 17.2491L29.9561 20.9605L22.7606 18.0691L22.7598 18.0691L22.7102 18.0005Z" fill="#454545" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M19.0744 19.0024L22.6844 21.415L16.4063 19.8805L11.2674 17.3287L19.0734 19.0024L19.0744 19.0024Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M20.3442 24.5385L20.256 24.5385L14.6995 24.5385C14.6995 24.5385 12.7365 23.6626 12.7016 23.5794C12.6676 23.4969 12.3591 22.9628 12.3591 22.9628L12.4271 22.6048L17.9763 23.6118L18.0745 23.6295L20.3442 24.5385Z" fill="#d3d3d3" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M20.3442 24.5385L19.2699 27.1132L19.1754 27.1434L18.3381 27.0881L17.6724 26.8958L17.2464 26.526L17.0885 26.0104L20.256 24.5385L20.3442 24.5385Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M20.256 24.5385L17.0884 26.0104L17.0747 25.9647L15.2971 25.7717L14.8712 25.2376L14.6995 24.5385L20.256 24.5385Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M16.4063 19.8805L19.6307 22.506L19.5114 22.4802L11.24 19.1166L10.0381 18.3305L9.06393 17.3007L8.82343 16.673L8.99512 16.3703L9.86642 16.7666L11.2675 17.3287L16.4063 19.8805Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M17.9763 23.6117L12.4271 22.6047L10.8552 22.2356L19.2993 22.4353L19.5114 22.4802L19.6307 22.506L19.7033 22.5214L17.9763 23.6117Z" fill="#707070" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M17.9763 23.6117L19.7033 22.5214L21.0998 22.6128L23.1381 25.8674L20.3442 24.5385L18.0745 23.6294L17.9763 23.6117Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M19.0735 19.0024L11.2675 17.3287L9.45504 15.5216L9.38617 15.0538L9.62673 14.8226L12.4959 16.5625C12.4959 16.5625 16.2373 18.0278 16.443 18.1383C16.6487 18.248 18.1664 18.7549 18.1664 18.7549L19.0735 19.0024Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M19.2993 22.4353L11.463 20.7601L11.542 20.7704L9.59278 19.9608L8.99511 19.2617L8.78939 18.6724L9.06398 18.4411L11.3354 19.1792L11.24 19.1166L19.5114 22.4802L19.2993 22.4353Z" fill="#9a9a9a" fill-rule="evenodd" opacity="1" stroke="none"/>
<path d="M11.4621 20.7601L11.4631 20.7601L19.2993 22.4353L10.8552 22.2357L10.073 21.5918L9.79847 20.8367L9.90129 20.5502L11.4621 20.7601Z" fill="#484848" fill-rule="evenodd" opacity="1" stroke="none"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

+5 -1
View File
@@ -1 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#0DBD8B"/><text x="32" y="44" font-family="sans-serif" font-size="30" font-weight="bold" fill="white" text-anchor="middle">[M]</text></svg>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M 30,2.0000001 V 30 h -1 -2 v 2 h 5 V -3.3333334e-8 L 27,0 v 2 z"/>
<path fill="#ffffff" d="M 9.9515939,10.594002 V 12.138 h 0.043994 c 0.3845141,-0.563728 0.8932271,-1.031728 1.4869981,-1.368 0.580003,-0.322998 1.244999,-0.485 1.993002,-0.485 0.72,0 1.376999,0.139993 1.971998,0.42 0.595,0.279004 1.047001,0.771001 1.355002,1.477001 0.338003,-0.500001 0.795999,-0.941 1.376999,-1.323001 0.579999,-0.382998 1.265998,-0.574 2.059998,-0.574 0.602003,0 1.160002,0.074 1.674002,0.220006 0.514,0.148006 0.953998,0.382998 1.321999,0.706998 0.36601,0.322999 0.653001,0.746 0.859,1.268002 0.205001,0.521998 0.307994,1.15 0.307994,1.887001 v 7.632997 h -3.127 v -6.463997 c 0,-0.383002 -0.01512,-0.743002 -0.04399,-1.082003 -0.02079,-0.3072 -0.103219,-0.607113 -0.242003,-0.881998 -0.133153,-0.25081 -0.335962,-0.457777 -0.584001,-0.596002 -0.257008,-0.146003 -0.605998,-0.220006 -1.046997,-0.220006 -0.440002,0 -0.796003,0.085 -1.068,0.253002 -0.272013,0.170003 -0.485001,0.390002 -0.639001,0.662003 -0.159119,0.287282 -0.263585,0.601602 -0.307994,0.926997 -0.05197,0.346923 -0.07801,0.697217 -0.07801,1.048002 v 6.353999 h -3.128005 v -6.398 c 0,-0.338003 -0.0072,-0.673001 -0.02116,-1.004001 -0.01134,-0.313663 -0.07487,-0.623229 -0.187994,-0.915999 -0.107943,-0.276623 -0.300435,-0.512126 -0.550001,-0.673001 -0.25799,-0.168 -0.636,-0.253002 -1.134999,-0.253002 -0.198123,0.0083 -0.394383,0.04195 -0.584002,0.100006 -0.258368,0.07446 -0.498455,0.201827 -0.704999,0.373985 -0.227981,0.183987 -0.421999,0.449 -0.583997,0.794003 -0.161008,0.345978 -0.242003,0.797998 -0.242003,1.356998 v 6.618999 H 6.99942 V 10.590001 Z"/>
<path fill="#ffffff" d="M 2,2.0000001 V 30 h 3 v 2 H 0 V 9.2650922e-8 L 5,0 v 2 z"/>
</svg>

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 1.8 KiB

+31 -1
View File
@@ -1 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#7D4698"/><text x="32" y="44" font-family="sans-serif" font-size="30" font-weight="bold" fill="white" text-anchor="middle">TOR</text></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512px" height="512px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient x1="50%" y1="100%" x2="50%" y2="0%" id="linearGradient-1">
<stop stop-color="#420C5D" offset="0%"></stop>
<stop stop-color="#951AD1" offset="100%"></stop>
</linearGradient>
<path d="M25,29 C152.577777,29 256,131.974508 256,259 C256,386.025492 152.577777,489 25,489 L25,29 Z" id="path-2"></path>
<filter x="-18.2%" y="-7.4%" width="129.4%" height="114.8%" filterUnits="objectBoundingBox" id="filter-3">
<feOffset dx="-8" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="10" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0.250980392 0 0 0 0 0.250980392 0 0 0 0 0.250980392 0 0 0 0.2 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="tor-browser-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon_512x512">
<g id="Group">
<g id="tb_icon/Stable">
<g id="Stable">
<circle id="background" fill="#F2E4FF" fill-rule="nonzero" cx="256" cy="256" r="246"></circle>
<path d="M256.525143,465.439707 L256.525143,434.406609 C354.826191,434.122748 434.420802,354.364917 434.420802,255.992903 C434.420802,157.627987 354.826191,77.8701558 256.525143,77.5862948 L256.525143,46.5531962 C371.964296,46.8441537 465.446804,140.489882 465.446804,255.992903 C465.446804,371.503022 371.964296,465.155846 256.525143,465.439707 Z M256.525143,356.820314 C311.970283,356.529356 356.8487,311.516106 356.8487,255.992903 C356.8487,200.476798 311.970283,155.463547 256.525143,155.17259 L256.525143,124.146588 C329.115485,124.430449 387.881799,183.338693 387.881799,255.992903 C387.881799,328.654211 329.115485,387.562455 256.525143,387.846316 L256.525143,356.820314 Z M256.525143,201.718689 C286.266674,202.00255 310.3026,226.180407 310.3026,255.992903 C310.3026,285.812497 286.266674,309.990353 256.525143,310.274214 L256.525143,201.718689 Z M0,255.992903 C0,397.384044 114.60886,512 256,512 C397.384044,512 512,397.384044 512,255.992903 C512,114.60886 397.384044,0 256,0 C114.60886,0 0,114.60886 0,255.992903 Z" id="center" fill="url(#linearGradient-1)"></path>
<g id="half" transform="translate(140.500000, 259.000000) scale(-1, 1) translate(-140.500000, -259.000000) ">
<use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-2"></use>
<use fill="url(#linearGradient-1)" fill-rule="evenodd" xlink:href="#path-2"></use>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 2.9 KiB

+236
View File
@@ -0,0 +1,236 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="128px"
height="128px"
viewBox="0 0 128 128"
version="1.1"
id="svg96"
sodipodi:docname="Sovran_SystemsOS_Updater_Iconv3.svg"
xml:space="preserve"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview98"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="5.2149125"
inkscape:cx="9.0126153"
inkscape:cy="64.430611"
inkscape:window-width="3440"
inkscape:window-height="1352"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer2" /><defs
id="defs67"><linearGradient
inkscape:collect="always"
id="linearGradient936"><stop
style="stop-color:#1e8e11;stop-opacity:1;"
offset="0"
id="stop932" /><stop
style="stop-color:#1bff00;stop-opacity:0;"
offset="1"
id="stop934" /></linearGradient><linearGradient
id="linearGradient1028"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop1026" /></linearGradient><linearGradient
id="linearGradient998"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop996" /></linearGradient><radialGradient
id="radial0"
gradientUnits="userSpaceOnUse"
cx="131.914749"
cy="55.927143"
fx="131.914749"
fy="55.927143"
r="160"
gradientTransform="matrix(0.232034,-0.541475,-0.368794,-0.0298398,4.277749,118.95849)"><stop
offset="0"
style="stop-color:#00ff39;stop-opacity:1;"
id="stop2" /><stop
offset="1"
style="stop-color:#004a19;stop-opacity:1;"
id="stop4" /></radialGradient><radialGradient
id="radial1"
gradientUnits="userSpaceOnUse"
cx="525.587769"
cy="638.591797"
fx="525.587769"
fy="638.591797"
r="192"
gradientTransform="matrix(-0.107656,-0.225172,-0.327748,0.258343,373.87973,30.205086)"><stop
offset="0"
style="stop-color:#43b60b;stop-opacity:1;"
id="stop7" /><stop
offset="1"
style="stop-color:#0b88ff;stop-opacity:0.00829875;"
id="stop9" /></radialGradient><clipPath
id="clip1"><path
d="M 7 46 L 57 46 L 57 93 L 7 93 Z M 7 46 "
id="path12" /></clipPath><clipPath
id="clip2"><path
d="M 32.25 46.957031 C 19.6875 46.96875 9.085938 56.636719 7.503906 69.53125 C 9.0625 82.445312 19.667969 92.144531 32.25 92.160156 C 44.816406 92.148438 55.414062 82.480469 57 69.585938 C 55.441406 56.671875 44.835938 46.972656 32.25 46.957031 Z M 32.25 46.957031 "
id="path15" /></clipPath><radialGradient
id="radial2"
gradientUnits="userSpaceOnUse"
cx="131.914749"
cy="55.927143"
fx="131.914749"
fy="55.927143"
r="160"
gradientTransform="matrix(0.485163,-1.148584,-0.771115,-0.0632965,-47.124961,203.98857)"><stop
offset="0"
style="stop-color:rgb(92.941177%,20%,23.137255%);stop-opacity:1;"
id="stop18" /><stop
offset="1"
style="stop-color:rgb(63.921571%,27.843139%,72.941178%);stop-opacity:1;"
id="stop20" /></radialGradient><radialGradient
id="radial3"
gradientUnits="userSpaceOnUse"
cx="525.587769"
cy="638.591797"
fx="525.587769"
fy="638.591797"
r="192"
gradientTransform="matrix(-0.225099,-0.477638,-0.685291,0.548001,725.67923,15.723794)"><stop
offset="0"
style="stop-color:rgb(10.980392%,44.313726%,84.705883%);stop-opacity:1;"
id="stop23" /><stop
offset="1"
style="stop-color:rgb(20.784314%,51.764709%,89.411765%);stop-opacity:0.00829876;"
id="stop25" /></radialGradient><linearGradient
id="linear0"
gradientUnits="userSpaceOnUse"
x1="22"
y1="37"
x2="62"
y2="37"
gradientTransform="matrix(1.4,0,0,1.4,-26.799973,2.491745)"><stop
offset="0"
style="stop-color:rgb(58.039218%,57.647061%,56.470591%);stop-opacity:1;"
id="stop28" /><stop
offset="0.0908155"
style="stop-color:rgb(87.058824%,86.666667%,85.490197%);stop-opacity:1;"
id="stop30" /><stop
offset="0.336093"
style="stop-color:rgb(60.392159%,60.000002%,58.823532%);stop-opacity:1;"
id="stop32" /><stop
offset="0.844326"
style="stop-color:rgb(76.47059%,75.294119%,72.941178%);stop-opacity:1;"
id="stop34" /><stop
offset="0.930505"
style="stop-color:rgb(87.058824%,86.666667%,85.490197%);stop-opacity:1;"
id="stop36" /><stop
offset="1"
style="stop-color:rgb(75.294119%,74.901962%,73.725492%);stop-opacity:1;"
id="stop38" /></linearGradient><radialGradient
id="radial4"
gradientUnits="userSpaceOnUse"
cx="-172.560638"
cy="28.569126"
fx="-172.560638"
fy="28.569126"
r="15.85742"
gradientTransform="matrix(1.560712,0,0,1.4252,300.69366,13.349996)"><stop
offset="0"
style="stop-color:rgb(100%,100%,100%);stop-opacity:0.358268;"
id="stop41" /><stop
offset="1"
style="stop-color:rgb(100%,100%,100%);stop-opacity:0.0944882;"
id="stop43" /></radialGradient><filter
id="alpha"
filterUnits="objectBoundingBox"
x="0"
y="0"
width="1"
height="1"><feColorMatrix
type="matrix"
in="SourceGraphic"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
id="feColorMatrix46" /></filter><mask
id="mask0"><g
filter="url(#alpha)"
id="g51"><rect
x="0"
y="0"
width="128"
height="128"
style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"
id="rect49" /></g></mask><clipPath
id="clip3"><rect
x="0"
y="0"
width="192"
height="152"
id="rect54" /></clipPath><g
id="surface382"
clip-path="url(#clip3)"><path
style=" stroke:none;fill-rule:nonzero;fill:rgb(27.058825%,21.176471%,21.568628%);fill-opacity:1;"
d="M 40 59.957031 C 26.191406 59.957031 15 71.152344 15 84.957031 C 15.011719 85.996094 15.085938 86.777344 15.222656 87.804688 C 15.222656 75.957031 27.421875 65.96875 40 65.957031 C 52.597656 65.972656 64.777344 75.957031 64.777344 87.859375 C 64.917969 86.816406 64.992188 86.011719 65 84.957031 C 65 71.152344 53.808594 59.957031 40 59.957031 Z M 40 59.957031 "
id="path57" /></g><radialGradient
id="radial5"
gradientUnits="userSpaceOnUse"
cx="40"
cy="227"
fx="40"
fy="227"
r="28"
gradientTransform="matrix(0.575553,0,1.60551e-8,1.540703,8.977913,-280.78108)"><stop
offset="0"
style="stop-color:rgb(100%,100%,100%);stop-opacity:1;"
id="stop60" /><stop
offset="0.744626"
style="stop-color:rgb(98.039216%,98.039216%,98.039216%);stop-opacity:1;"
id="stop62" /><stop
offset="1"
style="stop-color:rgb(87.450981%,87.450981%,87.450981%);stop-opacity:1;"
id="stop64" /></radialGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient936"
id="linearGradient938"
x1="-48.519272"
y1="18.511358"
x2="287.07454"
y2="18.511358"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1020247,0,0,1.1097375,37.198581,-10.424856)" /></defs><path
style="fill:#f5f5f3;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 20,11.957031 h 88 c 4.41797,0 8,3.582031 8,8 V 108 c 0,4.41797 -3.58203,8 -8,8 H 20 c -4.417969,0 -8,-3.58203 -8,-8 V 19.957031 c 0,-4.417969 3.582031,-8 8,-8 z m 0,0"
id="path69" /><path
style="fill:url(#radial0);fill-rule:nonzero;stroke:none"
d="m 20,85.957031 h 88 v -66 H 20 Z m 0,0"
id="path71" /><path
style="fill:none;fill-rule:nonzero;stroke:none;fill-opacity:1"
d="m 20,85.957031 h 88 v -66 H 20 Z m 0,0"
id="path73" /><g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 1"
transform="matrix(0.1816,0,0,0.1816,35.224187,79.037164)"><ellipse
fill="#54c147"
cx="168.64549"
cy="10.117889"
id="circle8314"
rx="184.91634"
ry="179.91556"
style="fill:url(#linearGradient938);fill-opacity:1;stroke-width:1.71591" /><polygon
fill="#ffffff"
points="46.678,120.299 63.562,96.402 96.977,121.718 145.084,50.79 168.752,69.02 103.583,164.647 "
id="polygon8316"
transform="matrix(1.7395866,0,0,1.6925423,-18.737581,-172.19767)" /></g></svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

+267 -1
View File
@@ -1 +1,267 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#175DDC"/><path d="M32 10 L48 20 L48 36 C48 48 32 54 32 54 C32 54 16 48 16 36 L16 20 Z" fill="white" opacity="0.9"/><text x="32" y="40" font-family="sans-serif" font-size="18" font-weight="bold" fill="#175DDC" text-anchor="middle">VW</text></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="0 0 256 256"
id="svg383"
sodipodi:docname="vaultwarden.svg"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
width="256"
height="256"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs387">
<mask
id="holes">
<rect
x="-60"
y="-60"
width="120"
height="120"
fill="#fff"
id="rect368" />
<circle
id="hole"
cy="-40"
r="3" />
<use
transform="rotate(72)"
xlink:href="#hole"
id="use371" />
<use
transform="rotate(144)"
xlink:href="#hole"
id="use373" />
<use
transform="rotate(216)"
xlink:href="#hole"
id="use375" />
<use
transform="rotate(-72)"
xlink:href="#hole"
id="use377" />
</mask>
</defs>
<sodipodi:namedview
id="namedview385"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="3.3359375"
inkscape:cx="128.14988"
inkscape:cy="127.85012"
inkscape:window-width="3440"
inkscape:window-height="1363"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="gear" />
<title
id="title287">Vaultwarden Icon</title>
<g
id="gear"
mask="url(#holes)"
transform="matrix(2.4381018,0,0,2.4381018,128,128)">
<path
d="M -31.1718,-33.813208 -4.675771,40.375675 H 4.6757689 L 31.171799,-33.813208 H 21.404635 L 4.6757689,13.77574 q -1.662496,4.571864 -2.805462,8.624198 -1.142966,3.948427 -1.870308,7.585137 -0.72734199,-3.63671 -1.8703079,-7.689043 -1.142966,-4.052334 -2.805462,-8.728104 L -21.30073,-33.813208 Z"
stroke="#ffffff"
stroke-width="4.51171"
id="path289"
style="fill:#ffffff;fill-opacity:1" />
<circle
transform="scale(-1,1)"
r="43"
fill="none"
stroke="#ffffff"
stroke-width="9"
id="circle291"
cx="0"
cy="0" />
<g
id="cogs"
transform="scale(-1,1)">
<polygon
id="cog"
points="46,3 51,0 46,-3 "
stroke="#ffffff"
stroke-linejoin="round"
stroke-width="3" />
<use
transform="rotate(11.25)"
xlink:href="#cog"
id="use294" />
<use
transform="rotate(22.5)"
xlink:href="#cog"
id="use296" />
<use
transform="rotate(33.75)"
xlink:href="#cog"
id="use298" />
<use
transform="rotate(45)"
xlink:href="#cog"
id="use300" />
<use
transform="rotate(56.25)"
xlink:href="#cog"
id="use302" />
<use
transform="rotate(67.5)"
xlink:href="#cog"
id="use304" />
<use
transform="rotate(78.75)"
xlink:href="#cog"
id="use306" />
<use
transform="rotate(90)"
xlink:href="#cog"
id="use308" />
<use
transform="rotate(101.25)"
xlink:href="#cog"
id="use310" />
<use
transform="rotate(112.5)"
xlink:href="#cog"
id="use312" />
<use
transform="rotate(123.75)"
xlink:href="#cog"
id="use314" />
<use
transform="rotate(135)"
xlink:href="#cog"
id="use316" />
<use
transform="rotate(146.25)"
xlink:href="#cog"
id="use318" />
<use
transform="rotate(157.5)"
xlink:href="#cog"
id="use320" />
<use
transform="rotate(168.75)"
xlink:href="#cog"
id="use322" />
<use
transform="scale(-1)"
xlink:href="#cog"
id="use324" />
<use
transform="rotate(-168.75)"
xlink:href="#cog"
id="use326" />
<use
transform="rotate(-157.5)"
xlink:href="#cog"
id="use328" />
<use
transform="rotate(-146.25)"
xlink:href="#cog"
id="use330" />
<use
transform="rotate(-135)"
xlink:href="#cog"
id="use332" />
<use
transform="rotate(-123.75)"
xlink:href="#cog"
id="use334" />
<use
transform="rotate(-112.5)"
xlink:href="#cog"
id="use336" />
<use
transform="rotate(-101.25)"
xlink:href="#cog"
id="use338" />
<use
transform="rotate(-90)"
xlink:href="#cog"
id="use340" />
<use
transform="rotate(-78.75)"
xlink:href="#cog"
id="use342" />
<use
transform="rotate(-67.5)"
xlink:href="#cog"
id="use344" />
<use
transform="rotate(-56.25)"
xlink:href="#cog"
id="use346" />
<use
transform="rotate(-45)"
xlink:href="#cog"
id="use348" />
<use
transform="rotate(-33.75)"
xlink:href="#cog"
id="use350" />
<use
transform="rotate(-22.5)"
xlink:href="#cog"
id="use352" />
<use
transform="rotate(-11.25)"
xlink:href="#cog"
id="use354" />
</g>
<g
id="mounts"
transform="scale(-1,1)">
<polygon
id="mount"
points="-7,-42 0,-35 7,-42 "
stroke="#ffffff"
stroke-linejoin="round"
stroke-width="6" />
<use
transform="rotate(72)"
xlink:href="#mount"
id="use358" />
<use
transform="rotate(144)"
xlink:href="#mount"
id="use360" />
<use
transform="rotate(-144)"
xlink:href="#mount"
id="use362" />
<use
transform="rotate(-72)"
xlink:href="#mount"
id="use364" />
</g>
</g>
<metadata
id="metadata381">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title>Vaultwarden Icon</dc:title>
<dc:creator>
<cc:Agent>
<dc:title>Mathijs van Veluw</dc:title>
</cc:Agent>
</dc:creator>
<dc:relation>Rust Logo</dc:relation>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 6.7 KiB

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#21759B"/><text x="32" y="44" font-family="serif" font-size="38" font-weight="bold" fill="white" text-anchor="middle">W</text></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve"><style>.style0{fill: #0073aa;}</style><g><g><path d="M4.548 31.999c0 10.9 6.3 20.3 15.5 24.706L6.925 20.827C5.402 24.2 4.5 28 4.5 31.999z M50.531 30.614c0-3.394-1.219-5.742-2.264-7.57c-1.391-2.263-2.695-4.177-2.695-6.439c0-2.523 1.912-4.872 4.609-4.872 c0.121 0 0.2 0 0.4 0.022C45.653 7.3 39.1 4.5 32 4.548c-9.591 0-18.027 4.921-22.936 12.4 c0.645 0 1.3 0 1.8 0.033c2.871 0 7.316-0.349 7.316-0.349c1.479-0.086 1.7 2.1 0.2 2.3 c0 0-1.487 0.174-3.142 0.261l9.997 29.735l6.008-18.017l-4.276-11.718c-1.479-0.087-2.879-0.261-2.879-0.261 c-1.48-0.087-1.306-2.349 0.174-2.262c0 0 4.5 0.3 7.2 0.349c2.87 0 7.317-0.349 7.317-0.349 c1.479-0.086 1.7 2.1 0.2 2.262c0 0-1.489 0.174-3.142 0.261l9.92 29.508l2.739-9.148 C49.628 35.7 50.5 33 50.5 30.614z M32.481 34.4l-8.237 23.934c2.46 0.7 5.1 1.1 7.8 1.1 c3.197 0 6.262-0.552 9.116-1.556c-0.072-0.118-0.141-0.243-0.196-0.379L32.481 34.4z M56.088 18.8 c0.119 0.9 0.2 1.8 0.2 2.823c0 2.785-0.521 5.916-2.088 9.832l-8.385 24.242c8.161-4.758 13.65-13.6 13.65-23.728 C59.451 27.2 58.2 22.7 56.1 18.83z M32 0c-17.645 0-32 14.355-32 32C0 49.6 14.4 64 32 64s32-14.355 32-32.001 C64 14.4 49.6 0 32 0z M32 62.533c-16.835 0-30.533-13.698-30.533-30.534C1.467 15.2 15.2 1.5 32 1.5 s30.534 13.7 30.5 30.532C62.533 48.8 48.8 62.5 32 62.533z" class="style0"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 230 B

After

Width:  |  Height:  |  Size: 1.5 KiB

+144
View File
@@ -0,0 +1,144 @@
<svg width="160" height="160" version="1.1" viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="160" height="160" image-rendering="optimizeQuality" preserveAspectRatio="none" xlink:href="data:image/jpeg;base64,/9j/4QDKRXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAA
XgEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAZgAAAAAAAABIAAAAAQAAAEgAAAAB
AAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAA
AQAAAUCgAwAEAAAAAQAAAUCkBgADAAAAAQAAAAAAAAAAAAD/2wCEAAEBAQEBAQIBAQIDAgICAwQD
AwMDBAUEBAQEBAUGBQUFBQUFBgYGBgYGBgYHBwcHBwcICAgICAkJCQkJCQkJCQkBAQEBAgICBAIC
BAkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCf/d
AAQACv/AABEIAKAAoAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQ
AAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYX
GBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqS
k5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz
9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQID
EQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RF
RkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqy
s7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/
AP8AP/ooooA734ceM734f+MrLxVZ5b7LIC6D+OI8SJ26r098elft/wCHLyz1fT7fVtMcS21zGssT
r0ZHGVI/CvwEBx0r9R/2J/iMuveH5/hxqT5udL/fWuTy1s5+ZR/1zc/grAdq/LvFHKPaYT63Baw3
9P8AgH+gf0DfFz+zs3qcL4uVqeI1h5VEtv8At6Kt6xij9CtGjOQPavWtFi6Zrz7RrbnOK9X0W3Py
iv4z4jxKsz/WqrM9P0GPG2vbNBQgrXlWg2/3QRXtGgW/Sv514qxC1PJxM9LHrGgLt24Hpiux8TfE
XwD8KPC03jb4maxZ6FpFtjzLq9lWKME8BRn7zE8BVBJPAFfmd+1x/wAFBvh1+yhpsnhvRkj8QeMm
VdmnCTbFah+kl265K8crEo3v/sr8w/mX+PH7S3xh/aO8WHxd8U9Zm1GZC32eL/V29sjHOy3hX5Il
AAHHzkD5navrvCz6J+a8WcuOzCXsMK9nb35L+6ui/vP5Jn8YeOX0rcq4bnLLsuSr4lbr7EH/AHmt
3/dXzaP6Ff2hf+C8PhTwo8/h39mPw6dXmUFV1fWg8NvnjDRWSkTOOf8Alq0PTpivxT+OH/BRn9sX
9oOSW18d+N9SNjMXH9n2EhsLMK/AUQ2vl7gBxiRpK+Fq+of2Pf2dPE/7VH7Qfhr4K+F9ySazdrHN
Oo/49rZPnubg+nkwqzKenmbF/ir++eFPAvgfgjBTzGhhYL2UXKVWfvSSirt8z20/lsvI/wA3eKfG
DirizFLD4mvJ87SUI+7HXZcq3+d2f1H/APBvV+x6ngP4Y6p+1n4vtsal4oDaZom9eU06F/8ASJ19
PtM6hVI4aKFCOtf06ab2FeOfDTwR4Z+GvgvSPh74LtFsdI0O0hsbKBBhY4IEEcagf7o/OvadNQcC
v+cX6RviviOM+KcXn9f4Zu0F/LCOkF92/m2f6OcF8GUuH8lo5ZT3ive85Pf8dF5WO1sB09K7Ky6i
uSsF7fSuwsV6Gv5dzFnmZpIb4r8aeFfhv4M1X4h+Ob2LTdE0Gzn1C/u5mCRwW1tGZJXYngBUUmv8
sH9v39rTxV+2v+1L4s/aA8TGSJdbvCbK0kY/6Hp8I8uytQu91UxQBfMC8ec0rD71f12/8HKX7cSf
Cb4EaV+xx4KvAmt+OwNR1wRvh4tGtpP3UDBXVgL25UDGCrwQzKetfwgySPLIZZSWZjkk9STX++H7
KrwB/sfh+txvj4WrYv3aflRi9/8At+S+6MWfwn498W/WMbHLKT92nv8A4v8AgL9RlFFFf62H8+H/
0P8AP/ooooAK9M+E/j+/+GPj7TvGVjlvscoMkY/5aQt8sidh8yZx23Y9K8zpQcHI7VjiMPCrTdKo
tGrHo5RmtfA4qnjMLLlnBqUWujWqP6gfCNxp2vaVaa5pEiz2l5Ek8Mi9GjcBlI+or2jRLLleK/MX
/gnF8WU8VeFrr4SavLm90X/SLPceWtHb5lGTn9zIfoFdR2r9btH04gjAr/NfxRwM8pzCrgqnTbzX
T8D/AKBfCnxHocU8P4bOaO817y/lktJL79vKx1uh2p44r4Q/bp/bzs/2f9Om+FPwonSbxlcR4uLg
YddMRxxwcg3LLyiHiMYdh91W9B/bJ/aksv2Xvhqv9ilZvFOsq0WmxHBEIAw91Iv9yPICj+JyB0yR
/L1rGr6j4g1S51rVpnnuruV55pZDueSSQlndm7sxOSfWvq/APwThnNVZ7m8L0Iv3IvabXV/3V26v
yWv8jfS2+kjPJ4vhvI52ryX7yS+wntFdpNdfsrbV6R6nquoa3fzalqsz3FxcSPLLLKxd5JHOWd2b
JZmPJY8msuiiv9A0kkopWR/lLObk7sVVLEKoyTwAK/sU/wCDfz9kIeBPhlqf7Vniy226j4o3ado2
8cpp0L/6RMvHH2mddoI4aKFCOtfzEfshfs8eJv2pv2gfDfwX8MbkfWLtY5p1H/HtbIN9zcHjjyYg
xU9N+xf4q/0e/hp4G8N/DrwdpPgDwbarZaRotpDY2dugwscECBI1AHoB+df5pftH/Gv+yMghwpgp
2q4nWdulJdP+3np6KSP6/wDoleHixmYzz3Ex9yjpH/G1/wC2r80eu6ZFjFeg6cnSuN0yLaRXf6dH
gV/gJmlQ/u3Nq51VhH0qz4q8Y+Ffhv4K1X4heObyLTtF0Gznv7+6lISOC2tozJK7E4ACopp9knAF
fza/8HJP7b6/Cj4E6V+x34KvAmteOQNQ1sRvh4tHt5P3UDBXVh9tuVwRgq8MMy96+t8BvCHFcd8Y
YPhrDbVJLna+zTjrOXyjt52R+F+IvE9PKsvq42fRaLu+i/rofyPft8ftYeK/20v2ofFfx98TmSNd
buy1naOT/odhEPLsrULuZVMUAXeF485pWH3q+MKe7vK5kkO5mOST3NMr/rk4fyLC5XgKOW4GChSp
RUIxWyjFWS+SR/mbjMXOvVlWqO7eoUUUV65zH//R/wA/+iiigAooooA9k+A/xV1P4MfFPSPiFpoZ
vsEwM0S/8trdvlmixkD5oydueNwU9q/q+l8f+BPD/wAMJfjFfXqf8I9BYf2n9pT5g1uUDqUx94sC
AoHUkAV/G8CVIZeCK+pNY/ar+IGsfs26b+zVccaZp1+9154c75Ifvw2zL/chlJdef7owAvP4H4y+
Dn+s1fCVqT5eWSU/+ve7t5rp6n9U/R8+kRU4OwOOwdRc0Zx5qa6KqtF8mt/8KOO/aI+NniP4/fFT
UviL4jyjXT7LeAElbe2jyIYV9kU846uWbvXhdFFfuGX4CjhKEMLh48sIJJJdEtEfzPm2a18diZ4z
FS5pzbbfdsKVVLEKoyTwAKSvp79kH9nfxL+1N+0B4b+C/hvdG+sXSpPcKP8Aj2tU+e5uDwQPKiDF
SePM2L3rHOc2w+X4SrjsXJRp04uTfRJK7/Azy3L6uKxEMNQjeUmkku70R/TR/wAECf2RR4D+GGpf
tTeK7bGpeKd2n6PvXlNOhf8AfzLkcfaZ1wCOGiiQjrX9KOlQDivMPhz4G8NfDzwfpXgLwdarZaTo
tpDZWcEYwscECBEUAeigV7RpkHHSv+Xj6Q/ivX4w4mxWe1tpu0F/LBaRX3b+dz/Y3gLg+lw7klDK
6W8V7z7yfxP79vJJHVadDxiu4sIugrmtPiwVFdrYJ2r+XcyrGWaVuxD4r8Y+Ffhr4L1b4ieObyPT
tF0Gzn1C/upSFSG3tozJK7E4ACqpr/Lt/by/as8Vftm/tP8Air4+eJzJGNavC1naOT/oljEPLs7U
LuYL5UAXeF485pGH3q/rW/4OQv22U+FfwM0r9j3wZdhNZ8bhdR1wRthotIt5P3MDbWVh9suV6EFX
hhlXvX8MLu8jmSQ7mY5JPc1/u5+y28BP7IyCrxtj4WrYv3aflSi9/wDt+Sv6Ri+p/n59IjjP61jo
5XRfu09/8X/AX6jaKKK/1fP5tCiiigD/0v8AP/ooooAKKKKACiiigAooooAVVZmCIMk8ACv7Bf8A
ggb+yIPAfwu1L9qXxZbbdS8VbtP0cuvMemwv++mXIBH2mdcAjhookI61/M1+x9+zr4l/ao/aB8N/
Bfw3vRtXu1S4uEH/AB7WqfPc3GcEDyoQxUnjzNi/xV/o0/DzwN4a+H3hHS/Ang61Wy0nRrSGxs7e
MYWOCBAkagDphRX+a/7Rnxr/ALIyKnwpgp2q4nWdulJdP+3np6Jn9ifRJ8OvruYzz7ER9yjpHzm1
/wC2r8Wj0DTLY5HrXe6fCBj2rB02AKvArsLGMYz/AJ4r/BfMsQf33mFU6Wwi4qx4q8ZeFfhp4L1b
4i+OLyPTtF0Kznv766lIVIbe3QySOxPAAVTUlivAr+cP/g44/bXX4XfBHSv2P/Bd2E1fxqF1HWxG
2Gj0m3k/cwNtZSPtlwvIwVeGGVe9fYeBPhJiuOeL8Hw3h17tSXvtfZprWb+S287I/F/Eniqnk+WV
cdU6LRd30X9dD+Tf9u39qjxT+2R+054q+PXigyRf21eFrS1cn/RLKIeXZ2oG5gvkwKocLx5pkYfe
r45pzu8jmSQ5ZuSTTa/6ycgyPC5ZgaOXYKChSpRUYpbKMVZL5JH+WWNxlTEVpV6rvKTuwooor1zl
CiiigD//0/8AP/ooooAKKKKACiiigApyqzMEQZJ4AFNr6j/Y7/Zz8SftU/tB+G/gv4dDo2r3SpcX
CD/j2tI/nubjOCB5UIYqSMeYUX+KvNznN8Pl+Dq47FyUadOLlJ9Ekrv8Dty3L6uLxEMNQjeUmkku
70SP6a/+CBf7Ia+AfhVqX7U3i22C6n4sBsNH3r8yabA/76ZcgEfap1wCOGiijI61/R5p8IGK878B
eDfDfw/8JaV4G8HWqWOk6NaQ2NnbxgKsUECCONAB6KBXp9oAuB7V/wAvv0gPFPEcY8T4rPa203aC
/lgtIr7t/O5/sv4ecF0uHskoZXS3ivefeT3f+XlY6WzQADFdTZgACuXtSAMV0Vs4xzX86Yu56uMj
oT+LPGnhX4a+DNW+Ifje8j0/RtCs57++upCFSK3t0MkjknphVNf5jn7c37Uvir9sT9pjxT8ePFBk
j/tq7LWts5P+iWUQ8u0tQNzBfJgVQ4HHmmRh96v6q/8Ag4m/bUT4a/BbSv2Q/Bl2F1XxkF1HWhG3
zR6VA/7iBsMCPtdwvIIIaGGRe9fxKuzSMXc5J5JNf7ofsxvAf+x8iq8ZY+Fq2K92n5Uovf8A7fkr
+kYn+dP0muN/rWYxyii/cpfF/i/4C/UbRRRX+p5/LgUUUUAFFFFAH//U/wA/+iiigAooooAKKKKA
HKrOwRBkngAV/YX/AMEEP2R0+Hvwm1L9qXxZahdU8W5sNH3r80emQP8AvpVyoI+1TrgEcNFFGR1r
+Zj9jn9nLxJ+1T+0J4b+C/hzfH/a90q3Nwg/49rSP57m4ztIHlQhipPHmGNf4q/0UPCHhPw38P8A
wlpngbwfapY6To1rDY2VvGNqxQQII40AHTCgV/mx+0V8Z/7LyOnwngp2q4nWdulNdP8At56eiaP7
H+iJ4dfXcynnuJj7lHSP+Nr/ANtX5o7WFgP8K27e5GMdK5EXBU8VMt72r/EKpheY/wBHpwuei212
FxzVXxb4/wDDHw68Har4/wDGd2lho+iWk1/e3EhCrFBboXkYnpworlIdRAPBr+fT/gv1+2UPAXwg
0z9lLwfd7dT8W7b/AFnY2Gj0yB/3MLYII+1TrkjGGihkHev0TwR8GsTxlxThMgoq0Zy99/ywWsn9
23nZH5h4ocU0chyatmVX7K91d5PSK+/8D+ZD9tn9pvxT+1z+0f4n+OXicvG2s3Za2tnJ/wBFs4x5
drbAZIHlQqoYDjzTIw+9XyXTmZnYu5yT1Jptf9RmR5LhstwVLL8HDlp04qMUtkoqyXyR/jrmGOqY
mvLEVneUnd/MKKKK9Q4wooooAKKKKAP/1f8AP/ooooAKKKKACnIrOwRBkngAU2vq79jD9mvxL+1j
+0R4a+CXh0PH/a92FurlB/x62cfz3VxnawXyoQ2wkY80xr/FXl53nGGy7BVcfi5ctOnFyk+iUVd/
gjty3L6uLrww1BXlJpJeb0R/TP8A8EE/2Q/+Fc/B7Uv2qPFtrt1XxeDYaPvXDR6XA/72ZcqCPtU6
8EZDRRRkda/fmeYIMtUPhzwn4b+HvhHTPAng20Sx0jRbSGxsreMBUigt0EcaADgAKBWPqF1sJJNf
8zHjH4l4jjXijFZ9X2m/cX8sFpGP3b+dz/aPwv4IpZBk1DK6X2Vq+8nq3/l5WHzXuDVJtS29DXK3
2o7ec1zNzrO08HFfL4TJnLofpccOdX4u+Ifh34feE9S8c+LrpLPStHtZby8ncgLHBAhd2J9lFf57
v7X37RfiX9qf9oDxH8Z/ExdG1e6LW9ux/wCPa1jGy2twMkDyoVUMBx5hdh96v6Ef+C2/7Wj+Efhh
p/7NPhe523/ibbe6tsPKafC/7mI4II+0TLkjo0cTDvX8qDMzHcxyTX+yf0AfBKOTZRU4nxcLVcRp
Dypr/wCSevoon+aP0wPENYvNIZDhpe5Q1l5za/8AbVp6tiUUUV/ogfxkFFFFABRRRQAUUUUAf//W
/wA/+iiigAooooAciM7hIxljwAK/s2/4N/8A9jlfhr8GNT/au8YWmzVvGQNhoxdcNHpUD/vZlyoY
fa7heCCQ0UMRHWv5f/2Iv2YvE/7Xv7SPhj4G+HBJGNZuwt3coD/otlEPMu7knawXyoQ2wkY80xr/
ABV/pI6J4P8ADPw78HaZ4C8F2cdho+iWkNhY20ShUht7dBHGigcABVFf5i/tJPHBZVktLg7Az/e4
r3qlulKPT/t+St6Rkj+vfoneH6xeZyzrER9yjpH/ABP/AORX4tHJ6tJszXles323I9K77XrjYDmv
DfEGoeWGPev8iOGcDz2R/p3gaVzD1bVdmea8h8bfELQ/BHhrUPGPia4W007SraS6upmOFSGFSzn8
hVzW9Z2bhmvwg/4K+/tNnw74Esv2fvDtxi81/beansPKWUTfuozjp50q5I6FI2Hev628F/CWrxHn
OHyyC92T959or4n923nY8DxP41ocM5BiM3q7xXurvJ6RX37+SPw6/ad+OviD9o341698WPEO5X1S
4LQwsf8AUW6DZBAByB5cYAIHG/ce9fPlKSScmkr/AHtyvLaGCw1PCYaPLCCUUl0SVkj/AA0zPMau
LxE8VXd5Sbbfmwoor6L/AGXPgF4o/aS+MukfC/wzmM3km+5uSCUtrWMgzzt2/dqflB+85Re9LNMz
oYLDVMXiZKMIJtvskPKsrr43EQwmGjzTm0kl1b0SPWfDf7CXxY8S/sc61+2RY4Gj6NqcdkbUxMZJ
bbiO4vFYf8sreZljf5cffbcAnPw6ysjFHGCOCD2r/Q2+H/wy+GGg/Be3/Z8tNNjbwlFpX9jGyk+d
ZLRo/KdXznczgksT1Y5r+GL9rr9nvX/2YPj34i+D+u75BpVyRa3Dj/j5s5BvtZ87VB8yIruxx5gd
f4a/lb6OH0klxnmOY5fiI8kqcuaktm6Oyv5p7/4l2P6G8dfAWpwnhcHiYPmU48s30VRau3k18P8A
hPmSiiiv64P5qCiiigD/1/8AP/ooooAKciPI4jjGWbgAU2vr/wDYa/Zb8UfthftLeF/gV4bEkS6z
dgXd0gP+iWMQ8y7uc7WCmKFTsJGPNaJf4hXk59nWGyzA1cxxklGnSi5SfRKKu38kjsy/A1MTWhh6
KvKTSS9dD+qb/g3m/YzT4W/BLVP2tvGNoF1jxqDYaMZEw8Wk27/vZlyisBeXC8EEq8MMTDrX79a9
dKikCtXQ/C3hj4c+DdM8A+CrOPT9H0SzhsLG1iUKkNvboI4o1A4AVVArz3xFeFQRmv8Alq8XvE3F
cc8XYviHEbVJe4v5YLSEfkrX87s/188KuEKWSZTRy+nvFa+b6v79vI8v8T3wAY9hXzn4o1LG7mvW
fFN8NrZNfM/izUfvYr7zgfJ72uj9/wAsp6Hj3xJ8daN4O8O6h4r8R3AttP02CS5uJG4CRRLuY/kO
K/i4/aE+MOufHf4t618TNcyr6lOWiiP/ACxgUbYYgO3lxgA443bj3r9sP+Ctn7RB8P8AhKy+BGgz
4u9bxeajtPK2kbfuoz6edIucd0jI71/PCSScmv8Aan6IHhjHLMplnVeP7ytpHygv/kn+CR/mz9NP
xOWOzWHDuFl+7w+svOo1/wC2rT1bQlFFXrS0udQuI7OzRpZZWVERAWZmbhVUDkkngAda/sbY/iCE
HJ8sSxpWl32t6lBpOmRPPcXMiQxRxjc7u5CqiqOrMTgCv6qP+Cf/AOzfpf7Mfw+Muq7JvFGthJNR
mGD5Kj7lrGQPuR9WP8UhJ6bQPz+/Yz/Zl0/4UGH4i+OYkl8SSL+4iOGWxVhg47GZhwzD7g+Ve5P6
zeGde+7g1/DH0mPEGeZ4d5LgH+6+2/5rbJf3V+Poj/Vz6KP0ZquTUlxJntO1eS/dwf2Ivq+0mun2
Vvq7L748MeId23nivya/4LW/szx/E34Rad+0r4Xt92q+EwLLVSi/NJpkz/u5G2qSfsszewWKWQ9q
++/C2uHanNe4mz0Dxt4Yv/Bniy3S90vVraWzu7eQZSWCZCkiEHjBU4r/AD94P4jxHCPEmHzzDLSm
/eXeD0kvu280j+iPFzw6o8RZJXyqotZL3X2kvhf37+R/nvsrIxRxgjgj0ptfSn7WHwB179mn46+I
PhHrW+RdMuCLW4Yf8fFpIN9tPnaoO+IjdjjzA6j7tfNdf7s5RmlDHYWnjMLLmhNKSa6pq6/A/wAK
M0yytg8TPCYiPLODaa7NaNBRRRXoHAf/0P8AP/ooooAfGjyOscQLMxAAHUnsBX9vH/Bu1+xbH8K/
gbqn7XXjK0C6z43BsNFMiYeLSLeT95Mu5FYfbLlcgglXhhhI61/KX+wh+yt4o/bJ/ad8K/Abw2JI
k1q7AvbqMH/RLCIeZeXOdjqpigDeWWGPOaJf4q/029C8H+F/h34P0vwD4Hs49O0bQ7OGwsbWFQsc
NtboI4o1A4AVVA4r/K79p347f2TkdHgvATtWxXvVLdKUXt/2/JW9IyR/T30auCvrWYSzasvdpaR/
xf8AAX5o5rW5sKa8N8S3RwwBr2DX2+UivBfE0h2k1/jXwrhk2mf6O5P0PC/Fl5jd7V8jfFTxno3g
7w5qPi3xFOttYaZbyXNxK3ASKJSzH8hxX014smIVjX88P/BYX9ohfDXhKx+APh+bbe65tvdS2nlL
ONv3MR/67SrkjukZHev7y+j14fVM/wA5w+WU1pJ+95RW7+7bzsd/iBx7R4ayCvm9XeKtFd5PSK+/
fyR+FX7QHxd1z45/FnWfiXrmVfUpy0cRP+phX5YYR6eXGFB7btx714pX0T4C/Za+O/xK2T+HfDl3
9mfaftVyn2aDB775tuR/uBq+0/Af/BP3TNC2aj8VdT+2yDk2VjlIs88POQHb/gCp9a/24xXF2SZN
Rjg/aRXIklGOrSWiVlt87H+VfCHgjxjxli3isLhpP2ju6k/djru7vf8A7du/I/OTwJ8N/F/xH1Zd
H8J2b3Mgx5jdI4lP8Ujn5UH6nsDX6n/Az9nzwv8ACNY9Xviuo66Rzclfkhz1WFT09C5+Y+w4r3mx
8P8Ah/wjpiaF4Xs4bCzj+7FAgRfqcdT7nmoVkPmfLX5dxHx5iMxg6NFclP8AF+v+R/p94C/Q+yfh
WcMyzJrEYpbO3uQ/wrq1/M/kkes6NfEMMGvcfDOpsNuTXzJo1ydwHpXtfh25I2mv534qy5ODP65r
wR9geFdUb5eeK+kfC2qH5Qa+OPClwflzX0p4WuDhea/kbjjKo3Z8rjqSPzf/AOCzn7M6fEb4S6f+
0f4Ytt2qeEwLTVNi/NJpkzfJI2FLH7LM3qAsckh7V/LMysjFHGCOCPSv9CKXQ9C8aeGr7wd4ptkv
dM1W2ls7u3kAKSQTIUkQg8YKk1/Df+1j+z/r37M3x28QfCHW97rpdwRa3Dj/AI+LSQb7aYHaoO+I
jdgYEgdf4a/t36EHih9dy2rwvi5fvMPrDzpt7f8Abr09HFdD/Jr6Z/hl9QzWHEGGj7lbSVuk0v8A
25fimfNdFFFf3gfxCf/Z
"/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

-40
View File
@@ -1,40 +0,0 @@
"""Thin wrapper around the systemctl CLI for Sovran_SystemsOS_Hub."""
from __future__ import annotations
import subprocess
from typing import Literal
def _run(cmd: list[str]) -> str:
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
return result.stdout.strip()
except Exception:
return ""
def is_active(unit: str, scope: Literal["system", "user"] = "system") -> str:
return _run(["systemctl", f"--{scope}", "is-active", unit]) or "unknown"
def is_enabled(unit: str, scope: Literal["system", "user"] = "system") -> str:
return _run(["systemctl", f"--{scope}", "is-enabled", unit]) or "unknown"
def run_action(
action: str,
unit: str,
scope: Literal["system", "user"] = "system",
method: str = "systemctl",
) -> bool:
base_cmd = ["systemctl", f"--{scope}", action, unit]
if scope == "system" and method == "pkexec":
cmd = ["pkexec", "--user", "root"] + base_cmd
else:
cmd = base_cmd
try:
subprocess.Popen(cmd)
return True
except Exception:
return False
-727
View File
@@ -1,727 +0,0 @@
"""Sovran_SystemsOS_Hub — Main GTK4 Application."""
from __future__ import annotations
import json
import os
import socket
import subprocess
import threading
import urllib.request
from datetime import datetime
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gdk, Gio, GLib, Gtk
from .config import load_config
from .service_tile import ServiceTile
APP_ID = "com.sovransystems.hub"
Adw.init()
CATEGORY_ORDER = [
("infrastructure", "Infrastructure"),
("bitcoin-base", "Bitcoin Base"),
("bitcoin-apps", "Bitcoin Apps"),
("communication", "Communication"),
("apps", "Self-Hosted Apps"),
("nostr", "Nostr"),
]
ROLE_LABELS = {
"server_plus_desktop": "Server + Desktop",
"desktop": "Desktop Only",
"node": "Bitcoin Node",
}
AUTOSTART_DIR = os.path.join(
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
"autostart",
)
USER_AUTOSTART_FILE = os.path.join(AUTOSTART_DIR, "sovran-hub.desktop")
SYSTEM_AUTOSTART_FILE = "/etc/xdg/autostart/sovran-hub.desktop"
DOWNLOADS_DIR = os.path.join(os.path.expanduser("~"), "Downloads")
FLAKE_LOCK_PATH = "/etc/nixos/flake.lock"
FLAKE_INPUT_NAME = "Sovran_Systems"
GITEA_API_BASE = "https://git.sovransystems.com/api/v1/repos/Sovran_Systems/Sovran_SystemsOS/commits"
UPDATE_COMMAND = [
"ssh", "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes",
"root@localhost",
"cd /etc/nixos && nix flake update && nixos-rebuild switch && flatpak update -y",
]
REBOOT_COMMAND = [
"ssh", "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes",
"root@localhost",
"reboot",
]
UPDATE_CHECK_INTERVAL = 1800
TILE_GRID_WIDTH = 820
# ── Autostart helpers ────────────────────────────────────────────
def get_autostart_enabled() -> bool:
if os.path.isfile(USER_AUTOSTART_FILE):
try:
with open(USER_AUTOSTART_FILE, "r") as f:
for line in f:
if line.strip().lower() == "x-gnome-autostart-enabled=false":
return False
if line.strip().lower() == "hidden=true":
return False
return True
except Exception:
return True
return os.path.isfile(SYSTEM_AUTOSTART_FILE)
def set_autostart_enabled(enabled: bool):
os.makedirs(AUTOSTART_DIR, exist_ok=True)
if enabled:
if os.path.isfile(USER_AUTOSTART_FILE):
os.remove(USER_AUTOSTART_FILE)
else:
with open(USER_AUTOSTART_FILE, "w") as f:
f.write("[Desktop Entry]\n")
f.write("Type=Application\n")
f.write("Name=Sovran_SystemsOS Hub\n")
f.write("Exec=sovran-hub\n")
f.write("X-GNOME-Autostart-enabled=false\n")
f.write("Hidden=true\n")
# ── Update check helpers ────────────────────────────────────────
def _get_locked_info():
try:
with open(FLAKE_LOCK_PATH, "r") as f:
lock = json.load(f)
nodes = lock.get("nodes", {})
node = nodes.get(FLAKE_INPUT_NAME, {})
locked = node.get("locked", {})
rev = locked.get("rev")
branch = locked.get("ref")
if not branch:
branch = node.get("original", {}).get("ref")
return rev, branch
except Exception:
pass
return None, None
def _get_remote_rev(branch=None):
try:
url = GITEA_API_BASE + "?limit=1"
if branch:
url += f"&sha={branch}"
req = urllib.request.Request(url, method="GET")
req.add_header("Accept", "application/json")
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read().decode())
if isinstance(data, list) and len(data) > 0:
return data[0].get("sha")
except Exception:
pass
return None
def check_for_updates() -> bool:
locked_rev, branch = _get_locked_info()
remote_rev = _get_remote_rev(branch)
if locked_rev and remote_rev:
return locked_rev != remote_rev
return False
# ── IP address helpers ───────────────────────────────────────────
def _get_internal_ip():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
s.connect(("1.1.1.1", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
pass
try:
result = subprocess.run(
["hostname", "-I"],
capture_output=True, text=True, timeout=5,
)
if result.returncode == 0:
parts = result.stdout.strip().split()
if parts:
return parts[0]
except Exception:
pass
return "unavailable"
def _get_external_ip():
for url in [
"https://api.ipify.org",
"https://ifconfig.me/ip",
"https://icanhazip.com",
]:
try:
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=8) as resp:
ip = resp.read().decode().strip()
if ip and len(ip) < 46:
return ip
except Exception:
continue
return "unavailable"
# ── UpdateDialog ─────────────────────────────────────────────────
class UpdateDialog(Adw.Window):
def __init__(self, parent):
super().__init__(
title="Sovran_SystemsOS Update",
default_width=900,
default_height=700,
modal=True,
transient_for=parent,
)
self._process = None
self._full_log = ""
header = Adw.HeaderBar()
self._close_btn = Gtk.Button(label="Close", sensitive=False)
self._close_btn.connect("clicked", lambda _b: self.close())
header.pack_end(self._close_btn)
self._reboot_btn = Gtk.Button(
label="Reboot",
css_classes=["destructive-action"],
tooltip_text="Reboot the system now",
visible=False,
)
self._reboot_btn.connect("clicked", self._on_reboot_clicked)
header.pack_end(self._reboot_btn)
self._save_btn = Gtk.Button(
label="Save Error Report",
css_classes=["warning"],
tooltip_text="Save full log to ~/Downloads",
visible=False,
)
self._save_btn.connect("clicked", self._on_save_report)
header.pack_start(self._save_btn)
self._spinner = Gtk.Spinner(spinning=True)
header.pack_start(self._spinner)
self._status_label = Gtk.Label(
label="Updating…",
css_classes=["title-4"],
halign=Gtk.Align.CENTER,
margin_top=12,
margin_bottom=8,
)
self._textview = Gtk.TextView(
editable=False,
cursor_visible=False,
monospace=True,
wrap_mode=Gtk.WrapMode.WORD_CHAR,
top_margin=8,
bottom_margin=8,
left_margin=12,
right_margin=12,
)
self._buffer = self._textview.get_buffer()
scrolled = Gtk.ScrolledWindow(
hscrollbar_policy=Gtk.PolicyType.AUTOMATIC,
vscrollbar_policy=Gtk.PolicyType.AUTOMATIC,
vexpand=True,
child=self._textview,
)
content = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=0,
)
content.append(self._status_label)
content.append(scrolled)
toolbar_view = Adw.ToolbarView()
toolbar_view.add_top_bar(header)
toolbar_view.set_content(content)
self.set_content(toolbar_view)
self._start_update()
def _append_text(self, text):
self._full_log += text
end_iter = self._buffer.get_end_iter()
self._buffer.insert(end_iter, text)
mark = self._buffer.create_mark(None, self._buffer.get_end_iter(), False)
self._textview.scroll_mark_onscreen(mark)
self._buffer.delete_mark(mark)
def _start_update(self):
self._append_text(
"$ ssh root@localhost 'cd /etc/nixos && nix flake update "
"&& nixos-rebuild switch && flatpak update -y'\n\n"
)
thread = threading.Thread(target=self._run_update, daemon=True)
thread.start()
def _run_update(self):
try:
self._process = subprocess.Popen(
UPDATE_COMMAND,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
for line in self._process.stdout:
GLib.idle_add(self._append_text, line)
self._process.wait()
rc = self._process.returncode
if rc == 0:
GLib.idle_add(self._on_finished, True, "Update complete!")
else:
GLib.idle_add(self._on_finished, False, f"Update failed (exit code {rc})")
except Exception as e:
GLib.idle_add(self._on_finished, False, f"Error: {e}")
def _on_finished(self, success, message):
self._spinner.set_spinning(False)
self._close_btn.set_sensitive(True)
if success:
self._status_label.set_label("✓ " + message)
self._status_label.set_css_classes(["title-4", "success"])
self._reboot_btn.set_visible(True)
else:
self._status_label.set_label("✗ " + message)
self._status_label.set_css_classes(["title-4", "error"])
self._save_btn.set_visible(True)
self._append_text(f"\n{'─' * 60}\n{message}\n")
def _on_save_report(self, _btn):
os.makedirs(DOWNLOADS_DIR, exist_ok=True)
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"sovran-update-error-{timestamp}.log"
filepath = os.path.join(DOWNLOADS_DIR, filename)
try:
with open(filepath, "w") as f:
f.write(f"Sovran_SystemsOS Update Error Report\n")
f.write(f"Date: {datetime.now().isoformat()}\n")
f.write(f"{'═' * 60}\n\n")
f.write(self._full_log)
self._save_btn.set_label(f"Saved: {filename}")
self._save_btn.set_sensitive(False)
self._append_text(f"\n✓ Error report saved to ~/Downloads/{filename}\n")
except Exception as e:
self._append_text(f"\n✗ Failed to save report: {e}\n")
def _on_reboot_clicked(self, _btn):
dialog = Adw.MessageDialog(
transient_for=self,
heading="Reboot Now?",
body="The system will restart immediately. Save any open work first.",
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("reboot", "Reboot")
dialog.set_response_appearance("reboot", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("cancel")
dialog.set_close_response("cancel")
dialog.connect("response", self._on_reboot_confirmed)
dialog.present()
def _on_reboot_confirmed(self, dialog, response):
if response == "reboot":
try:
subprocess.Popen(REBOOT_COMMAND)
except Exception as e:
self._append_text(f"\n✗ Reboot failed: {e}\n")
# ── Main Window ──────────────────────────────────────────────────
class SovranHubWindow(Adw.ApplicationWindow):
def __init__(self, app, config):
super().__init__(
application=app,
title="Sovran_SystemsOS Hub",
default_width=860,
default_height=800,
)
self._config = config
self._tiles = []
self._update_available = False
css_path = os.environ.get("SOVRAN_HUB_CSS", "")
if css_path and os.path.isfile(css_path):
provider = Gtk.CssProvider()
provider.load_from_path(css_path)
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(), provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
header = Adw.HeaderBar()
header.set_title_widget(self._build_title_box())
self._update_btn = Gtk.Button(
label="Update System",
css_classes=["suggested-action"],
tooltip_text="System is up to date",
)
self._update_btn.connect("clicked", self._on_update_clicked)
header.pack_start(self._update_btn)
self._badge = Gtk.Label(
label=" ●",
css_classes=["update-badge"],
visible=False,
)
header.pack_start(self._badge)
refresh_btn = Gtk.Button(
icon_name="view-refresh-symbolic",
tooltip_text="Refresh now",
)
refresh_btn.connect("clicked", lambda _b: self._refresh_all())
header.pack_end(refresh_btn)
menu_btn = Gtk.MenuButton(
icon_name="open-menu-symbolic",
tooltip_text="Settings",
)
popover = Gtk.Popover()
menu_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=8,
margin_top=12,
margin_bottom=12,
margin_start=12,
margin_end=12,
)
autostart_row = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=12,
)
autostart_label = Gtk.Label(
label="Start at login",
hexpand=True,
halign=Gtk.Align.START,
)
self._autostart_switch = Gtk.Switch(
valign=Gtk.Align.CENTER,
active=get_autostart_enabled(),
)
self._autostart_switch.connect("state-set", self._on_autostart_toggled)
autostart_row.append(autostart_label)
autostart_row.append(self._autostart_switch)
menu_box.append(autostart_row)
popover.set_child(menu_box)
menu_btn.set_popover(popover)
header.pack_end(menu_btn)
# ── Main content area ────────────────────────────────────
self._main_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=0,
)
# ── IP Address Banner ────────────────────────────────────
self._ip_bar = self._build_ip_bar()
self._main_box.append(self._ip_bar)
scrolled = Gtk.ScrolledWindow(
hscrollbar_policy=Gtk.PolicyType.NEVER,
vscrollbar_policy=Gtk.PolicyType.AUTOMATIC,
vexpand=True,
)
self._tiles_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=0,
)
scrolled.set_child(self._tiles_box)
self._main_box.append(scrolled)
toolbar_view = Adw.ToolbarView()
toolbar_view.add_top_bar(header)
toolbar_view.set_content(self._main_box)
self.set_content(toolbar_view)
self._build_tiles()
interval = config.get("refresh_interval", 5)
if interval and interval > 0:
GLib.timeout_add_seconds(interval, self._auto_refresh)
GLib.timeout_add_seconds(5, self._check_for_updates_once)
GLib.timeout_add_seconds(UPDATE_CHECK_INTERVAL, self._periodic_update_check)
GLib.timeout_add_seconds(1, self._fetch_ips_once)
# ── IP Address Bar ───────────────────────────────────────────
def _build_ip_bar(self):
bar = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=28,
halign=Gtk.Align.CENTER,
margin_top=14,
margin_bottom=6,
margin_start=24,
margin_end=24,
css_classes=["ip-bar"],
)
internal_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=8,
)
internal_icon = Gtk.Image(
icon_name="network-wired-symbolic",
pixel_size=18,
css_classes=["dim-label"],
)
internal_label = Gtk.Label(
label="Internal:",
css_classes=["dim-label"],
)
self._internal_ip_label = Gtk.Label(
label="…",
css_classes=["ip-value"],
selectable=True,
)
internal_box.append(internal_icon)
internal_box.append(internal_label)
internal_box.append(self._internal_ip_label)
sep = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
external_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=8,
)
external_icon = Gtk.Image(
icon_name="network-server-symbolic",
pixel_size=18,
css_classes=["dim-label"],
)
external_label = Gtk.Label(
label="External:",
css_classes=["dim-label"],
)
self._external_ip_label = Gtk.Label(
label="…",
css_classes=["ip-value"],
selectable=True,
)
external_box.append(external_icon)
external_box.append(external_label)
external_box.append(self._external_ip_label)
bar.append(internal_box)
bar.append(sep)
bar.append(external_box)
return bar
def _fetch_ips_once(self):
thread = threading.Thread(target=self._do_fetch_ips, daemon=True)
thread.start()
return False
def _do_fetch_ips(self):
internal = _get_internal_ip()
GLib.idle_add(self._internal_ip_label.set_label, internal)
external = _get_external_ip()
GLib.idle_add(self._external_ip_label.set_label, external)
# ── Title box ────────────────────────────────────────────────
def _build_title_box(self):
role = self._config.get("role", "server_plus_desktop")
role_label = ROLE_LABELS.get(role, role)
box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
halign=Gtk.Align.CENTER,
)
box.append(Gtk.Label(
label="Sovran_SystemsOS Hub",
css_classes=["hub-title"],
))
box.append(Gtk.Label(
label=role_label,
css_classes=["role-badge", "dim-label"],
))
return box
# ── Service tiles ────────────────────────────────────────────
def _build_tiles(self):
method = self._config.get("command_method", "systemctl")
services = self._config.get("services", [])
grouped = {}
for entry in services:
cat = entry.get("category", "other")
grouped.setdefault(cat, []).append(entry)
for cat_key, cat_label in CATEGORY_ORDER:
entries = grouped.get(cat_key, [])
if not entries:
continue
container = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
halign=Gtk.Align.CENTER,
css_classes=["tiles-container"],
)
container.set_size_request(TILE_GRID_WIDTH, -1)
section_label = Gtk.Label(
label=cat_label,
css_classes=["section-header"],
halign=Gtk.Align.START,
margin_top=24,
margin_bottom=6,
margin_start=16,
)
container.append(section_label)
sep = Gtk.Separator(
orientation=Gtk.Orientation.HORIZONTAL,
margin_start=16,
margin_end=16,
margin_bottom=10,
)
container.append(sep)
flowbox = Gtk.FlowBox(
max_children_per_line=4,
min_children_per_line=2,
selection_mode=Gtk.SelectionMode.NONE,
homogeneous=False,
row_spacing=14,
column_spacing=14,
margin_top=4,
margin_bottom=10,
margin_start=16,
margin_end=16,
halign=Gtk.Align.START,
valign=Gtk.Align.START,
)
for entry in entries:
tile = ServiceTile(
name=entry.get("name", entry["unit"]),
unit=entry["unit"],
scope=entry.get("type", "system"),
method=method,
icon_name=entry.get("icon", ""),
enabled=entry.get("enabled", True),
)
flowbox.append(tile)
self._tiles.append(tile)
container.append(flowbox)
self._tiles_box.append(container)
GLib.idle_add(self._refresh_all)
# ── Update check ─────────────────────────────────────────────
def _check_for_updates_once(self):
thread = threading.Thread(target=self._do_update_check, daemon=True)
thread.start()
return False
def _periodic_update_check(self):
thread = threading.Thread(target=self._do_update_check, daemon=True)
thread.start()
return True
def _do_update_check(self):
available = check_for_updates()
GLib.idle_add(self._set_update_indicator, available)
def _set_update_indicator(self, available):
self._update_available = available
if available:
self._update_btn.set_label("Update Available")
self._update_btn.set_css_classes(["update-available"])
self._update_btn.set_tooltip_text("A new version of Sovran_SystemsOS is available!")
self._badge.set_visible(True)
else:
self._update_btn.set_label("Update System")
self._update_btn.set_css_classes(["suggested-action"])
self._update_btn.set_tooltip_text("System is up to date")
self._badge.set_visible(False)
# ── Callbacks ────────────────────────────────────────────────
def _on_update_clicked(self, _btn):
dialog = UpdateDialog(self)
dialog.connect("close-request", lambda _w: self._after_update())
dialog.present()
def _after_update(self):
GLib.timeout_add_seconds(3, self._check_for_updates_once)
return False
def _on_autostart_toggled(self, switch, state):
set_autostart_enabled(state)
return False
def _refresh_all(self):
for t in self._tiles:
t.refresh()
return False
def _auto_refresh(self):
self._refresh_all()
return True
class SovranHubApp(Adw.Application):
def __init__(self):
super().__init__(
application_id=APP_ID,
flags=Gio.ApplicationFlags.DEFAULT_FLAGS,
)
self._config = load_config()
def do_activate(self):
win = self.get_active_window()
if not win:
win = SovranHubWindow(self, self._config)
win.present()
-20
View File
@@ -1,20 +0,0 @@
"""Load the Nix-generated config for Sovran_SystemsOS_Hub."""
import json
import os
def load_config() -> dict:
"""Read config from the path injected by the Nix derivation."""
path = os.environ.get(
"SOVRAN_HUB_CONFIG",
os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"config.json",
),
)
try:
with open(path, "r") as fh:
return json.load(fh)
except (FileNotFoundError, json.JSONDecodeError):
return {"refresh_interval": 5, "command_method": "systemctl", "services": []}
-104
View File
@@ -1,104 +0,0 @@
"""Adw.ActionRow subclass representing a single systemd unit."""
from __future__ import annotations
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, GLib, Gtk # noqa: E402
from . import systemctl # noqa: E402
LOADING_STATES = {"reloading", "activating", "deactivating", "maintenance"}
class ServiceRow(Adw.ActionRow):
"""A row showing one systemd unit with a toggle switch and action buttons."""
def __init__(
self,
name: str,
unit: str,
scope: str = "system",
method: str = "systemctl",
**kwargs,
):
super().__init__(title=name, subtitle=unit, **kwargs)
self._unit = unit
self._scope = scope
self._method = method
# ── Active / Inactive switch ──
self._switch = Gtk.Switch(valign=Gtk.Align.CENTER)
self._switch.connect("state-set", self._on_toggled)
self.add_suffix(self._switch)
self.set_activatable_widget(self._switch)
# ── Restart button ──
restart_btn = Gtk.Button(
icon_name="view-refresh-symbolic",
valign=Gtk.Align.CENTER,
tooltip_text="Restart",
css_classes=["flat"],
)
restart_btn.connect("clicked", self._on_restart)
self.add_suffix(restart_btn)
# ── Status pill ──
self._status_label = Gtk.Label(
css_classes=["caption", "dim-label"],
valign=Gtk.Align.CENTER,
margin_end=4,
)
self.add_suffix(self._status_label)
# Initial state
self.refresh()
# ── public API ──
def refresh(self):
"""Poll systemctl and update the row."""
active_state = systemctl.is_active(self._unit, self._scope)
enabled_state = systemctl.is_enabled(self._unit, self._scope)
is_active = active_state == "active"
is_loading = active_state in LOADING_STATES
is_failed = active_state == "failed"
# Block the handler so we don't trigger a start/stop when we
# programmatically flip the switch.
self._switch.handler_block_by_func(self._on_toggled)
self._switch.set_active(is_active)
self._switch.handler_unblock_by_func(self._on_toggled)
self._switch.set_sensitive(not is_loading)
# Status text
label = enabled_state
if is_failed:
label = "failed"
elif is_loading:
label = active_state
self._status_label.set_label(label)
# Visual cue for failures
if is_failed:
self.add_css_class("error")
else:
self.remove_css_class("error")
# ── signal handlers ──
def _on_toggled(self, switch: Gtk.Switch, state: bool) -> bool:
action = "start" if state else "stop"
systemctl.run_action(action, self._unit, self._scope, self._method)
# Delay refresh so systemd has a moment to change state
GLib.timeout_add(1500, self.refresh)
return False # let GTK update the switch position
def _on_restart(self, _btn: Gtk.Button):
systemctl.run_action("restart", self._unit, self._scope, self._method)
GLib.timeout_add(1500, self.refresh)
-172
View File
@@ -1,172 +0,0 @@
"""Square tile widget representing a single systemd unit."""
from __future__ import annotations
import os
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
gi.require_version("Gdk", "4.0")
from gi.repository import Gdk, GdkPixbuf, GLib, Gtk, Pango
from . import systemctl
LOADING_STATES = {"reloading", "activating", "deactivating", "maintenance"}
ICON_DIR = os.environ.get("SOVRAN_HUB_ICONS", "")
ICON_EXTENSIONS = [".svg", ".png"]
# ── Locked tile dimensions ───────────────────────────────────────
TILE_W = 180
TILE_H = 210
ICON_PX = 48
LABEL_W = TILE_W - 24 # 12px padding each side
class ServiceTile(Gtk.Box):
def __init__(self, name, unit, scope="system", method="systemctl",
icon_name="", enabled=True, **kw):
super().__init__(
orientation=Gtk.Orientation.VERTICAL,
spacing=2,
halign=Gtk.Align.CENTER,
valign=Gtk.Align.START,
css_classes=["card", "sovran-tile"],
**kw,
)
self.set_size_request(TILE_W, TILE_H)
self.set_hexpand(False)
self.set_vexpand(False)
self._unit = unit
self._scope = scope
self._method = method
self._enabled = enabled
# ── Icon ─────────────────────────────────────────────────
self._logo = Gtk.Image(
pixel_size=ICON_PX,
margin_top=18,
halign=Gtk.Align.CENTER,
)
self._set_logo(icon_name)
self.append(self._logo)
# ── Name label ───────────────────────────────────────────
self._name_label = Gtk.Label(
label=name,
css_classes=["tile-name"],
halign=Gtk.Align.CENTER,
justify=Gtk.Justification.CENTER,
wrap=True,
wrap_mode=Pango.WrapMode.WORD_CHAR,
lines=2,
ellipsize=Pango.EllipsizeMode.END,
margin_start=12,
margin_end=12,
margin_top=6,
)
self._name_label.set_size_request(LABEL_W, -1)
self._name_label.set_max_width_chars(1)
self.append(self._name_label)
# ── Status label ─────────────────────────────────────────
self._status_label = Gtk.Label(
label="● …",
css_classes=["caption", "tile-status", "dim-label"],
halign=Gtk.Align.CENTER,
margin_top=2,
)
self.append(self._status_label)
# ── Spacer ───────────────────────────────────────────────
spacer = Gtk.Box(vexpand=True)
self.append(spacer)
# ── Controls ───────────────���─────────────────────────────
controls = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=10,
halign=Gtk.Align.CENTER,
margin_bottom=14,
)
self._switch = Gtk.Switch(valign=Gtk.Align.CENTER)
self._switch.connect("state-set", self._on_toggled)
controls.append(self._switch)
restart_btn = Gtk.Button(
icon_name="view-refresh-symbolic",
valign=Gtk.Align.CENTER,
tooltip_text="Restart",
css_classes=["flat", "circular"],
)
restart_btn.connect("clicked", self._on_restart)
controls.append(restart_btn)
self.append(controls)
if not self._enabled:
self._switch.set_active(False)
self._switch.set_sensitive(False)
self._status_label.set_label("○ disabled")
self._status_label.set_css_classes(["caption", "tile-status", "disabled-label"])
self._logo.set_opacity(0.35)
self.set_tooltip_text(f"{name} is not enabled in custom.nix")
def _set_logo(self, icon_name):
if icon_name and ICON_DIR:
for ext in ICON_EXTENSIONS:
path = os.path.join(ICON_DIR, f"{icon_name}{ext}")
if os.path.isfile(path):
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
path, ICON_PX, ICON_PX, True)
texture = Gdk.Texture.new_for_pixbuf(pixbuf)
self._logo.set_from_paintable(texture)
return
except Exception:
break
self._logo.set_from_icon_name("system-run-symbolic")
def refresh(self):
if not self._enabled:
return
active = systemctl.is_active(self._unit, self._scope)
is_on = active == "active"
is_loading = active in LOADING_STATES
is_failed = active == "failed"
self._switch.handler_block_by_func(self._on_toggled)
self._switch.set_active(is_on)
self._switch.handler_unblock_by_func(self._on_toggled)
self._switch.set_sensitive(not is_loading)
if is_failed:
self._status_label.set_label("● failed")
self._status_label.set_css_classes(["caption", "tile-status", "error"])
elif is_on:
self._status_label.set_label("● running")
self._status_label.set_css_classes(["caption", "tile-status", "success"])
elif is_loading:
self._status_label.set_label(f"● {active}")
self._status_label.set_css_classes(["caption", "tile-status", "warning"])
else:
self._status_label.set_label(f"● {active}")
self._status_label.set_css_classes(["caption", "tile-status", "dim-label"])
def _on_toggled(self, switch, state):
if not self._enabled:
return True
systemctl.run_action("start" if state else "stop", self._unit, self._scope, self._method)
GLib.timeout_add(1500, self.refresh)
return False
def _on_restart(self, _btn):
if not self._enabled:
return
systemctl.run_action("restart", self._unit, self._scope, self._method)
GLib.timeout_add(1500, self.refresh)
-40
View File
@@ -1,40 +0,0 @@
"""Thin wrapper around the systemctl CLI for Sovran_SystemsOS_Hub."""
from __future__ import annotations
import subprocess
from typing import Literal
def _run(cmd: list[str]) -> str:
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
return result.stdout.strip()
except Exception:
return ""
def is_active(unit: str, scope: Literal["system", "user"] = "system") -> str:
return _run(["systemctl", f"--{scope}", "is-active", unit]) or "unknown"
def is_enabled(unit: str, scope: Literal["system", "user"] = "system") -> str:
return _run(["systemctl", f"--{scope}", "is-enabled", unit]) or "unknown"
def run_action(
action: str,
unit: str,
scope: Literal["system", "user"] = "system",
method: str = "systemctl",
) -> bool:
base_cmd = ["systemctl", f"--{scope}", action, unit]
if scope == "system" and method == "pkexec":
cmd = ["pkexec", "--user", "root"] + base_cmd
else:
cmd = base_cmd
try:
subprocess.Popen(cmd)
return True
except Exception:
return False
Whitespace-only changes.
+40
View File
@@ -0,0 +1,40 @@
"""Load the Nix-generated config and build-time versions for Sovran_SystemsOS_Hub."""
import json
import os
def load_config() -> dict:
"""Read config from the path injected by the Nix derivation."""
path = os.environ.get(
"SOVRAN_HUB_CONFIG",
os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"config.json",
),
)
try:
with open(path, "r") as fh:
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 {}
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""Sovran DDNS update runner (sovran-ddns-update.service).
For every stored Njal.la update URL this asks Njal.la to point the record at
the address the request came from ("&auto"), then reads back the address
Njal.la says it recorded. That address is saved to /var/lib/secrets/external-ip,
where livekit-turn-setup and the Hub read it.
Njal.la is the only party involved. It has to learn the address to publish
it, so nothing else -- no STUN server, no public resolver, no "what is my IP"
service -- is ever asked for it.
Kept from the previous runner:
* every URL goes through _validate_ddns_url() (https, njal.la only, /update/)
* curl is run directly: no shell, no redirects
* the update key is never printed or logged
The module is installed next to security_helpers.py (/etc/sovran/) and run as a
script by the service; it is also importable as
sovran_systemsos_web.ddns_update so the tests can exercise it.
"""
import ipaddress
import json
import os
import subprocess
import sys
import tempfile
try:
from .security_helpers import _validate_ddns_url # imported as part of the Hub package
except ImportError: # run as a script from /etc/sovran, next to security_helpers.py
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from security_helpers import _validate_ddns_url
URLS_FILE = "/var/lib/njalla/ddns_urls.json"
IP_FILE = "/var/lib/secrets/external-ip"
_CGNAT = ipaddress.ip_network("100.64.0.0/10")
def is_public_ipv4(value) -> bool:
"""True for a globally routable IPv4 literal (not private, loopback, CGNAT ...)."""
try:
ip = ipaddress.ip_address(str(value).strip())
except ValueError:
return False
if ip.version != 4 or ip in _CGNAT:
return False
# is_global alone is not enough: CPython reports multicast as global.
return ip.is_global and not (
ip.is_multicast or ip.is_reserved or ip.is_loopback
or ip.is_link_local or ip.is_unspecified or ip.is_private
)
def normalise_url(raw: str) -> str:
"""Return the URL to call for a stored (or freshly pasted) update URL.
* Older Hubs stored "...&a=${IP}": the address was looked up locally and
substituted. Njal.la can use the address the request came from, so that
placeholder becomes "&auto".
* "&quiet" is dropped: the reply is how we learn the address Njal.la recorded.
"""
return raw.replace("&a=${IP}", "&auto").replace("&quiet", "")
def parse_reply(body: str):
"""Return the public IPv4 address Njal.la says it recorded, or None.
A successful update replies with JSON of the form
{"status": 200, "message": "record updated", "value": {"A": "203.0.113.7", ...}}
"""
try:
data = json.loads(body)
except (TypeError, ValueError):
return None
if not isinstance(data, dict) or str(data.get("status")) != "200":
return None
value = data.get("value")
ip = value.get("A") if isinstance(value, dict) else None
return str(ip).strip() if is_public_ipv4(ip) else None
def read_ip_file(path: str = None):
"""The address recorded by the last successful update, or None."""
try:
with open(path or IP_FILE) as f:
return f.read().strip() or None
except OSError:
return None
def write_ip_file(ip: str, path: str = None) -> None:
"""Replace the file atomically so a path watcher never sees a partial write."""
path = path or IP_FILE
directory = os.path.dirname(path)
os.makedirs(directory, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=directory, prefix=".external-ip-")
try:
with os.fdopen(fd, "w") as f:
f.write(ip)
os.chmod(tmp, 0o644)
os.replace(tmp, path)
except BaseException:
try:
os.unlink(tmp)
except OSError:
pass
raise
def update_all(urls, *, run=None, validate=_validate_ddns_url):
"""Call every update URL once; return the address Njal.la reported, or None.
Nothing secret is printed: URLs are only ever referred to by position.
"""
run = run or subprocess.run # resolved per call so tests can substitute it
reported = None
seen = set()
todo = []
for raw in urls:
url = normalise_url(raw)
if url not in seen: # an old "&a=${IP}" entry and its "&auto" twin are one record
seen.add(url)
todo.append(url)
for number, url in enumerate(todo, 1):
try:
validate(url)
proc = run(
["curl", "--silent", "--ipv4", "--max-time", "15", "--fail", "--no-location", url],
capture_output=True, text=True, timeout=20, check=False,
)
except Exception:
print(f"DDNS update {number}/{len(todo)}: skipped (invalid URL or curl unavailable)")
continue
if proc.returncode != 0:
print(f"DDNS update {number}/{len(todo)}: failed (curl exit {proc.returncode})")
continue
ip = parse_reply(proc.stdout)
if ip is None:
print(f"DDNS update {number}/{len(todo)}: Njal.la did not report a public IPv4 address")
continue
print(f"DDNS update {number}/{len(todo)}: ok")
if reported is None:
reported = ip
elif ip != reported:
print("DDNS: Njal.la reported different addresses for different records; using the first")
return reported
def main() -> int:
try:
with open(URLS_FILE) as f:
urls = json.load(f)
if not isinstance(urls, list):
raise ValueError("not a list")
except Exception:
return 0 # no URLs configured -- nothing to do
urls = [u for u in urls if isinstance(u, str)]
if not urls:
return 0
ip = update_all(urls)
if ip is None:
print("DDNS: no address reported by Njal.la; keeping the last known one")
return 0
previous = read_ip_file()
if ip == previous:
print(f"DDNS: public IP unchanged ({ip})")
return 0
write_ip_file(ip)
print(f"DDNS: public IP is now {ip} (was {previous or 'unknown'})")
return 0
if __name__ == "__main__":
sys.exit(main())
+637
View File
@@ -0,0 +1,637 @@
#!/usr/bin/env bash
# ── Sovran Hub External Backup Script ────────────────────────────
# Backs up Sovran_SystemsOS data to an external USB hard drive using rsync.
# 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).
#
# Usage:
# BACKUP_TARGET=/run/media/<user>/<drive> bash sovran-hub-backup.sh
# (or run with no env var to auto-detect the first external USB drive)
set -euo pipefail
BACKUP_LOG="/var/log/sovran-hub-backup.log"
BACKUP_STATUS="/var/log/sovran-hub-backup.status"
MEDIA_ROOT="/run/media"
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"
# ── Logging helpers ──────────────────────────────────────────────
log() {
local msg
msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$BACKUP_LOG"
}
set_status() {
echo "$1" > "$BACKUP_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"
for internal in "${INTERNAL_MOUNTS[@]}"; do
if [[ "$mnt" == "$internal" || "$mnt" == "${internal}/"* ]]; then
return 0
fi
done
return 1
}
# ── Use lsblk to find the first genuine external USB drive ───────
find_external_drive() {
local target=""
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
local skip=0
for lbl in "${INTERNAL_LABELS[@]}"; do
[[ "$label" == "$lbl" ]] && skip=1 && break
done
[[ "$skip" -eq 1 ]] && 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 "
import sys, json
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([
d.get('type') or '',
str(d.get('hotplug') or '0'),
str(d.get('rm') or '0'),
d.get('label') or '',
d.get('mountpoint') or '',
]))
" 2>/dev/null || true)
if [[ -z "$target" && -d "$MEDIA_ROOT" ]]; then
while IFS= read -r -d '' mnt; do
is_internal "$mnt" && continue
if mountpoint -q "$mnt" 2>/dev/null; then
target="$mnt"
break
fi
done < <(find "$MEDIA_ROOT" -mindepth 2 -maxdepth 2 -type d -print0 2>/dev/null)
fi
echo "$target"
}
# ── Detect the configured system role ───────────────────────────
detect_role() {
local role="server_plus_desktop"
if [[ -f "$HUB_CONFIG_JSON" ]] && command -v python3 &>/dev/null; then
local r
r=$(python3 -c \
"import json,sys; d=json.load(open(sys.argv[1])); print(d.get('role',''))" \
"$HUB_CONFIG_JSON" 2>/dev/null || true)
if [[ -n "$r" ]]; then
echo "$r"
return
fi
fi
if [[ -f "$ROLE_STATE_NIX" ]]; then
if grep -q 'roles\.desktop = lib\.mkDefault true' "$ROLE_STATE_NIX" 2>/dev/null; then
role="desktop"
elif grep -q 'roles\.node = lib\.mkDefault true' "$ROLE_STATE_NIX" 2>/dev/null; then
role="node"
fi
fi
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"
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" ;;
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"
if is_internal "$TARGET"; then
fail "Target '$TARGET' is an internal system drive and cannot be used for external backup."
fi
log "Using specified backup target: $TARGET"
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."
fi
log "Detected external drive: $TARGET"
fi
validate_target_mount "$TARGET"
# ── Set up stable backup destination ────────────────────────────
# Subsequent runs update the same mirror, transferring only new or changed files.
BACKUP_DIR="${TARGET}/${BACKUP_SUBPATH}"
mkdir -p -- "$BACKUP_DIR"
# 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"
# 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"
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 ────────────────────────────────
log ""
log "── Stage 1/${TOTAL_STAGES}: NixOS configuration (/etc/nixos) ──────────────"
if [[ -d /etc/nixos ]]; then
sync_tree "/etc/nixos" no /etc/nixos/ "$BACKUP_DIR/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.
log ""
log "── Stage ${HOME_STAGE_NUM}/${TOTAL_STAGES}: Home directory (/home) ───────────────────────"
if [[ -d /home ]]; then
sync_tree "/home" yes /home/ "$BACKUP_DIR/home/" \
--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='.config/BraveSoftware/Brave-Origin/*/Cache/' \
--exclude='.config/BraveSoftware/Brave-Origin/*/Code Cache/' \
--exclude='.local/share/baloo/' \
--exclude='.thumbnails/' \
--exclude='.xsession-errors' \
--exclude='.xsession-errors.old'
log "Stage ${HOME_STAGE_NUM} 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.
if [[ "$ROLE" != "desktop" ]]; then
log ""
log "── Stage 4/${TOTAL_STAGES}: System data (/var/lib) ───────────────────────"
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/' \
--exclude='*/logs/' \
--exclude='*/cache/' \
--exclude='*/tmp/'
log "Stage 4 complete."
else
log "WARNING: /var/lib not found — skipping."
fi
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 "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"
# ── 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 "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"
@@ -0,0 +1,526 @@
"""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 threading
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
# ── Login throttling ──────────────────────────────────────────────────────────
#
# Delays applied after each failed login, and the lockout that follows once an
# address has tripped LOGIN_FAIL_MAX inside the window.
#
# LOGIN_FAIL_WINDOW has to be longer than the time it takes to reach
# LOGIN_FAIL_MAX failures under the ramping delay: with a 2s ramp capped at
# LOGIN_FAIL_MAX_DELAY, 10 attempts take about 80 seconds, so a 60 second
# window would silently expire the earliest failures and the counter could
# never reach the limit. 900s (15 minutes) keeps the whole ramp inside it.
LOGIN_FAIL_DELAY = 2.0 # base delay; the nth failure waits n x this
LOGIN_FAIL_MAX_DELAY = 10.0 # ceiling for a single delay
LOGIN_FAIL_WINDOW = 900.0 # rolling window failures are counted in
LOGIN_FAIL_MAX = 10 # failures in the window that trigger a lockout
LOGIN_LOCKOUT_SECONDS = 300.0 # how long the lockout lasts
# Cap on how many addresses are tracked, so a distributed sweep cannot grow
# the table without bound.
_LOGIN_THROTTLE_MAX_IPS = 4096
class LoginThrottle:
"""Per-address failed-login tracking with a ramping delay and a lockout.
The delay ramps so a script hammering the login form slows down as it goes,
and once LOGIN_FAIL_MAX failures land inside the window the address is
refused outright for LOGIN_LOCKOUT_SECONDS. A successful login clears the
address so a legitimate user who fumbles a password is not penalised later.
``sleep`` and ``clock`` are injectable so tests run without waiting.
"""
def __init__(
self,
fail_delay=LOGIN_FAIL_DELAY,
max_delay=LOGIN_FAIL_MAX_DELAY,
window=LOGIN_FAIL_WINDOW,
max_failures=LOGIN_FAIL_MAX,
lockout=LOGIN_LOCKOUT_SECONDS,
max_tracked_ips=_LOGIN_THROTTLE_MAX_IPS,
sleep=None,
clock=None,
):
self._fail_delay = float(fail_delay)
self._max_delay = float(max_delay)
self._window = float(window)
self._max_failures = int(max_failures)
self._lockout = float(lockout)
self._max_tracked_ips = int(max_tracked_ips)
self._sleep = sleep if sleep is not None else time.sleep
self._clock = clock if clock is not None else time.monotonic
self._lock = threading.Lock()
self._failures: dict[str, list[float]] = {}
# ── internals ────────────────────────────────────────────────────────────
def _prune(self, ip, now):
"""Drop timestamps outside the window; return what is left."""
keep = [t for t in self._failures.get(ip, ()) if now - t < self._window]
if keep:
self._failures[ip] = keep
else:
self._failures.pop(ip, None)
return keep
def _evict(self, now):
"""Forget addresses that can no longer affect anything."""
horizon = max(self._window, self._lockout)
for ip in [i for i, ts in self._failures.items()
if ts and now - max(ts) > horizon]:
self._failures.pop(ip, None)
while len(self._failures) > self._max_tracked_ips:
oldest = min(self._failures, key=lambda i: max(self._failures[i]))
self._failures.pop(oldest, None)
# ── public API ───────────────────────────────────────────────────────────
def delay_for(self, count):
"""Return the delay owed after *count* failures in the current window."""
if count <= 0:
return 0.0
return min(self._fail_delay * count, self._max_delay)
def failure_count(self, ip):
"""Return the failures currently counted against *ip*."""
with self._lock:
return len(self._prune(ip, self._clock()))
def is_locked_out(self, ip):
"""Return True while *ip* is inside a lockout."""
now = self._clock()
with self._lock:
failures = self._prune(ip, now)
if len(failures) < self._max_failures:
return False
return (now - failures[-1]) < self._lockout
def remaining_lockout(self, ip):
"""Return the seconds left in *ip*'s lockout, or 0.0 if not locked out."""
now = self._clock()
with self._lock:
failures = self._prune(ip, now)
if len(failures) < self._max_failures:
return 0.0
return max(0.0, self._lockout - (now - failures[-1]))
def record_failure(self, ip):
"""Record a failure for *ip* and serve out the delay it has earned.
Returns the delay that was applied. The lock is never held across the
sleep, so one slow client cannot stall every other login.
"""
now = self._clock()
with self._lock:
failures = list(self._prune(ip, now))
failures.append(now)
self._failures[ip] = failures
count = len(failures)
self._evict(now)
delay = self.delay_for(count)
if delay > 0:
self._sleep(delay)
return delay
def clear(self, ip):
"""Forget *ip*, e.g. after a successful login."""
with self._lock:
self._failures.pop(ip, None)
def tracked_addresses(self):
"""Return how many addresses are currently being tracked."""
with self._lock:
return len(self._failures)
# ── Local-network client policy ───────────────────────────────────────────────
#
# The Hub runs as root: it can display stored credentials, reboot the machine
# and rebuild the system. It answers this computer and the local network and
# nobody else. Whether a packet may reach its port is the firewall's and the
# router's business; this is the second lock, applied by the application itself
# so that a port forward, a firewall mistake, or a machine that has a public
# address does not put the login page in front of the internet.
#
# The Hub listens on IPv4 only (see sovran-hub.nix), so IPv6 clients never
# reach it directly and the IPv6 ranges below only matter if that bind is ever
# widened. Global IPv6 addresses (2000::/3) are deliberately not listed: a
# global address belonging to a laptop on the LAN cannot be told apart from a
# stranger's by the address alone, and allowing the range would let the whole
# IPv6 internet through.
LAN_ONLY_IPV4 = (
"127.0.0.0/8", # this computer
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"100.64.0.0/10", # Tailscale and other VPN/CGNAT ranges
"169.254.0.0/16", # link-local
)
LAN_ONLY_IPV6 = (
"::1/128",
"fc00::/7", # unique-local (covers fd00::/8)
"fe80::/10", # link-local
)
class LanPolicy:
"""Decides whether a client address counts as local.
``extra_networks`` are CIDR blocks an operator has declared local in
addition to the built-in ranges, of either address family. ``enabled=False``
turns the check off entirely; it is the one explicit way to do that.
"""
def __init__(self, extra_networks=(), enabled=True):
self.enabled = bool(enabled)
nets = [ipaddress.ip_network(c, strict=False)
for c in LAN_ONLY_IPV4 + LAN_ONLY_IPV6]
for cidr in (extra_networks or ()):
try:
net = ipaddress.ip_network(cidr, strict=False)
except ValueError:
# A malformed entry must never widen the policy. Ignore it and
# stay at the strictest interpretation.
continue
if net.prefixlen == 0:
# 0.0.0.0/0 and ::/0 are "everyone". That is lan_only = false,
# and it should be asked for by name, not arrive as a "network".
continue
nets.append(net)
self._nets = tuple(nets)
def allows(self, ip):
"""Return True if *ip* may reach the service."""
if not self.enabled:
return True
if not ip:
return False
try:
addr = ipaddress.ip_address(ip)
except ValueError:
return False
# A dual-stack socket reports IPv4 clients as ::ffff:a.b.c.d. The
# address that matters is the IPv4 one inside it.
if addr.version == 6 and addr.ipv4_mapped is not None:
addr = addr.ipv4_mapped
return any(addr.version == n.version and addr in n for n in self._nets)
@property
def networks(self):
return self._nets
# ── 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
@@ -0,0 +1,197 @@
/* Sovran_SystemsOS Hub — Web UI Stylesheet
The Hub redesign — softer dark theme, GNOME 50 / libadwaita surfaces,
Sovran green reserved for status and actions.
Design tokens are defined once here. Legacy variable names from the
previous theme are aliased to the new values so every stylesheet
(support, security, domain-setup, onboarding, …) re-skins without
needing per-rule edits. */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
color-scheme: dark;
/* ── The Hub palette ─────────────────────────────────────────── */
--bg: #17191d;
--surface: #1c1f24;
--card: #23272c;
--card-hover: #292e34;
--elevated: #26292e;
--inset: #121417;
--border: rgba(255, 255, 255, 0.07);
--border-strong: rgba(255, 255, 255, 0.14);
--text: #e9edec;
--text-2: #a9b0b3;
--text-3: #7a8388;
--accent: #3ecf8e;
--accent-strong: #42f39a;
--accent-deep: #1aa45d;
--accent-dim: rgba(66, 243, 154, 0.12);
--amber: #e9b64a;
--red: #f66151;
--blue: #78aeed;
--mono: 'JetBrains Mono', 'Fira Code', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
--radius-card: 20px;
--radius-dialog: 26px;
--shadow-card: 0 10px 30px rgba(0, 0, 0, 0.25);
--shadow-hover: 0 16px 36px rgba(0, 0, 0, 0.38);
--shadow-pop: 0 30px 80px rgba(0, 0, 0, 0.5);
/* ── Legacy aliases (previous theme) — keep every old sheet working */
--bg-color: var(--bg);
--surface-color: var(--surface);
--card-color: var(--card);
--border-color: var(--border);
--text-primary: var(--text);
--text-secondary: var(--text-2);
--text-dim: var(--text-3);
--accent-color: var(--accent);
--green: var(--accent);
--yellow: var(--amber);
--red: var(--red);
--grey: var(--text-3);
--radius-btn: 12px;
}
html, body {
height: 100%;
}
body {
font-family: Inter, Cantarell, 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 15px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
overflow: hidden;
}
button { font-family: inherit; }
::selection { background: rgba(66, 243, 154, 0.25); }
/* ── Scrollbars ─────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.10); border-radius: 99px; border: 2px solid transparent; background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.18); background-clip: padding-box; }
::-webkit-scrollbar-track { background: transparent; }
/* ── Login page ─────────────────────────────────────────────────── */
.login-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 24px;
position: relative;
z-index: 1;
}
.login-card {
width: 100%;
max-width: 402px;
text-align: center;
background: var(--elevated);
border: 1px solid var(--border-strong);
border-radius: 28px;
padding: 46px 42px 38px;
box-shadow: var(--shadow-pop);
}
.login-header {
text-align: center;
margin-bottom: 26px;
}
.login-logo {
width: 78px;
height: 78px;
margin-bottom: 18px;
filter: drop-shadow(0 8px 24px rgba(28, 196, 120, 0.3));
}
.login-title {
font-size: 1.3rem;
font-weight: 800;
letter-spacing: -0.01em;
color: var(--text);
}
.login-title em { font-style: normal; color: var(--accent); }
.login-sub {
color: var(--text-2);
font-size: 0.85rem;
margin: 7px 0 0;
}
.login-form {
display: flex;
flex-direction: column;
gap: 14px;
}
.form-group label {
display: block;
font-size: 0.8rem;
font-weight: 650;
color: var(--text-2);
margin-bottom: 6px;
text-align: left;
}
.form-group input {
width: 100%;
padding: 11px 16px;
border: 1px solid var(--border-strong);
border-radius: 14px;
background: var(--inset);
color: var(--text);
font: inherit;
font-size: 0.92rem;
outline: 0;
transition: border-color 0.15s, box-shadow 0.15s;
}
.form-group input:focus {
border-color: rgba(66, 243, 154, 0.55);
box-shadow: 0 0 0 3px rgba(66, 243, 154, 0.13);
}
.btn-login {
width: 100%;
padding: 12px;
border-radius: 99px;
background: linear-gradient(180deg, #3fd68e, #1ea263);
color: #04220f;
font-size: 0.92rem;
font-weight: 700;
margin-top: 8px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 6px 18px rgba(35, 199, 124, 0.22);
}
.btn-login:hover:not(:disabled) {
filter: brightness(1.08);
}
.login-error {
background: rgba(246, 97, 81, 0.10);
border: 1px solid rgba(246, 97, 81, 0.35);
color: #f8a39b;
padding: 10px 14px;
border-radius: 12px;
font-size: 0.85rem;
display: none;
}
.login-error.visible {
display: block;
}
@@ -0,0 +1,186 @@
/* ── Buttons ────────────────────────────────────────────────────── */
button {
font-family: inherit;
cursor: pointer;
border: none;
outline: none;
transition: background 0.16s, border-color 0.16s, color 0.16s, transform 0.16s, filter 0.16s, opacity 0.15s;
}
button:disabled {
opacity: 0.55;
cursor: default;
pointer-events: none;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: 99px;
padding: 8px 18px;
font-size: 0.84rem;
font-weight: 700;
letter-spacing: 0.01em;
cursor: pointer;
border: 1px solid transparent;
white-space: nowrap;
}
.btn svg { width: 15px; height: 15px; }
.btn:active { transform: scale(0.97); }
.btn-primary {
background: linear-gradient(180deg, #3fd68e, #1ea263);
color: #04220f;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 6px 18px rgba(35, 199, 124, 0.22);
}
.btn-primary:hover:not(:disabled) {
filter: brightness(1.08);
transform: translateY(-1px);
}
.btn-primary:active:not(:disabled) { transform: scale(0.97); }
/* Ghost — the default secondary action (legacy: btn-close-modal / btn-save) */
.btn-ghost,
.btn-close-modal,
.btn-save {
background: transparent;
border-color: var(--border-strong);
color: var(--text-2);
}
.btn-ghost:hover:not(:disabled),
.btn-close-modal:hover:not(:disabled),
.btn-save:hover:not(:disabled) {
color: var(--text);
border-color: rgba(255, 255, 255, 0.26);
background: rgba(255, 255, 255, 0.04);
}
/* Blue — restart / retry actions (amber reads as an error) */
.btn-amber,
.btn-reboot,
.btn-restart-amber,
.btn-warning {
background: rgba(120, 174, 237, 0.10);
border-color: rgba(120, 174, 237, 0.35);
color: #a5cbf2;
}
.btn-amber:hover:not(:disabled),
.btn-reboot:hover:not(:disabled),
.btn-restart-amber:hover:not(:disabled),
.btn-warning:hover:not(:disabled) {
background: rgba(120, 174, 237, 0.18);
border-color: rgba(120, 174, 237, 0.55);
color: #c2dcf8;
}
.btn-danger {
background: rgba(246, 97, 81, 0.10);
border-color: rgba(246, 97, 81, 0.35);
color: #f8a39b;
}
.btn-danger:hover:not(:disabled) {
background: rgba(246, 97, 81, 0.18);
border-color: rgba(246, 97, 81, 0.5);
}
.btn-sm { padding: 6px 14px; font-size: 0.77rem; }
/* ── Header / topbar buttons ────────────────────────────────────── */
.btn-header-reboot {
background: rgba(120, 174, 237, 0.10);
border: 1px solid rgba(120, 174, 237, 0.35);
color: #a5cbf2;
font-size: 0.77rem;
font-weight: 700;
padding: 6px 14px;
border-radius: 99px;
}
.btn-header-reboot:hover:not(:disabled) {
background: rgba(120, 174, 237, 0.18);
border-color: rgba(120, 174, 237, 0.55);
}
.btn-logout {
background: transparent;
border: 1px solid var(--border-strong);
color: var(--text-2);
font-size: 0.77rem;
font-weight: 700;
padding: 6px 14px;
border-radius: 99px;
}
.btn-logout:hover {
color: var(--text);
border-color: rgba(255, 255, 255, 0.26);
background: rgba(255, 255, 255, 0.04);
}
/* ── Update System button (sidebar) ─────────────────────────────── */
.btn-update {
background: transparent;
border: 1px solid var(--border-strong);
color: var(--text-2);
position: relative;
display: flex;
align-items: center;
gap: 8px;
}
.btn-update:hover:not(:disabled) {
color: var(--text);
border-color: rgba(255, 255, 255, 0.26);
background: rgba(255, 255, 255, 0.04);
}
.btn-update.has-updates {
background: rgba(66, 243, 154, 0.12);
border-color: rgba(66, 243, 154, 0.4);
color: var(--accent);
}
.update-badge {
display: none;
width: 10px;
height: 10px;
background-color: var(--amber);
border-radius: 50%;
animation: pulse-badge 1.4s ease-in-out infinite;
}
.update-badge.visible {
display: inline-block;
}
@keyframes pulse-badge {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.35); }
}
/* ── Icon buttons ───────────────────────────────────────────────── */
.btn-icon {
background: none;
color: var(--text-2);
padding: 6px;
border-radius: 50%;
font-size: 1.1rem;
line-height: 1;
}
.btn-icon:hover:not(:disabled) {
background-color: rgba(255, 255, 255, 0.06);
color: var(--text);
}
@@ -0,0 +1,237 @@
/* ── Domain setup modal ──────────────────────────────────────────── */
domain-narrow-dialog {
max-width: 500px;
}
domain-field-group {
margin-bottom: 14px;
}
domain-field-label {
display: block;
font-size: 0.82rem;
color: var(--text-secondary);
margin-bottom: 6px;
font-weight: 600;
}
domain-field-input {
width: 100%;
background-color: var(--inset);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 10px 12px;
font-size: 0.9rem;
box-sizing: border-box;
}
domain-field-input:focus {
outline: none;
border-color: var(--accent-color);
}
domain-field-actions {
display: flex;
gap: 10px;
margin-top: 18px;
justify-content: flex-end;
}
/* ── Port Requirements modal ─────────────────────────────────────── */
.domain-narrow-dialog {
max-width: 500px;
}
.domain-setup-intro {
font-size: 0.88rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 16px;
}
.domain-setup-intro ol {
padding-left: 20px;
margin-top: 8px;
}
.domain-setup-intro li {
margin-bottom: 6px;
}
.domain-field-group {
margin-bottom: 14px;
}
.domain-field-label {
display: block;
font-size: 0.82rem;
color: var(--text-secondary);
margin-bottom: 6px;
font-weight: 600;
}
.domain-field-input {
width: 100%;
background-color: var(--inset);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 10px 12px;
font-size: 0.9rem;
box-sizing: border-box;
}
.domain-field-input:focus {
outline: none;
border-color: var(--accent-color);
}
.domain-field-hint {
font-size: 0.75rem;
color: var(--text-dim);
margin-top: 4px;
font-style: italic;
}
.domain-field-actions {
display: flex;
gap: 10px;
margin-top: 18px;
justify-content: flex-end;
}
.port-req-intro {
font-size: 0.88rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 10px;
}
.port-req-hint {
font-size: 0.82rem;
color: var(--text-dim);
line-height: 1.6;
margin-top: 10px;
margin-bottom: 10px;
}
.port-req-internal-ip {
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-weight: 700;
color: var(--accent-color);
}
.port-req-table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
margin-top: 8px;
margin-bottom: 8px;
}
.port-req-table th {
text-align: left;
color: var(--text-dim);
font-weight: 600;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 6px 10px;
border-bottom: 1px solid var(--border-color);
}
.port-req-table td {
padding: 8px 10px;
border-bottom: 1px solid rgba(30, 45, 39, 0.5);
color: var(--text-primary);
}
.port-req-table tr:last-child td {
border-bottom: none;
}
.port-req-port {
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-weight: 600;
color: var(--accent-color);
}
.port-req-proto {
text-transform: uppercase;
color: var(--text-secondary);
}
.port-req-desc {
color: var(--text-secondary);
}
.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: var(--accent);
background: rgba(66, 243, 154, 0.12);
border: 1px solid rgba(66, 243, 154, 0.4);
}
.port-proto-badge--udp {
color: var(--blue);
background: rgba(106, 169, 224, 0.12);
border: 1px solid rgba(106, 169, 224, 0.4);
}
.port-proto-badge--both {
color: var(--amber);
background: rgba(233, 182, 74, 0.12);
border: 1px solid rgba(233, 182, 74, 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, var(--amber));
border-radius: 8px;
font-size: 0.88rem;
line-height: 1.6;
}
@@ -0,0 +1,196 @@
/* ── Sidebar: Feature Manager + Preferences ─────────────────────── */
.category-section.autolaunch-section,
.category-section.feature-manager-section {
margin-bottom: 0;
}
/* The old "Preferences" header + hr becomes a small sidebar label.
features.js renders: .section-header + hr.section-divider + cards */
.autolaunch-section .section-header,
.feature-manager-section .section-header {
font-size: 0.66rem;
font-weight: 750;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-3);
margin: 0;
padding: 14px 10px 7px;
display: block;
}
.autolaunch-section .section-divider,
.feature-manager-section .section-divider {
background: none;
height: 0;
margin: 0;
}
.feature-cards-wrap { display: flex; flex-direction: column; }
.feature-subcategory { display: flex; flex-direction: column; }
.feature-subcategory-header {
font-size: 0.66rem;
font-weight: 750;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-3);
padding: 12px 10px 6px;
}
/* Feature card — sidebar preference row with a switch */
.feature-card {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 9px 10px;
border-radius: 10px;
transition: background 0.15s;
}
.feature-card:hover { background: rgba(255, 255, 255, 0.05); }
.feature-card-top {
display: flex;
align-items: flex-start;
gap: 10px;
width: 100%;
}
.feature-card-info { min-width: 0; flex: 1; }
.feature-card-name {
font-size: 0.84rem;
font-weight: 600;
color: var(--text);
line-height: 1.3;
}
.feature-card-desc {
margin-top: 2px;
font-size: 0.68rem;
color: var(--text-3);
line-height: 1.45;
}
.feature-card-status {
font-size: 0.66rem;
font-weight: 700;
padding: 2px 8px;
border-radius: 99px;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
color: var(--text-2);
}
.feature-card-status.status-on {
color: var(--accent);
background: var(--accent-dim);
border-color: rgba(66, 243, 154, 0.3);
}
.feature-conflict-warning {
margin-top: 8px;
font-size: 0.68rem;
color: var(--amber);
line-height: 1.5;
}
/* Domain badges inside feature cards */
.feature-domain-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.64rem;
font-weight: 700;
padding: 2px 8px;
border-radius: 99px;
margin-top: 6px;
}
.feature-domain-badge.configured {
color: var(--accent);
background: var(--accent-dim);
}
.feature-domain-badge.not-configured {
color: var(--amber);
background: rgba(233, 182, 74, 0.10);
}
.feature-domain-icon { font-size: 0.8rem; }
.feature-domain-label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 0.66rem;
font-weight: 650;
margin-top: 5px;
color: var(--text-3);
}
.feature-domain-label--ok { color: var(--accent); }
.feature-domain-label--warn { color: var(--amber); }
.feature-domain-label--error { color: var(--red); }
.feature-domain-label--checking { color: var(--blue); }
/* ── Toggle switch (libadwaita style) ───────────────────────────── */
.feature-toggle {
position: relative;
display: inline-block;
width: 46px;
height: 26px;
flex-shrink: 0;
margin-left: auto;
margin-top: 2px;
cursor: pointer;
}
.feature-toggle-input {
position: absolute;
inset: 0;
opacity: 0;
margin: 0;
cursor: pointer;
}
.feature-toggle-slider {
position: absolute;
inset: 0;
border-radius: 99px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid var(--border-strong);
transition: 0.2s;
pointer-events: none;
}
.feature-toggle-slider::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: 50%;
background: #cfd8d2;
transition: 0.2s cubic-bezier(0.3, 0.8, 0.4, 1.2);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.feature-toggle-input:checked + .feature-toggle-slider {
background: var(--accent);
border-color: transparent;
}
.feature-toggle-input:checked + .feature-toggle-slider::after {
left: 23px;
background: #fff;
}
.feature-toggle-input:focus-visible + .feature-toggle-slider {
box-shadow: 0 0 0 3px rgba(66, 243, 154, 0.3);
}
@@ -0,0 +1,107 @@
/* ── Topbar (replaces the old header-bar + ip-bar) ──────────────── */
.topbar {
display: flex;
align-items: center;
gap: 14px;
padding: 13px 26px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
position: relative;
z-index: 5;
}
.page-title {
font-size: 1.02rem;
font-weight: 800;
letter-spacing: -0.01em;
white-space: nowrap;
}
/* ── Search ─────────────────────────────────────────────────────── */
.search-box {
display: flex;
align-items: center;
gap: 9px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 99px;
padding: 8px 16px;
flex: 1;
max-width: 400px;
min-width: 140px;
margin-left: 18px;
color: var(--text-3);
transition: border-color 0.15s, box-shadow 0.15s;
}
.search-box:focus-within {
border-color: rgba(66, 243, 154, 0.35);
box-shadow: 0 0 0 3px rgba(66, 243, 154, 0.09);
}
.search-box svg { width: 15px; height: 15px; flex-shrink: 0; }
.search-box input {
background: none;
border: 0;
outline: 0;
color: var(--text);
width: 100%;
font: inherit;
font-size: 0.87rem;
}
.search-box input::placeholder { color: var(--text-3); }
.top-actions {
display: flex;
gap: 9px;
margin-left: auto;
order: 5;
}
/* ── Legacy badges (still used inside dialogs) ──────────────────── */
.role-badge {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-strong);
color: var(--text-2);
font-size: 0.72rem;
font-weight: 750;
padding: 6px 12px;
border-radius: 99px;
letter-spacing: 0.02em;
}
.os-version-badge {
background: rgba(255, 255, 255, 0.05);
color: var(--text-2);
font-size: 0.66rem;
font-weight: 600;
padding: 2px 9px;
border-radius: 99px;
border: 1px solid var(--border);
letter-spacing: 0.02em;
display: inline-flex;
align-items: center;
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 480px) {
.btn-header-reboot {
padding: 5px 10px;
font-size: 0.72rem;
}
.btn-logout {
padding: 5px 10px;
font-size: 0.72rem;
}
}
@@ -0,0 +1,668 @@
/* ── App shell: sidebar + main column ───────────────────────────── */
.app {
display: flex;
height: 100vh;
position: relative;
z-index: 1;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.content {
flex: 1;
overflow-y: auto;
padding: 26px 30px 70px;
min-width: 0;
}
/* ── Sidebar ────────────────────────────────────────────────────── */
.sidebar {
width: 250px;
flex-shrink: 0;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 20px 14px 14px;
overflow-y: auto;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
padding: 2px 8px 20px;
}
.brand-logo {
width: 42px;
height: 42px;
flex-shrink: 0;
filter: drop-shadow(0 4px 14px rgba(28, 196, 120, 0.25));
}
.brand-text { display: flex; flex-direction: column; min-width: 0; }
.brand-title {
font-size: 1.02rem;
font-weight: 800;
letter-spacing: -0.01em;
line-height: 1.2;
}
.brand-title em { font-style: normal; color: var(--accent); }
.brand-sub {
font-size: 0.7rem;
color: var(--text-3);
font-family: var(--mono);
margin-top: 3px;
}
.nav-label {
font-size: 0.66rem;
font-weight: 750;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-3);
padding: 14px 10px 7px;
}
/* Category nav (dashboard.js) */
.nav-item {
display: flex;
align-items: center;
gap: 11px;
width: 100%;
padding: 8px 10px;
border-radius: 10px;
border: 0;
background: none;
color: var(--text-2);
font-size: 0.87rem;
font-weight: 600;
cursor: pointer;
text-align: left;
transition: background 0.15s, color 0.15s;
}
.nav-item svg { width: 17px; height: 17px; flex-shrink: 0; }
.nav-item:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--text);
}
.nav-item.active {
background: rgba(255, 255, 255, 0.06);
color: var(--text);
box-shadow: inset 2.5px 0 0 var(--accent);
}
.nav-text {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-count {
margin-left: auto;
font-size: 0.67rem;
font-weight: 750;
flex-shrink: 0;
color: var(--text-3);
background: rgba(255, 255, 255, 0.05);
padding: 2px 8px;
border-radius: 99px;
}
.nav-item.active .nav-count {
background: rgba(255, 255, 255, 0.10);
color: var(--text-2);
}
/* System items (tiles.js renders these as .sidebar-support-btn) */
.sidebar-support-btn {
display: flex;
align-items: center;
gap: 11px;
width: 100%;
padding: 8px 10px;
border-radius: 10px;
border: 0;
background: none;
color: var(--text-2);
font-size: 0.87rem;
font-weight: 600;
cursor: pointer;
text-align: left;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.sidebar-support-btn:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--text);
}
.sidebar-support-btn + .sidebar-support-btn {
margin-top: 2px;
}
.sidebar-support-icon {
font-size: 1rem;
width: 17px;
height: 17px;
display: grid;
place-items: center;
flex-shrink: 0;
}
.sidebar-support-icon svg { width: 17px; height: 17px; }
.sidebar-support-text {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.sidebar-support-title {
font-size: 0.87rem;
font-weight: 600;
line-height: 1.3;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-support-hint {
font-size: 0.68rem;
font-weight: 650;
color: var(--text-3);
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-divider {
border: none;
border-top: 1px solid var(--border);
margin: 14px 0 0;
}
.sidebar-spacer { flex: 1; min-height: 18px; }
/* ── Welcome dashboard (default view) ───────────────────────────── */
.welcome {
position: relative;
/* Full-bleed: cancel .content's padding so the glow reaches
every edge of the panel (sidebar border, topbar, viewport). */
margin: -26px -30px -70px;
padding: 70px 38px 70px;
overflow: hidden;
min-height: calc(100% + 96px);
}
/* Soft drifting glow — calm, GPU-cheap (transforms only) */
.welcome-bg {
position: absolute;
inset: 0;
pointer-events: none;
}
.orb {
position: absolute;
border-radius: 50%;
will-change: transform;
}
.orb-a {
width: 560px; height: 560px;
left: -140px; top: -180px;
background: radial-gradient(circle, rgba(62, 207, 142, 0.10), transparent 70%);
animation: orb-drift-a 26s ease-in-out infinite alternate;
}
.orb-b {
width: 680px; height: 680px;
right: -200px; top: 20%;
background: radial-gradient(circle, rgba(120, 174, 237, 0.07), transparent 70%);
animation: orb-drift-b 34s ease-in-out infinite alternate;
}
.orb-c {
width: 460px; height: 460px;
left: 34%; bottom: -200px;
background: radial-gradient(circle, rgba(66, 243, 154, 0.06), transparent 70%);
animation: orb-drift-c 42s ease-in-out infinite alternate;
}
@keyframes orb-drift-a { to { transform: translate(70px, 50px) scale(1.14); } }
@keyframes orb-drift-b { to { transform: translate(-60px, -70px) scale(1.10); } }
@keyframes orb-drift-c { to { transform: translate(50px, -40px) scale(1.18); } }
@media (prefers-reduced-motion: reduce) {
.orb { animation: none; }
.welcome-inner { animation: none; }
}
/* ── Boot splash ────────────────────────────────────────────────── */
/* Covers the shell while the first services/config data loads; the
inline script in index.html lifts it (window.__liftAppSplash). */
#app-splash {
position: fixed;
inset: 0;
z-index: 2000;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.45s ease;
}
#app-splash.done { opacity: 0; pointer-events: none; }
.splash-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 24px;
}
.splash-ring {
position: relative;
width: 96px;
height: 96px;
display: grid;
place-items: center;
margin-bottom: 8px;
}
.splash-ring img { width: 60px; height: 60px; opacity: 0.95; }
.splash-ring-arc {
position: absolute;
inset: 0;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.10);
border-top-color: var(--accent);
animation: splash-spin 1s linear infinite;
}
@keyframes splash-spin { to { transform: rotate(360deg); } }
.splash-title {
font-size: 1.02rem;
font-weight: 700;
color: var(--text);
}
.splash-sub {
font-size: 0.84rem;
color: var(--text-3);
}
@media (prefers-reduced-motion: reduce) {
.splash-ring-arc { animation-duration: 2.5s; }
}
.welcome-inner {
position: relative;
max-width: 1040px;
margin: 0 auto;
animation: welcome-in 0.5s ease both;
}
@keyframes welcome-in {
from { opacity: 0; transform: translateY(14px); }
}
.welcome-greeting {
font-size: 0.98rem;
font-weight: 650;
color: var(--text-2);
}
.welcome-title {
margin-top: 6px;
font-size: clamp(1.7rem, 2.8vw, 2.25rem);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.18;
}
.welcome-title em { font-style: normal; color: var(--accent); }
.welcome-meta {
margin-top: 12px;
font-size: 0.8rem;
color: var(--text-3);
font-family: var(--mono);
}
.welcome-meta-sep { margin: 0 8px; opacity: 0.6; }
.welcome-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 1fr; /* both rows share the taller height → uniform cards */
gap: 16px;
margin-top: 34px;
}
/* Three cards (Desktop-only role, no Bitcoin card) → one symmetric row */
@media (min-width: 681px) {
.welcome-cards:not(:has(#w-btc)) {
grid-template-columns: repeat(3, 1fr);
}
}
/* wrappers whose children become grid items */
.welcome-dyn { display: contents; }
.w-network .net-row {
display: flex;
align-items: baseline;
gap: 10px;
margin-top: 6px;
font-family: var(--mono);
}
.w-network .net-k {
font-size: 0.6rem;
font-weight: 800;
letter-spacing: 0.08em;
color: var(--text-3);
width: 30px;
flex-shrink: 0;
}
.w-network .ip-value {
font-size: 0.8rem;
font-weight: 600;
color: var(--text);
}
.w-network .sub { margin-top: 8px; }
.welcome-browse {
margin-top: 26px;
}
.widget {
background: var(--card);
border: 1px solid var(--border);
border-radius: 20px;
padding: 17px 19px;
display: flex;
gap: 14px;
align-items: center;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), var(--shadow-card);
transition: border-color 0.18s;
position: relative;
overflow: hidden;
min-width: 0;
min-height: 88px; /* uniform card height everywhere */
}
.widget:hover { border-color: var(--border-strong); }
.widget.clickable:hover { transform: translateY(-1px); }
.widget.clickable { cursor: pointer; }
.widget.clickable:focus-visible {
outline: 2px solid rgba(66, 243, 154, 0.45);
outline-offset: 2px;
}
.widget-chip {
width: 46px;
height: 46px;
border-radius: 14px;
flex-shrink: 0;
display: grid;
place-items: center;
color: #fff;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 6px 16px rgba(0, 0, 0, 0.3);
}
.widget-chip svg { width: 22px; height: 22px; }
.widget-chip img { width: 46px; height: 46px; display: block; object-fit: contain; }
.widget-chip.chip-green {
background: linear-gradient(160deg, #2f9f6b, #17683f);
}
.widget-chip.chip-btc {
background: linear-gradient(160deg, #f7931a, #b96a0a);
}
.widget-chip.chip-amber {
background: linear-gradient(160deg, #b98426, #8a5f16);
}
.widget-chip.chip-sovran {
background: linear-gradient(160deg, #42f39a, #1aa45d);
}
.widget-chip.chip-neutral {
background: linear-gradient(160deg, #2e333a, #23272c);
}
.w-body { min-width: 0; }
.widget h3 {
font-size: 0.9rem;
font-weight: 700;
letter-spacing: -0.005em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.widget .sub {
margin-top: 4px;
font-size: 0.8rem;
line-height: 1.45;
color: var(--text-2);
overflow-wrap: anywhere;
}
.widget .sub b { color: var(--text); font-weight: 650; }
.widget .sub .good { color: var(--accent); font-weight: 650; }
.widget .sub .warn { color: var(--amber); font-weight: 650; }
.w-bar {
height: 7px;
border-radius: 99px;
background: rgba(255, 255, 255, 0.07);
overflow: hidden;
margin: 8px 0 7px;
}
.w-bar-fill {
height: 100%;
border-radius: 99px;
background: linear-gradient(90deg, #42f39a, #1aa45d);
transition: width 0.6s cubic-bezier(0.3, 0.7, 0.3, 1);
}
/* ── Tiles area ─────────────────────────────────────────────────── */
#tiles-area {
min-width: 0;
}
.dashboard-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 72px 24px;
color: var(--text-3);
font-size: 0.9rem;
}
.dashboard-loading-spinner {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2.5px solid rgba(255, 255, 255, 0.12);
border-top-color: var(--accent);
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Category sections ──────────────────────────────────────────── */
.category-section {
margin-bottom: 32px;
}
.section-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
font-size: 0.72rem;
font-weight: 750;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-2);
white-space: nowrap;
}
.section-divider {
border: none;
flex: 1;
height: 1px;
background: linear-gradient(90deg, var(--border), transparent);
margin: 0;
}
.tiles-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
gap: 15px;
}
/* ── Empty state ────────────────────────────────────────────────── */
.empty-state {
text-align: center;
padding: 64px 24px;
color: var(--text-3);
}
.empty-state p {
font-size: 1rem;
margin-bottom: 8px;
}
/* ── Upgrade modal (kept from previous layout) ──────────────────── */
.upgrade-dialog {
max-width: 480px;
}
.upgrade-info-box {
background: rgba(0, 0, 0, 0.16);
border: 1px solid var(--border-strong);
border-radius: 16px;
padding: 14px 16px;
margin: 14px 0;
}
.upgrade-info-title {
font-size: 0.88rem;
font-weight: 700;
color: var(--text);
margin-bottom: 8px;
}
.upgrade-info-list {
padding-left: 20px;
font-size: 0.85rem;
color: var(--text-2);
line-height: 1.7;
margin: 0;
}
.upgrade-info-list a {
color: var(--accent);
}
.upgrade-rebuild-note {
font-style: italic;
color: var(--text-3);
font-size: 0.82rem;
}
/* ── First-login security banner (inside .content) ──────────────── */
.security-first-login-banner {
display: flex;
align-items: center;
gap: 12px;
background: rgba(233, 182, 74, 0.08);
border: 1px solid rgba(233, 182, 74, 0.30);
border-radius: 16px;
padding: 12px 16px;
color: var(--text-2);
font-size: 0.85rem;
margin-bottom: 24px;
}
/* ── Narrow viewports (phone / half-screen RDP) ──────────────────── */
/* The sidebar becomes an icon rail so the content keeps room, and the
topbar wraps its search onto a second row instead of overflowing. */
@media (max-width: 920px) {
.sidebar {
width: 76px;
padding: 16px 8px 12px;
}
.brand { justify-content: center; padding: 2px 0 18px; gap: 0; }
.brand-text,
.nav-label,
.nav-text,
.nav-count,
.sidebar-support-text,
.sidebar-divider,
#sidebar-features { display: none; }
.nav-item { justify-content: center; gap: 0; padding: 10px 0; }
.sidebar-support-btn { justify-content: center; gap: 0; padding: 10px 0; }
.content { padding: 18px 18px 40px; }
/* keep the welcome background full-bleed against the new padding */
.welcome {
margin: -18px -18px -40px;
padding: 48px 20px 40px;
min-height: calc(100% + 58px);
}
}
@media (max-width: 640px) {
.topbar { flex-wrap: wrap; row-gap: 10px; padding: 12px 16px; }
.search-box { order: 5; flex-basis: 100%; }
.welcome { padding: 40px 16px 36px; }
.welcome-cards { grid-template-columns: 1fr; }
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,830 @@
/* ── Onboarding wizard ──────────────────────────────────────────── */
.onboarding-body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
min-height: 100vh;
background:
radial-gradient(ellipse at top, rgba(62, 207, 142, 0.04) 0%, transparent 50%),
var(--bg-color);
padding: 24px 16px 48px;
overflow-y: auto;
}
.onboarding-shell {
width: 100%;
max-width: 680px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 0;
}
/* Progress bar */
.onboarding-progress-bar {
width: 100%;
height: 4px;
background-color: var(--card-color);
border-radius: 2px;
overflow: hidden;
margin-bottom: 24px;
}
.onboarding-progress-fill {
height: 100%;
background-color: var(--accent-color);
border-radius: 2px;
transition: width 0.4s ease;
width: 20%;
}
/* Step indicator dots */
.onboarding-steps-nav {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
margin-bottom: 28px;
}
.onboarding-step-dot {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: var(--card-color);
border: 2px solid var(--border-color);
color: var(--text-dim);
font-size: 0.78rem;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background-color 0.2s, border-color 0.2s, color 0.2s;
}
.onboarding-step-dot.active {
background-color: var(--accent-color);
border-color: var(--accent-color);
color: #04220f;
}
.onboarding-step-dot.completed {
background-color: var(--green);
border-color: var(--green);
color: #04220f;
}
.onboarding-step-connector {
flex: 1;
height: 2px;
background-color: var(--border-color);
min-width: 24px;
max-width: 80px;
transition: background-color 0.2s;
}
/* Panel wrapper and panels */
.onboarding-panel-wrap {
position: relative;
}
.onboarding-panel {
display: flex;
flex-direction: column;
gap: 20px;
animation: panel-fade-in 0.25s ease-out;
}
@keyframes panel-fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Hero / welcome section */
.onboarding-hero {
text-align: center;
padding: 16px 0 4px;
}
.onboarding-logo {
margin-bottom: 16px;
font-size: 3rem;
}
.onboarding-logo-img {
height: 90px;
width: auto;
}
.onboarding-title {
font-size: 1.6rem;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 8px;
line-height: 1.2;
}
.onboarding-subtitle {
font-size: 1rem;
color: var(--text-secondary);
}
/* Cards */
.onboarding-card {
background-color: rgba(18, 24, 20, 0.65);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: var(--radius-card);
padding: 24px 28px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Body text */
.onboarding-body-text {
font-size: 0.92rem;
color: var(--text-secondary);
line-height: 1.65;
}
.onboarding-body-text--dim {
color: var(--text-dim);
font-size: 0.85rem;
}
/* Role row */
.onboarding-role-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background-color: var(--card-color);
border-radius: 8px;
}
.onboarding-role-label {
font-size: 0.82rem;
font-weight: 600;
color: var(--text-secondary);
}
.onboarding-role-badge {
font-size: 0.82rem;
font-weight: 700;
color: var(--accent-color);
background-color: rgba(62, 207, 142, 0.10);
padding: 3px 10px;
border-radius: 20px;
border: 1px solid rgba(62, 207, 142, 0.25);
}
/* Step header */
.onboarding-step-header {
padding-bottom: 4px;
}
.onboarding-step-icon {
font-size: 2rem;
display: block;
margin-bottom: 8px;
}
.onboarding-step-title {
font-size: 1.35rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.onboarding-step-desc {
font-size: 0.88rem;
color: var(--text-secondary);
line-height: 1.6;
}
/* Footer / navigation */
.onboarding-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 24px;
padding-bottom: 24px;
margin-top: auto;
}
.onboarding-btn-next {
/* inherits from .btn.btn-primary */
}
.onboarding-btn-back {
/* inherits from .btn.btn-close-modal */
}
/* Domain configuration (Step 2) */
.onboarding-domain-group {
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px 0;
border-bottom: 1px solid var(--border-color);
}
.onboarding-domain-group:last-child {
border-bottom: none;
}
.onboarding-domain-group--email {
/* email-specific variant */
}
.onboarding-domain-label {
font-size: 0.88rem;
font-weight: 600;
color: var(--text-primary);
}
.onboarding-domain-label--sub {
font-size: 0.78rem;
font-weight: 400;
color: var(--text-dim);
}
.onboarding-domain-input,
.domain-field-input {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-btn);
background-color: var(--card-color);
color: var(--text-primary);
font-size: 0.88rem;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
transition: border-color 0.15s;
}
.onboarding-domain-input:focus,
.domain-field-input:focus {
outline: none;
border-color: var(--accent-color);
}
.onboarding-hint {
font-size: 0.78rem;
color: var(--text-dim);
line-height: 1.5;
}
.onboarding-hint--inline {
display: inline;
}
/* Port forwarding (Step 3) */
.onboarding-port-note {
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.6;
}
.onboarding-port-ip {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background-color: var(--card-color);
border-radius: 8px;
border: 1px solid var(--border-color);
}
.onboarding-port-ip-label {
font-size: 0.82rem;
font-weight: 600;
color: var(--text-secondary);
white-space: nowrap;
}
.onboarding-port-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.onboarding-port-section-title {
font-size: 0.88rem;
font-weight: 700;
color: var(--text-primary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.onboarding-port-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.onboarding-port-table thead th {
text-align: left;
font-size: 0.78rem;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 6px 8px;
border-bottom: 1px solid var(--border-color);
}
.onboarding-port-table td {
padding: 8px 8px;
border-bottom: 1px solid rgba(30, 45, 39, 0.5);
color: var(--text-secondary);
vertical-align: middle;
}
.onboarding-port-table tr:last-child td {
border-bottom: none;
}
.onboarding-port-totals {
padding: 10px 14px;
background-color: var(--card-color);
border-radius: 8px;
border: 1px solid var(--border-color);
font-size: 0.85rem;
color: var(--text-secondary);
}
.onboarding-port-warn {
padding: 10px 14px;
background-color: rgba(233, 182, 74, 0.1);
border: 1px solid rgba(233, 182, 74, 0.35);
border-radius: 8px;
font-size: 0.85rem;
color: var(--yellow);
line-height: 1.5;
}
.onboarding-port-details {
font-size: 0.85rem;
}
.onboarding-port-details-summary {
cursor: pointer;
font-size: 0.82rem;
font-weight: 600;
color: var(--accent-color);
list-style: none;
user-select: none;
}
.onboarding-port-details-summary::-webkit-details-marker {
display: none;
}
.onboarding-port-details-summary::before {
content: '▶ ';
font-size: 0.65em;
}
.onboarding-port-details[open] .onboarding-port-details-summary::before {
content: '▼ ';
}
/* Credentials (Step 4) */
.onboarding-creds-notice {
padding: 12px 16px;
background-color: rgba(62, 207, 142, 0.08);
border: 1px solid rgba(62, 207, 142, 0.20);
border-radius: 8px;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.55;
}
.onboarding-creds-category {
display: flex;
flex-direction: column;
gap: 8px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.onboarding-creds-category:last-child {
border-bottom: none;
padding-bottom: 0;
}
.onboarding-creds-category-title {
font-size: 0.78rem;
font-weight: 700;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 2px;
}
.onboarding-creds-service {
background-color: var(--card-color);
border-radius: 8px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 8px;
}
.onboarding-creds-service-name {
font-size: 0.88rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.onboarding-cred-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.82rem;
}
.onboarding-cred-label {
color: var(--text-dim);
font-size: 0.78rem;
min-width: 80px;
flex-shrink: 0;
}
.onboarding-cred-value {
color: var(--text-primary);
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.82rem;
word-break: break-all;
flex: 1;
}
.onboarding-cred-secret {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
}
.onboarding-cred-hidden {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.82rem;
color: var(--text-dim);
letter-spacing: 0.1em;
}
.onboarding-cred-real {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.82rem;
color: var(--text-primary);
word-break: break-all;
display: none;
}
.onboarding-cred-reveal-btn {
font-size: 0.72rem;
padding: 2px 8px;
border-radius: 4px;
background-color: rgba(62, 207, 142, 0.08);
border: 1px solid rgba(62, 207, 142, 0.20);
color: var(--accent-color);
cursor: pointer;
white-space: nowrap;
transition: background-color 0.15s;
}
.onboarding-cred-reveal-btn:hover {
background-color: rgba(62, 207, 142, 0.15);
}
/* Completion checklist (Step 5) */
.onboarding-checklist {
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
}
.onboarding-checklist li {
font-size: 0.92rem;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 10px;
}
/* Status & feedback */
.onboarding-loading {
font-size: 0.88rem;
color: var(--text-dim);
text-align: center;
padding: 16px 0;
font-style: italic;
}
.onboarding-error {
font-size: 0.85rem;
color: var(--red);
padding: 10px 14px;
background-color: rgba(224, 27, 36, 0.1);
border: 1px solid rgba(224, 27, 36, 0.3);
border-radius: 8px;
line-height: 1.5;
}
.onboarding-save-status {
font-size: 0.85rem;
min-height: 1.2em;
transition: color 0.2s;
}
.onboarding-save-status--ok {
color: var(--green);
}
.onboarding-save-status--error {
color: var(--red);
}
.onboarding-save-status--info {
color: var(--text-secondary);
}
/* ── Timezone / Locale step (Step 2) ────────────────────────────── */
.onboarding-tz-group {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 0;
}
.onboarding-tz-search {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-btn);
background-color: var(--card-color);
color: var(--text-primary);
font-size: 0.88rem;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
transition: border-color 0.15s;
}
.onboarding-tz-search:focus {
outline: none;
border-color: var(--accent-color);
}
.onboarding-tz-select {
width: 100%;
padding: 6px 10px;
border: 1px solid var(--border-color);
border-radius: var(--radius-btn);
background-color: var(--card-color);
color: var(--text-primary);
font-size: 0.88rem;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
transition: border-color 0.15s;
overflow-y: auto;
}
.onboarding-tz-select:focus {
outline: none;
border-color: var(--accent-color);
}
.onboarding-tz-select option {
padding: 4px 8px;
background-color: var(--card-color);
color: var(--text-primary);
}
.onboarding-locale-select {
height: auto;
size: auto;
}
/* ── Password step (Step 3) ─────────────────────────────────────── */
.onboarding-password-group {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px 0;
}
.onboarding-password-input-wrap {
display: flex;
align-items: center;
gap: 6px;
}
.onboarding-password-input {
flex: 1;
padding: 9px 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-btn);
background-color: var(--card-color);
color: var(--text-primary);
font-size: 0.88rem;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
transition: border-color 0.15s;
}
.onboarding-password-input:focus {
outline: none;
border-color: var(--accent-color);
}
.onboarding-password-toggle {
padding: 6px 10px;
background-color: var(--card-color);
border: 1px solid var(--border-color);
border-radius: var(--radius-btn);
color: var(--text-secondary);
cursor: pointer;
font-size: 1rem;
line-height: 1;
transition: background-color 0.15s, border-color 0.15s;
flex-shrink: 0;
}
.onboarding-password-toggle:hover {
background-color: rgba(62, 207, 142, 0.10);
border-color: var(--accent-color);
}
.onboarding-password-hint {
font-size: 0.78rem;
color: var(--text-dim);
line-height: 1.4;
}
.onboarding-password-warning {
padding: 10px 14px;
background-color: rgba(233, 182, 74, 0.1);
border: 1px solid rgba(233, 182, 74, 0.35);
border-radius: 8px;
font-size: 0.85rem;
color: var(--yellow);
line-height: 1.5;
margin-top: 6px;
}
.onboarding-password-success {
padding: 12px 16px;
background-color: rgba(62, 207, 142, 0.10);
border: 1px solid rgba(62, 207, 142, 0.30);
border-radius: 8px;
font-size: 0.92rem;
color: var(--green);
line-height: 1.5;
}
.onboarding-password-optional {
margin-top: 12px;
font-size: 0.88rem;
}
.onboarding-password-optional > summary {
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
color: var(--accent-color);
list-style: none;
user-select: none;
}
.onboarding-password-optional > summary::-webkit-details-marker {
display: none;
}
.onboarding-password-optional > summary::before {
content: '▶ ';
font-size: 0.65em;
}
.onboarding-password-optional[open] > summary::before {
content: '▼ ';
}
/* ── Reboot overlay ─────────────────────────────────────────────── */
.reboot-overlay {
display: none;
position: fixed;
inset: 0;
background-color: rgba(6, 8, 7, 0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 999;
align-items: center;
justify-content: center;
}
.reboot-overlay.visible {
display: flex;
}
.reboot-card {
background-color: rgba(18, 24, 20, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
padding: 48px 56px;
text-align: center;
max-width: 480px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.8);
animation: reboot-fade-in 0.4s ease-out;
}
@keyframes reboot-fade-in {
from { opacity: 0; transform: scale(0.92) translateY(12px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.reboot-icon {
font-size: 3rem;
color: var(--accent-color);
margin-bottom: 16px;
animation: reboot-spin 2s linear infinite;
display: inline-block;
}
@keyframes reboot-spin {
to { transform: rotate(360deg); }
}
.reboot-title {
font-size: 1.35rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.reboot-message {
font-size: 0.92rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 24px;
}
.reboot-dots {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 16px;
}
.reboot-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--accent-color);
animation: reboot-bounce 1.4s ease-in-out infinite;
}
.reboot-dot:nth-child(2) { animation-delay: 0.2s; }
.reboot-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes reboot-bounce {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1.2); }
}
.reboot-submessage {
font-size: 0.82rem;
color: var(--text-dim);
font-style: italic;
}
/* ── Responsive ─────────────────────────────────────────────────── */
/* Legacy pre-redesign narrow-screen rules removed: they targeted the old
DOM (.main-content, .header-bar, .ip-bar) and their .sidebar /
#tiles-area / .service-tile / .creds-qr-img overrides fought the new
responsive layout in layout.css (sidebar icon rail, wrapping topbar).
Narrow-viewport behavior now lives in layout.css + header.css. */
@@ -0,0 +1,460 @@
/* ── Security Modal sections ──────────────────────────────────────── */
.security-section {
padding: 0 0 8px 0;
}
.security-section-title {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
margin: 0 0 10px 0;
}
.security-section-desc {
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 10px;
}
.security-divider {
border: none;
border-top: 1px solid var(--border-color);
margin: 18px 0;
}
/* ── Security Reset warning box ──────────────────────────────────── */
.security-warning-box {
background-color: rgba(180, 40, 40, 0.10);
border-left: 3px solid var(--red);
border-radius: 6px;
padding: 12px 14px;
margin-bottom: 14px;
}
.security-warning-text {
font-size: 0.85rem;
color: var(--text-primary);
margin: 0 0 8px 0;
line-height: 1.5;
}
.security-warning-list {
margin: 6px 0 6px 20px;
padding: 0;
font-size: 0.82rem;
color: var(--text-secondary);
line-height: 1.7;
}
.security-erase-group {
margin-bottom: 14px;
}
.security-erase-label {
display: block;
font-size: 0.85rem;
color: var(--text-primary);
margin-bottom: 6px;
}
.security-erase-input {
width: 100%;
padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--input-bg, var(--card-color));
color: var(--text-primary);
font-size: 0.9rem;
box-sizing: border-box;
}
.security-reset-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn-danger {
background-color: var(--red);
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 18px;
font-size: 0.88rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.15s;
}
.btn-danger:hover:not(:disabled) {
background-color: var(--red);
}
.btn-danger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.security-status-msg {
font-size: 0.83rem;
margin-top: 10px;
min-height: 1.2em;
}
.security-status-info { color: var(--text-secondary); }
.security-status-ok { color: var(--accent); }
.security-status-error { color: var(--red); }
/* ── Verify System Integrity ─────────────────────────────────────── */
.security-verify-list {
margin: 8px 0 10px 20px;
padding: 0;
font-size: 0.84rem;
color: var(--text-secondary);
line-height: 1.7;
}
.security-verify-loading {
font-size: 0.85rem;
color: var(--text-secondary);
}
.security-verify-result-card {
background: var(--card-color);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.security-verify-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.security-verify-label {
font-size: 0.82rem;
color: var(--text-secondary);
min-width: 130px;
font-weight: 600;
}
.security-verify-value {
font-size: 0.82rem;
color: var(--text-primary);
}
.security-verify-mono {
font-family: monospace;
font-size: 0.78rem;
word-break: break-all;
}
.security-verify-link {
font-size: 0.78rem;
color: var(--accent-color, var(--accent));
text-decoration: none;
}
.security-verify-link:hover { text-decoration: underline; }
.security-verify-badge {
font-size: 0.82rem;
font-weight: 700;
padding: 2px 10px;
border-radius: 12px;
}
.security-verify-pass {
background-color: rgba(66, 243, 154, 0.15);
color: var(--accent);
}
.security-verify-fail {
background-color: rgba(224, 82, 82, 0.15);
color: var(--red);
}
.security-verify-errors {
font-size: 0.8rem;
color: var(--text-secondary);
}
.security-verify-pre {
white-space: pre-wrap;
word-break: break-all;
font-size: 0.75rem;
background: var(--card-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px;
margin-top: 6px;
max-height: 150px;
overflow-y: auto;
}
.security-verify-path-row {
display: flex;
gap: 8px;
font-size: 0.78rem;
align-items: flex-start;
flex-wrap: wrap;
}
.security-verify-path-label {
font-weight: 600;
color: var(--text-secondary);
min-width: 70px;
}
/* ── Security Reset full-screen overlay ──────────────────────────── */
.security-reset-overlay {
display: none;
position: fixed;
inset: 0;
background: radial-gradient(1000px 640px at 50% 18%, #202429, #0f1113 72%);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 26px;
animation: security-reset-fade-in 0.35s ease-out;
}
.security-reset-overlay.visible {
display: flex;
}
@keyframes security-reset-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.security-reset-overlay-icon {
font-size: 3rem;
margin-bottom: 16px;
}
/* ── Phase 2: password display box ──────────────────────────────── */
.security-reset-password-label {
font-size: 0.88rem;
color: var(--text-secondary);
margin: 16px 0 8px 0;
}
.security-reset-password-box {
font-family: var(--mono);
font-size: 1.35rem;
font-weight: 700;
color: var(--text-primary);
background: var(--accent-dim);
border: 1.5px solid rgba(66, 243, 154, 0.35);
border-radius: 14px;
padding: 14px 24px;
letter-spacing: 0.04em;
text-align: center;
word-break: break-all;
margin-bottom: 16px;
min-width: 260px;
}
.security-reset-password-warning {
font-size: 0.84rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 20px;
text-align: center;
}
.security-reset-reboot-btn {
background: linear-gradient(180deg, #3fd68e, #1ea263);
color: #04220f;
border: none;
border-radius: 99px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 6px 18px rgba(35, 199, 124, 0.22);
padding: 11px 22px;
font-size: 0.88rem;
font-weight: 700;
cursor: pointer;
transition: background-color 0.15s, opacity 0.15s;
white-space: nowrap;
}
.security-reset-reboot-btn:hover:not(:disabled) {
background-color: var(--accent);
}
.security-reset-reboot-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ── First-login security banner ─────────────────────────────────── */
.security-first-login-banner {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
margin-bottom: 24px;
background: rgba(233, 182, 74, 0.08);
border: 1px solid rgba(233, 182, 74, 0.30);
border-radius: 16px;
color: var(--text-primary);
}
.security-banner-content {
display: flex;
align-items: flex-start;
gap: 10px;
flex: 1;
min-width: 0;
}
.security-banner-icon {
font-size: 1.2rem;
flex-shrink: 0;
margin-top: 1px;
}
.security-banner-text {
font-size: 0.85rem;
line-height: 1.5;
color: var(--text-primary);
}
.security-banner-dismiss {
background: none;
border: 1px solid var(--border-strong);
border-radius: 99px;
cursor: pointer;
font-size: 0.9rem;
color: var(--text-secondary);
padding: 4px 10px;
flex-shrink: 0;
line-height: 1.4;
transition: 0.15s;
}
.security-banner-dismiss:hover {
color: var(--text);
border-color: rgba(255, 255, 255, 0.26);
background: rgba(255, 255, 255, 0.04);
}
/* ── Legacy security inline warning banner ───────────────────────── */
.security-inline-banner {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 14px;
margin-bottom: 12px;
background-color: rgba(180, 100, 0, 0.12);
border-left: 3px solid #c98d08;
border-radius: 6px;
color: var(--text-primary);
}
.security-inline-icon {
font-size: 1rem;
color: #e0a010;
flex-shrink: 0;
}
.security-inline-text {
font-size: 0.82rem;
line-height: 1.5;
color: var(--text-secondary);
}
.security-inline-link {
display: inline-block;
font-size: 0.82rem;
font-weight: 600;
color: #e0a010;
text-decoration: none;
border: 1px solid #c98d08;
border-radius: 4px;
padding: 4px 10px;
align-self: flex-start;
transition: background-color 0.15s;
}
.security-inline-link:hover {
background-color: rgba(180, 100, 0, 0.22);
}
/* ── System change-password form extras ──────────────────────────── */
.sys-chpw-header {
margin-bottom: 14px;
}
.sys-chpw-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.sys-chpw-desc {
font-size: 0.82rem;
color: var(--text-secondary);
line-height: 1.5;
}
.pw-input-wrap {
position: relative;
display: flex;
align-items: center;
}
.pw-input-wrap .matrix-form-input {
padding-right: 2.4rem;
width: 100%;
}
.pw-toggle-btn {
position: absolute;
right: 6px;
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
padding: 2px 4px;
line-height: 1;
color: var(--text-secondary);
opacity: 0.75;
transition: opacity 0.15s;
}
.pw-toggle-btn:hover {
opacity: 1;
}
.pw-hint {
font-size: 0.76rem;
color: var(--text-secondary);
margin-top: 4px;
}
.pw-credentials-note {
font-size: 0.78rem;
color: #c98d08;
background-color: rgba(180, 100, 0, 0.10);
border-left: 2px solid #c98d08;
border-radius: 4px;
padding: 7px 10px;
margin-bottom: 12px;
line-height: 1.5;
}
@@ -0,0 +1,376 @@
/* ── Tech Support modal ──────────────────────────────────────────── */
.support-section {
text-align: center;
}
.support-icon-big {
font-size: 3rem;
margin-bottom: 12px;
}
.support-active-icon {
animation: none;
}
.support-heading {
font-size: 1.15rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.support-active-heading {
color: var(--green);
}
.support-desc {
font-size: 0.88rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 16px;
text-align: left;
}
.support-active-note {
font-size: 0.88rem;
color: var(--text-secondary);
margin-bottom: 16px;
}
.support-info-box {
background-color: var(--card-color);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 14px 18px;
margin-bottom: 16px;
text-align: left;
}
.support-active-box {
border-color: var(--green);
}
.support-info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
}
.support-info-label {
font-size: 0.82rem;
color: var(--text-dim);
font-weight: 600;
}
.support-info-value {
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-size: 0.88rem;
color: var(--accent-color);
font-weight: 600;
}
.support-info-hint {
font-size: 0.72rem;
color: var(--text-dim);
margin-top: 6px;
font-style: italic;
}
.support-steps {
text-align: left;
margin-bottom: 16px;
padding: 14px 18px;
background-color: var(--card-color);
border-radius: 10px;
border: 1px solid var(--border-color);
}
.support-steps-title {
font-size: 0.82rem;
font-weight: 700;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 8px;
}
.support-steps ol {
padding-left: 20px;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.7;
}
.support-steps code {
background-color: rgba(62, 207, 142, 0.10);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.82rem;
color: var(--accent-color);
}
.support-btn-enable {
width: 100%;
padding: 12px;
border-radius: var(--radius-btn);
background-color: var(--accent-color);
color: #04220f;
font-size: 0.95rem;
font-weight: 700;
margin-bottom: 10px;
}
.support-btn-enable:hover:not(:disabled) {
opacity: 0.88;
}
.support-btn-disable {
width: 100%;
padding: 12px;
border-radius: var(--radius-btn);
background-color: var(--red);
color: #fff;
font-size: 0.95rem;
font-weight: 700;
margin-bottom: 10px;
}
.support-btn-disable:hover:not(:disabled) {
opacity: 0.88;
}
.support-btn-done {
width: 100%;
padding: 12px;
border-radius: var(--radius-btn);
background-color: var(--accent-color);
color: #04220f;
font-size: 0.95rem;
font-weight: 700;
margin-top: 16px;
}
.support-btn-done:hover:not(:disabled) {
opacity: 0.88;
}
.support-btn-auditlog {
width: 100%;
padding: 10px;
border-radius: var(--radius-btn);
background-color: var(--border-color);
color: var(--text-primary);
font-size: 0.85rem;
font-weight: 600;
margin-top: 8px;
}
.support-btn-auditlog:hover:not(:disabled) {
background-color: var(--card-hover);
}
.support-fine-print {
font-size: 0.72rem;
color: var(--text-dim);
font-style: italic;
margin-bottom: 8px;
}
.support-verify-box {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 16px 0;
padding: 12px;
background-color: var(--card-color);
border-radius: 8px;
}
.support-verify-label {
font-size: 0.82rem;
color: var(--text-dim);
font-weight: 600;
}
.support-verify-value {
font-size: 0.88rem;
font-weight: 700;
}
.support-verify-value.verified-gone {
color: var(--green);
}
.support-verify-value.verify-warning {
color: var(--yellow);
}
/* ── Wallet protection ───────────────────────────────────────────── */
.support-wallet-box {
text-align: left;
padding: 14px 18px;
border-radius: 10px;
margin-bottom: 16px;
border: 1px solid var(--border-color);
}
.support-wallet-protected {
background-color: rgba(66, 243, 154, 0.06);
border-color: rgba(66, 243, 154, 0.3);
}
.support-wallet-unlocked {
background-color: rgba(233, 182, 74, 0.06);
border-color: rgba(233, 182, 74, 0.3);
}
.support-wallet-warning {
background-color: rgba(224, 27, 36, 0.06);
border-color: rgba(224, 27, 36, 0.3);
}
.support-wallet-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.support-wallet-icon {
font-size: 1.2rem;
}
.support-wallet-title {
font-size: 0.88rem;
font-weight: 700;
color: var(--text-primary);
}
.support-wallet-desc {
font-size: 0.82rem;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 8px;
}
.support-wallet-paths {
list-style: none;
padding: 0;
margin: 8px 0;
}
.support-wallet-paths li {
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-size: 0.78rem;
color: var(--text-dim);
padding: 2px 0;
}
.support-wallet-unlock-row {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.support-unlock-select {
background-color: var(--card-color);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 6px 10px;
font-size: 0.82rem;
}
.support-btn-wallet-unlock {
padding: 8px 16px;
border-radius: var(--radius-btn);
background-color: var(--yellow);
color: #04220f;
font-size: 0.82rem;
font-weight: 700;
}
.support-btn-wallet-unlock:hover:not(:disabled) {
background-color: #c98d08;
}
.support-btn-wallet-lock {
padding: 8px 16px;
border-radius: var(--radius-btn);
background-color: var(--green);
color: #fff;
font-size: 0.82rem;
font-weight: 700;
margin-top: 8px;
}
.support-btn-wallet-lock:hover:not(:disabled) {
background-color: var(--accent);
}
/* ── Audit log ───────────────────────────────────────────────────── */
.support-audit-container {
margin-top: 12px;
border-top: 1px solid var(--border-color);
padding-top: 12px;
}
.support-audit-log {
max-height: 200px;
overflow-y: auto;
background-color: var(--inset);
border-radius: 8px;
padding: 10px 14px;
}
.support-audit-entry {
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-size: 0.72rem;
color: var(--text-secondary);
padding: 3px 0;
border-bottom: 1px solid rgba(30, 45, 39, 0.4);
}
.support-audit-entry:last-child {
border-bottom: none;
}
.support-audit-empty {
font-size: 0.82rem;
color: var(--text-dim);
text-align: center;
padding: 12px;
}
/* ── Tech Support tile ───────────────────────────────────────────── */
.support-tile {
border-color: var(--border-color);
border-width: 1px;
border-style: solid;
}
.support-tile:hover {
border-color: var(--accent-color);
border-style: solid;
}
/* ── Manual Backup ───────────────────────────────────────────────── */
.support-backup-steps {
padding-left: 20px;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.8;
margin: 8px 0 0 0;
}
.support-backup-steps li {
margin-bottom: 4px;
}
@@ -0,0 +1,178 @@
/* ── Service tiles ──────────────────────────────────────────────── */
.service-tile {
background: var(--card);
border: 1px solid var(--border);
border-radius: 20px;
overflow: hidden;
padding: 21px 12px 17px;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
position: relative;
transition: transform 0.18s, border-color 0.18s, background 0.18s, box-shadow 0.18s, opacity 0.18s;
animation: tileIn 0.45s cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
}
@keyframes tileIn {
from { opacity: 0; transform: translateY(10px) scale(0.96); }
}
.service-tile:hover {
transform: translateY(-4px);
border-color: var(--border-strong);
background: var(--card-hover);
box-shadow: var(--shadow-hover);
}
.service-tile:active { transform: translateY(-1px) scale(0.985); }
.service-tile:focus-visible {
outline: 2px solid rgba(66, 243, 154, 0.45);
outline-offset: 2px;
}
.service-tile.disabled { opacity: 0.55; }
.tile-icon {
width: 62px;
height: 62px;
display: block;
object-fit: contain;
}
.tile-icon-fallback {
width: 62px;
height: 62px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-3);
font-size: 1.2rem;
font-weight: 700;
}
.tile-name {
margin-top: 13px;
font-size: 0.86rem;
font-weight: 650;
text-align: center;
line-height: 1.25;
min-height: 2.5em;
display: flex;
align-items: center;
justify-content: center;
color: var(--text);
max-width: 100%;
}
/* Status pill — dot + label inside a rounded pill */
.tile-status {
margin-top: 9px;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.69rem;
font-weight: 700;
padding: 3px 10px;
border-radius: 99px;
letter-spacing: 0.01em;
white-space: nowrap;
background: rgba(255, 255, 255, 0.05);
color: var(--text-2);
}
.status-text { line-height: 1; }
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-3);
flex-shrink: 0;
}
.status-dot.active { background: var(--accent); }
.status-dot.needs-attention { background: var(--amber); }
.status-dot.failed { background: var(--red); }
.status-dot.inactive,
.status-dot.disabled { background: var(--text-3); }
.status-dot.syncing,
.status-dot.loading,
.status-dot.checking-reachability { background: var(--blue); animation: tile-dot-pulse 1.4s ease-in-out infinite; }
.status-dot.unknown { background: var(--text-3); }
@keyframes tile-dot-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* Progressive enhancement: tint the whole pill on modern browsers */
.tile-status:has(.status-dot.active) { background: var(--accent-dim); color: var(--accent); }
.tile-status:has(.status-dot.needs-attention) { background: rgba(233, 182, 74, 0.10); color: var(--amber); }
.tile-status:has(.status-dot.failed) { background: rgba(246, 97, 81, 0.10); color: var(--red); }
.tile-status:has(.status-dot.syncing),
.tile-status:has(.status-dot.loading),
.tile-status:has(.status-dot.checking-reachability) { background: rgba(120, 174, 237, 0.10); color: var(--blue); }
.support-status-label {
font-size: 0.69rem;
font-weight: 700;
color: var(--text-2);
}
/* ── Bitcoin IBD sync tile ──────────────────────────────────────── */
.tile-sync-container {
margin-top: 12px;
width: 100%;
padding: 0 6px;
}
.tile-sync-label {
font-size: 0.66rem;
font-weight: 750;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--amber);
text-align: center;
}
.tile-sync-bar-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 7px;
}
.tile-sync-bar-track {
flex: 1;
height: 7px;
border-radius: 99px;
background: rgba(255, 255, 255, 0.07);
overflow: hidden;
}
.tile-sync-bar-fill {
height: 100%;
border-radius: 99px;
background: linear-gradient(90deg, #42f39a, #1aa45d);
transition: width 0.6s cubic-bezier(0.3, 0.7, 0.3, 1);
}
.tile-sync-percent {
font-family: var(--mono);
font-size: 0.7rem;
font-weight: 700;
color: var(--amber);
flex-shrink: 0;
}
.tile-sync-eta {
margin-top: 6px;
font-family: var(--mono);
font-size: 0.66rem;
color: var(--text-3);
text-align: center;
}
@@ -0,0 +1,236 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="128px"
height="128px"
viewBox="0 0 128 128"
version="1.1"
id="svg96"
sodipodi:docname="Sovran_SystemsOS_Updater_Iconv3.svg"
xml:space="preserve"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview98"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="5.2149125"
inkscape:cx="9.0126153"
inkscape:cy="64.430611"
inkscape:window-width="3440"
inkscape:window-height="1352"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer2" /><defs
id="defs67"><linearGradient
inkscape:collect="always"
id="linearGradient936"><stop
style="stop-color:#1e8e11;stop-opacity:1;"
offset="0"
id="stop932" /><stop
style="stop-color:#1bff00;stop-opacity:0;"
offset="1"
id="stop934" /></linearGradient><linearGradient
id="linearGradient1028"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop1026" /></linearGradient><linearGradient
id="linearGradient998"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop996" /></linearGradient><radialGradient
id="radial0"
gradientUnits="userSpaceOnUse"
cx="131.914749"
cy="55.927143"
fx="131.914749"
fy="55.927143"
r="160"
gradientTransform="matrix(0.232034,-0.541475,-0.368794,-0.0298398,4.277749,118.95849)"><stop
offset="0"
style="stop-color:#00ff39;stop-opacity:1;"
id="stop2" /><stop
offset="1"
style="stop-color:#004a19;stop-opacity:1;"
id="stop4" /></radialGradient><radialGradient
id="radial1"
gradientUnits="userSpaceOnUse"
cx="525.587769"
cy="638.591797"
fx="525.587769"
fy="638.591797"
r="192"
gradientTransform="matrix(-0.107656,-0.225172,-0.327748,0.258343,373.87973,30.205086)"><stop
offset="0"
style="stop-color:#43b60b;stop-opacity:1;"
id="stop7" /><stop
offset="1"
style="stop-color:#0b88ff;stop-opacity:0.00829875;"
id="stop9" /></radialGradient><clipPath
id="clip1"><path
d="M 7 46 L 57 46 L 57 93 L 7 93 Z M 7 46 "
id="path12" /></clipPath><clipPath
id="clip2"><path
d="M 32.25 46.957031 C 19.6875 46.96875 9.085938 56.636719 7.503906 69.53125 C 9.0625 82.445312 19.667969 92.144531 32.25 92.160156 C 44.816406 92.148438 55.414062 82.480469 57 69.585938 C 55.441406 56.671875 44.835938 46.972656 32.25 46.957031 Z M 32.25 46.957031 "
id="path15" /></clipPath><radialGradient
id="radial2"
gradientUnits="userSpaceOnUse"
cx="131.914749"
cy="55.927143"
fx="131.914749"
fy="55.927143"
r="160"
gradientTransform="matrix(0.485163,-1.148584,-0.771115,-0.0632965,-47.124961,203.98857)"><stop
offset="0"
style="stop-color:rgb(92.941177%,20%,23.137255%);stop-opacity:1;"
id="stop18" /><stop
offset="1"
style="stop-color:rgb(63.921571%,27.843139%,72.941178%);stop-opacity:1;"
id="stop20" /></radialGradient><radialGradient
id="radial3"
gradientUnits="userSpaceOnUse"
cx="525.587769"
cy="638.591797"
fx="525.587769"
fy="638.591797"
r="192"
gradientTransform="matrix(-0.225099,-0.477638,-0.685291,0.548001,725.67923,15.723794)"><stop
offset="0"
style="stop-color:rgb(10.980392%,44.313726%,84.705883%);stop-opacity:1;"
id="stop23" /><stop
offset="1"
style="stop-color:rgb(20.784314%,51.764709%,89.411765%);stop-opacity:0.00829876;"
id="stop25" /></radialGradient><linearGradient
id="linear0"
gradientUnits="userSpaceOnUse"
x1="22"
y1="37"
x2="62"
y2="37"
gradientTransform="matrix(1.4,0,0,1.4,-26.799973,2.491745)"><stop
offset="0"
style="stop-color:rgb(58.039218%,57.647061%,56.470591%);stop-opacity:1;"
id="stop28" /><stop
offset="0.0908155"
style="stop-color:rgb(87.058824%,86.666667%,85.490197%);stop-opacity:1;"
id="stop30" /><stop
offset="0.336093"
style="stop-color:rgb(60.392159%,60.000002%,58.823532%);stop-opacity:1;"
id="stop32" /><stop
offset="0.844326"
style="stop-color:rgb(76.47059%,75.294119%,72.941178%);stop-opacity:1;"
id="stop34" /><stop
offset="0.930505"
style="stop-color:rgb(87.058824%,86.666667%,85.490197%);stop-opacity:1;"
id="stop36" /><stop
offset="1"
style="stop-color:rgb(75.294119%,74.901962%,73.725492%);stop-opacity:1;"
id="stop38" /></linearGradient><radialGradient
id="radial4"
gradientUnits="userSpaceOnUse"
cx="-172.560638"
cy="28.569126"
fx="-172.560638"
fy="28.569126"
r="15.85742"
gradientTransform="matrix(1.560712,0,0,1.4252,300.69366,13.349996)"><stop
offset="0"
style="stop-color:rgb(100%,100%,100%);stop-opacity:0.358268;"
id="stop41" /><stop
offset="1"
style="stop-color:rgb(100%,100%,100%);stop-opacity:0.0944882;"
id="stop43" /></radialGradient><filter
id="alpha"
filterUnits="objectBoundingBox"
x="0"
y="0"
width="1"
height="1"><feColorMatrix
type="matrix"
in="SourceGraphic"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
id="feColorMatrix46" /></filter><mask
id="mask0"><g
filter="url(#alpha)"
id="g51"><rect
x="0"
y="0"
width="128"
height="128"
style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"
id="rect49" /></g></mask><clipPath
id="clip3"><rect
x="0"
y="0"
width="192"
height="152"
id="rect54" /></clipPath><g
id="surface382"
clip-path="url(#clip3)"><path
style=" stroke:none;fill-rule:nonzero;fill:rgb(27.058825%,21.176471%,21.568628%);fill-opacity:1;"
d="M 40 59.957031 C 26.191406 59.957031 15 71.152344 15 84.957031 C 15.011719 85.996094 15.085938 86.777344 15.222656 87.804688 C 15.222656 75.957031 27.421875 65.96875 40 65.957031 C 52.597656 65.972656 64.777344 75.957031 64.777344 87.859375 C 64.917969 86.816406 64.992188 86.011719 65 84.957031 C 65 71.152344 53.808594 59.957031 40 59.957031 Z M 40 59.957031 "
id="path57" /></g><radialGradient
id="radial5"
gradientUnits="userSpaceOnUse"
cx="40"
cy="227"
fx="40"
fy="227"
r="28"
gradientTransform="matrix(0.575553,0,1.60551e-8,1.540703,8.977913,-280.78108)"><stop
offset="0"
style="stop-color:rgb(100%,100%,100%);stop-opacity:1;"
id="stop60" /><stop
offset="0.744626"
style="stop-color:rgb(98.039216%,98.039216%,98.039216%);stop-opacity:1;"
id="stop62" /><stop
offset="1"
style="stop-color:rgb(87.450981%,87.450981%,87.450981%);stop-opacity:1;"
id="stop64" /></radialGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient936"
id="linearGradient938"
x1="-48.519272"
y1="18.511358"
x2="287.07454"
y2="18.511358"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1020247,0,0,1.1097375,37.198581,-10.424856)" /></defs><path
style="fill:#f5f5f3;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 20,11.957031 h 88 c 4.41797,0 8,3.582031 8,8 V 108 c 0,4.41797 -3.58203,8 -8,8 H 20 c -4.417969,0 -8,-3.58203 -8,-8 V 19.957031 c 0,-4.417969 3.582031,-8 8,-8 z m 0,0"
id="path69" /><path
style="fill:url(#radial0);fill-rule:nonzero;stroke:none"
d="m 20,85.957031 h 88 v -66 H 20 Z m 0,0"
id="path71" /><path
style="fill:none;fill-rule:nonzero;stroke:none;fill-opacity:1"
d="m 20,85.957031 h 88 v -66 H 20 Z m 0,0"
id="path73" /><g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 1"
transform="matrix(0.1816,0,0,0.1816,35.224187,79.037164)"><ellipse
fill="#54c147"
cx="168.64549"
cy="10.117889"
id="circle8314"
rx="184.91634"
ry="179.91556"
style="fill:url(#linearGradient938);fill-opacity:1;stroke-width:1.71591" /><polygon
fill="#ffffff"
points="46.678,120.299 63.562,96.402 96.977,121.718 145.084,50.79 168.752,69.02 103.583,164.647 "
id="polygon8316"
transform="matrix(1.7395866,0,0,1.6925423,-18.737581,-172.19767)" /></g></svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

@@ -0,0 +1,50 @@
/* Sovran_SystemsOS Hub — Vanilla JS Frontend
v7 — Status-only dashboard + Tech Support + Feature Manager */
"use strict";
const POLL_INTERVAL_SERVICES = 5000;
const POLL_INTERVAL_UPDATES = 1800000;
const UPDATE_POLL_INTERVAL = 2000;
// A pending fetch never rejects by itself. Bound every status request so a
// wedged browser connection cannot leave the modal spinning forever.
const STATUS_POLL_FETCH_TIMEOUT = 15000;
// Eight timed-out requests plus the poll interval is a little over two minutes.
// A brief Hub restart or a heavily loaded Nix build remains well inside this.
const STATUS_POLL_MAX_FAILURES = 8;
// Keep verbose Nix output from making textContent updates quadratic and
// freezing the Hub renderer (especially noticeable over RDP).
const UPDATE_VISIBLE_LOG_MAX_CHARS = 250000;
const UPDATE_VISIBLE_LOG_TRIM_CHARS = 200000;
const REBOOT_CHECK_INTERVAL = 5000;
const REBOOT_FETCH_TIMEOUT = 12000;
const REBOOT_REQUEST_TIMEOUT = 4000;
const REBOOT_INITIAL_DELAY = 25000;
const SUPPORT_TIMER_INTERVAL = 1000;
const CATEGORY_ORDER = [
"infrastructure",
"bitcoin",
"communication",
"apps",
"nostr",
];
/* The service catalog still distinguishes the Bitcoin foundation services
from the Bitcoin apps; the Hub shows them as one "Bitcoin" section. */
const CATEGORY_ALIASES = {
"bitcoin-base": "bitcoin",
"bitcoin-apps": "bitcoin",
};
const FEATURE_SUBCATEGORY_LABELS = {
"infrastructure": "🔧 Infrastructure",
"bitcoin": "₿ Bitcoin",
"communication": "💬 Communication",
"nostr": "📡 Nostr",
};
const FEATURE_SUBCATEGORY_ORDER = ["infrastructure", "bitcoin", "communication", "nostr"];
const STATUS_LOADING_STATES = new Set([
"reloading", "activating", "deactivating", "maintenance",
]);
@@ -0,0 +1,451 @@
"use strict";
/* ── The Hub dashboard chrome ──────────────────────────────────────
Welcome dashboard (default view), category navigation, service
search, status cards, and the Systems Operational modal.
Everything lives in an IIFE so no globals leak into the other Hub
scripts; tiles.js calls window.dashboardServicesUpdated() whenever
the service list refreshes. */
(function () {
var $nav = document.getElementById("sidebar-nav");
var $pageTitle = document.getElementById("page-title");
var $search = document.getElementById("search-input");
var $sysModal = document.getElementById("systems-modal");
var $sysBody = document.getElementById("systems-body");
var $welcome = document.getElementById("welcome-view");
var $tilesArea = document.getElementById("tiles-area");
var $wcSystems = document.getElementById("wc-systems");
var $wcMore = document.getElementById("wc-more");
var $greeting = document.getElementById("welcome-greeting");
var $welcomeRole = document.getElementById("welcome-role");
var $browseBtn = document.getElementById("welcome-browse-btn");
var _view = "dashboard"; // "dashboard" | "services"
var _cat = "all";
var _query = "";
var CAT_ICONS = {
"all": "g-grid",
"infrastructure": "g-server",
"bitcoin": "g-btc-sym",
"communication": "g-chat",
"apps": "g-dots",
"nostr": "g-antenna",
"other": "g-dots"
};
var CAT_FALLBACK_LABELS = {
"infrastructure": "Infrastructure",
"bitcoin": "Bitcoin",
"communication": "Communication",
"apps": "Personal Apps",
"nostr": "Nostr",
"other": "Other"
};
/* Units that serve a domain — used to find a live diagnostics
checklist for the Systems Operational modal (order matters only
for which service is polled first). */
var DOMAIN_UNITS = [
"matrix-synapse.service",
"btcpayserver.service",
"vaultwarden.service",
"phpfpm-nextcloud.service",
"phpfpm-wordpress.service",
"haven-relay.service",
"livekit.service",
"albyhub.service"
];
function icon(id) {
return '<svg><use href="#' + id + '"/></svg>';
}
function visibleServices() {
var services = (typeof _servicesCache !== "undefined" && _servicesCache) ? _servicesCache : [];
return services.filter(function (s) {
return s.category !== "support" && s.type !== "support";
});
}
/* ── Views ────────────────────────────────────────────────────── */
function setTitle(t) {
if ($pageTitle) $pageTitle.textContent = t;
}
function syncNav() {
if (!$nav) return;
$nav.querySelectorAll(".nav-item").forEach(function (b) {
var isActive;
if (b.dataset.cat === "__dash") isActive = (_view === "dashboard");
else isActive = (_view === "services" && b.dataset.cat === _cat);
b.classList.toggle("active", isActive);
});
}
function showDashboard() {
_view = "dashboard";
_cat = "all";
if ($welcome) $welcome.style.display = "";
if ($tilesArea) $tilesArea.style.display = "none";
setTitle("Dashboard");
syncNav();
}
function showServices(cat) {
_view = "services";
if (cat) _cat = cat;
if ($welcome) $welcome.style.display = "none";
if ($tilesArea) $tilesArea.style.display = "";
setTitle(_cat === "all" ? "All services" : catLabel(_cat));
syncNav();
applyFilter();
}
/* ── Category navigation ──────────────────────────────────────── */
function renderNav() {
if (!$nav) return;
var counts = {};
var order = [];
visibleServices().forEach(function (s) {
var cat = s.category || "other";
if (CATEGORY_ALIASES[cat]) cat = CATEGORY_ALIASES[cat];
if (!counts[cat]) { counts[cat] = 0; order.push(cat); }
counts[cat]++;
});
var total = visibleServices().length;
var html = '<div class="nav-label">Menu</div>';
html += '<button class="nav-item' + (_view === "dashboard" ? " active" : "") + '" data-cat="__dash" type="button">' +
icon("g-home") +
'<span class="nav-text">Dashboard</span>' +
'</button>';
html += '<div class="nav-label">Services</div>';
html += navItem("all", "All services", total);
order.forEach(function (cat) {
html += navItem(cat, catLabel(cat), counts[cat]);
});
$nav.innerHTML = html;
$nav.querySelectorAll(".nav-item").forEach(function (btn) {
btn.addEventListener("click", function () {
if (btn.dataset.cat === "__dash") showDashboard();
else showServices(btn.dataset.cat);
});
});
}
function navItem(cat, label, count) {
return '<button class="nav-item' + (_view === "services" && cat === _cat ? " active" : "") + '" data-cat="' + escHtml(cat) + '" type="button">' +
icon(CAT_ICONS[cat] || "g-dots") +
'<span class="nav-text">' + escHtml(label) + '</span>' +
'<span class="nav-count">' + count + '</span>' +
'</button>';
}
function catLabel(cat) {
if (typeof _categoryLabels !== "undefined" && _categoryLabels[cat]) return _categoryLabels[cat];
return CAT_FALLBACK_LABELS[cat] || cat;
}
/* ── Filtering (applies in the services view) ─────────────────── */
function applyFilter() {
if (_view !== "services") return;
var area = $tilesArea;
if (!area) return;
var q = _query.trim().toLowerCase();
area.querySelectorAll(".category-section").forEach(function (section) {
var catMatch = (_cat === "all") || (section.dataset.category === _cat);
var anyVisible = false;
section.querySelectorAll(".service-tile").forEach(function (tile) {
var nameEl = tile.querySelector(".tile-name");
var name = nameEl ? nameEl.textContent.toLowerCase() : "";
var match = catMatch && (!q || name.indexOf(q) !== -1);
tile.style.display = match ? "" : "none";
if (match) anyVisible = true;
});
section.style.display = (catMatch && (anyVisible || !q)) ? "" : "none";
});
}
if ($search) {
$search.addEventListener("input", function () {
// Searching implies browsing services — leave the welcome view.
if (_view === "dashboard" && $search.value) showServices("all");
_query = $search.value;
applyFilter();
});
}
if ($browseBtn) {
$browseBtn.addEventListener("click", function () { showServices("all"); });
}
/* ── Welcome header ───────────────────────────────────────────── */
function updateWelcomeMeta() {
if ($greeting) {
var h = new Date().getHours();
var g;
if (h >= 5 && h < 12) g = "Good morning";
else if (h >= 12 && h < 17) g = "Good afternoon";
else g = "Good evening";
$greeting.textContent = g;
}
if ($welcomeRole) {
$welcomeRole.textContent = (typeof window._roleLabel !== "undefined" && window._roleLabel) ? window._roleLabel : "";
}
}
/* ── Status cards ─────────────────────────────────────────────── */
function serviceCounts() {
var services = visibleServices();
var running = 0, attention = 0, off = 0;
var attentionNames = [], offNames = [];
services.forEach(function (s) {
if (!s.enabled) { off++; offNames.push(s.name); return; }
var h = s.health || s.status;
if (h === "needs_attention" || h === "failed") { attention++; attentionNames.push(s.name); }
else running++;
});
return { services: services, running: running, attention: attention, off: off, attentionNames: attentionNames, offNames: offNames };
}
function renderWidgets() {
if (!$wcSystems) return;
var c = serviceCounts();
if (!c.services.length) { $wcSystems.innerHTML = ""; if ($wcMore) $wcMore.innerHTML = ""; return; }
/* Systems operational */
var sub = '<b>' + c.running + '</b> running';
if (c.attention) sub += ' · <span class="warn">' + c.attention + ' needs attention</span>';
if (c.off) sub += ' · ' + c.off + ' off';
var attentionTitle = c.attention ? "Systems need attention" : "Systems operational";
$wcSystems.innerHTML =
'<div class="widget clickable" id="w-systems" role="button" tabindex="0" title="System status and router setup">' +
'<div class="widget-chip chip-green">' + icon("g-pulse") + '</div>' +
'<div class="w-body"><h3>' + attentionTitle + '</h3><div class="sub">' + sub + '</div></div>' +
'</div>';
var wSys = document.getElementById("w-systems");
if (wSys) {
wSys.addEventListener("click", openSystemsModal);
wSys.addEventListener("keydown", function (e) { if (e.key === "Enter") openSystemsModal(); });
}
if (!$wcMore) return;
var more = "";
/* Bitcoin Core sync */
var btc = null;
c.services.forEach(function (s) {
if (s.sync_ibd && s.enabled) btc = s;
});
if (btc) {
var pct = Math.round((btc.sync_progress || 0) * 100);
var blocks = btc.sync_blocks ? btc.sync_blocks.toLocaleString() : "—";
var eta = (typeof _calcBtcEta === "function") ? _calcBtcEta(btc.unit + "::" + btc.name, btc.sync_progress || 0) : "";
more +=
'<div class="widget clickable" id="w-btc" role="button" tabindex="0" title="' + escHtml(btc.name) + ' details">' +
'<div class="widget-chip chip-btc"><img src="/static/icons/' + escHtml(btc.icon) + '.svg" alt="" style="width:46px;height:46px;display:block;object-fit:contain"/></div>' +
'<div class="w-body"><h3>' + escHtml(btc.name) + ' — syncing timechain</h3>' +
'<div class="w-bar"><div class="w-bar-fill" style="width:' + pct + '%"></div></div>' +
'<div class="sub">Block <b>' + blocks + '</b> · ' + pct + '% · <span class="warn">' + escHtml(eta) + '</span></div></div>' +
'</div>';
} else {
var btcDone = null;
c.services.forEach(function (s) { if (s.unit === "bitcoind.service" && s.enabled) btcDone = s; });
if (btcDone) {
var blk = btcDone.sync_blocks ? btcDone.sync_blocks.toLocaleString() : "";
more +=
'<div class="widget clickable" id="w-btc" role="button" tabindex="0" title="' + escHtml(btcDone.name) + ' details">' +
'<div class="widget-chip chip-btc"><img src="/static/icons/' + escHtml(btcDone.icon) + '.svg" alt="" style="width:46px;height:46px;display:block;object-fit:contain"/></div>' +
'<div class="w-body"><h3>' + escHtml(btcDone.name) + '</h3><div class="sub"><span class="good">Fully synced</span>' + (blk ? ' · Block <b>' + blk + '</b>' : '') + '</div></div>' +
'</div>';
}
}
/* Updates — mirror the sidebar's read of the update state, so the card
never claims "up to date" while an update failed or needs a restart */
var upd = (typeof window._lastUpdateCheck === "object" && window._lastUpdateCheck) ? window._lastUpdateCheck : null;
var updStatus = (upd && upd.status) || "idle";
var updTitle, updSub, updChip;
if (updStatus === "failed") {
updTitle = "Update failed"; updSub = "Click to retry the update"; updChip = "chip-amber";
} else if (updStatus === "reboot_required") {
updTitle = "Restart required"; updSub = "Click to restart and finish the update"; updChip = "chip-amber";
} else if (updStatus === "running") {
updTitle = "Update in progress"; updSub = "Installing the new system generation"; updChip = "chip-amber";
} else if (upd && upd.available) {
updTitle = "Updates available"; updSub = "Click to review and update"; updChip = "chip-amber";
} else if (!upd) {
/* First check hasn't returned yet — never claim "up to date" before
a check has actually completed */
updTitle = "Checking for updates"; updSub = "Comparing with the latest Sovran_SystemsOS release"; updChip = "chip-green";
} else {
/* Updates are applied by the user, not automatically — the card must
not imply the OS updates itself */
updTitle = "System is up to date"; updSub = "Last check found no updates · Click to check again"; updChip = "chip-green";
}
more +=
'<div class="widget clickable" id="w-updates" role="button" tabindex="0" title="Check for system updates">' +
'<div class="widget-chip ' + updChip + '">' + icon("g-update") + '</div>' +
'<div class="w-body"><h3>' + updTitle + '</h3>' +
'<div class="sub">' + updSub + '</div></div>' +
'</div>';
$wcMore.innerHTML = more;
var wBtc = document.getElementById("w-btc");
if (wBtc) {
var svc = btc || btcDone;
if (svc) {
wBtc.addEventListener("click", function () { openServiceDetailModal(svc.unit, svc.name, svc.icon); });
wBtc.addEventListener("keydown", function (e) { if (e.key === "Enter") openServiceDetailModal(svc.unit, svc.name, svc.icon); });
}
}
var wUpd = document.getElementById("w-updates");
if (wUpd) {
wUpd.addEventListener("click", function () { openUpdateModal(); });
wUpd.addEventListener("keydown", function (e) { if (e.key === "Enter") openUpdateModal(); });
}
}
/* ── Systems Operational modal ────────────────────────────────── */
function isNodeRole() {
return (typeof _currentRole !== "undefined" && _currentRole === "node");
}
function hasEnabledDomainService(services) {
return DOMAIN_UNITS.some(function (u) {
return services.some(function (s) { return s.unit === u && s.enabled; });
});
}
function whoUsesPorts() {
if (isNodeRole()) {
return 'On this <strong>Bitcoin Node</strong> install, <strong>BTCPay Server</strong> and <strong>Lightning Wallet Connections (LNURL)</strong> are the domain services that use these ports.';
}
return 'All your domain services share ports 80 and 443 — Matrix, BTCPay Server, VaultWarden, Nextcloud, WordPress, Haven Relay, Lightning Wallet Connections, and Element Calling.';
}
function step(n, title, sub, value) {
return '<div class="sysstep"><div class="sysnum">' + n + '</div><div class="sysstep-x">' +
'<div class="sysstep-t">' + title + (sub ? ' <span class="sysstep-sub">· ' + sub + '</span>' : '') + '</div>' +
(value ? '<div class="sysval"><span class="sysval-text">' + value + '</span></div>' : '') +
'</div></div>';
}
function openSystemsModal() {
if (!$sysModal || !$sysBody) return;
var c = serviceCounts();
var html = "";
/* System status */
html += '<div class="sysmodal-card">' +
'<div class="sysmodal-card-title">' + icon("g-pulse") + 'System Status</div>' +
step(1, "Services running", "", String(c.running)) +
step(2, "Needs attention", "", c.attention ? escHtml(c.attentionNames.join(", ")) : "None") +
step(3, "Turned off", "", c.off ? escHtml(c.offNames.join(", ")) : "None") +
'</div>';
/* Router — a simple open / not-open verdict. How to open the ports is
covered during onboarding, so the modal does not repeat instructions.
When no domain service is enabled (a fresh Desktop-only install, a
Node with BTCPay/LNURL off, or everything turned off), there is
nothing to check on the router — say so instead of listing services
this machine does not have. */
if (!hasEnabledDomainService(c.services)) {
var noRouterDesc = isNodeRole()
? 'Ports 80 and 443 only need to be forwarded on your router if you turn on <strong>BTCPay Server</strong> or <strong>Lightning Wallet Connections (LNURL)</strong>. If you enable one of them, come back here to check your ports.'
: 'None of your services need ports forwarded from your router right now. If you turn on a service that uses a domain, come back here to check your ports.';
html += '<div class="sysmodal-card">' +
'<div class="sysmodal-card-title">' + icon("g-wifi") + 'Router</div>' +
'<div class="sysnote"><div class="sysnote-title">' + icon("g-check") + 'No router setup needed yet</div>' +
'<div class="sysnote-desc">' + noRouterDesc + '</div></div>' +
'</div>';
} else {
html += '<div class="sysmodal-card" id="sys-ports-card" style="display:none">' +
'<div class="sysmodal-card-title">' + icon("g-wifi") + 'Router</div>' +
'<div id="sys-ports-status"><div class="sysfineprint">Checking…</div></div>' +
'</div>';
}
/* Who uses these ports (only meaningful when a domain service is
actually enabled — otherwise the router note above covers it) */
if (hasEnabledDomainService(c.services)) {
html += '<div class="sysnote" style="margin-top:14px">' +
'<div class="sysnote-title">' + icon("g-antenna") + 'Who uses these ports</div>' +
'<div class="sysnote-desc">' + whoUsesPorts() + '</div></div>';
}
$sysBody.innerHTML = html;
$sysModal.classList.add("open");
/* Poll the first configured domain service and reduce its diagnostics
to one verdict: the ports are open or they are not. */
var portsCard = document.getElementById("sys-ports-card");
var portsEl = document.getElementById("sys-ports-status");
var units = DOMAIN_UNITS.filter(function (u) {
return c.services.some(function (s) { return s.unit === u && s.enabled; });
});
if (!units.length || !portsCard || !portsEl) return;
portsCard.style.display = "";
apiFetch("/api/service-detail/" + encodeURIComponent(units[0]))
.then(function (data) {
var steps = (data && data.domain_check_steps) || [];
var portsStep = null;
steps.forEach(function (s) {
if (Number(s.step) === 3 || /ports?\s*80/i.test(s.label || "")) portsStep = s;
});
if (!portsStep) { portsCard.style.display = "none"; return; }
if (portsStep.status === "ok") {
portsEl.innerHTML = '<div class="svc-detail-status" style="font-size:0.92rem"><span class="status-dot active"></span>Ports 80 and 443 are open</div>';
} else {
portsEl.innerHTML = '<div class="svc-detail-status" style="font-size:0.92rem"><span class="status-dot failed"></span>Ports 80 and 443 are not open</div>';
}
})
.catch(function () {
portsCard.style.display = "none";
});
}
function closeSystemsModal() {
if ($sysModal) $sysModal.classList.remove("open");
}
var sysClose = document.getElementById("systems-close-btn");
if (sysClose) sysClose.addEventListener("click", closeSystemsModal);
if ($sysModal) {
$sysModal.addEventListener("click", function (e) {
if (e.target === $sysModal) closeSystemsModal();
});
$sysModal.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeSystemsModal();
});
}
/* ── Public hook for tiles.js ────────────────────────────────── */
window.dashboardServicesUpdated = function () {
renderNav();
updateWelcomeMeta();
renderWidgets();
applyFilter();
};
// Initial view
showDashboard();
updateWelcomeMeta();
})();
@@ -0,0 +1,109 @@
/* Sovran_SystemsOS Hub — Shared domain and router instructions.
This is the single source of truth for the simple setup guidance shown in:
• First-boot onboarding for Server + Desktop
• Feature setup in the Hub
• Domain reconfiguration and troubleshooting
Keep the wording consistent by editing it here. */
"use strict";
function dpEsc(str) {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
/* Explain only what the user needs for the step in front of them. */
function renderDomainNeedsHtml(opts) {
opts = opts || {};
var serviceName = opts.serviceName || null;
var purpose = opts.purpose || "";
var html = "";
if (purpose === "btcpay") {
html += "<p><strong>Put your BTCPay Server online</strong></p>";
html += "<p>Let people donate Bitcoin, pay you, or visit your online store.</p>";
html += "<p>BTCPay Server needs a domain so people can reach it. "
+ "Sovran_SystemsOS walks you through getting your domain from "
+ '<a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">Njal.la</a>'
+ " and connecting it. Just follow the steps below.</p>";
} else if (serviceName) {
html += "<p>To make <strong>" + dpEsc(serviceName) + "</strong> available outside your home, it needs a domain.</p>";
html += "<p>Sovran_SystemsOS walks you through getting your domain from "
+ '<a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">Njal.la</a>'
+ " and connecting it. Just follow the steps below.</p>";
} else {
html += "<p>Your domain is the address people use to reach your services on the internet.</p>";
html += "<p>Sovran_SystemsOS walks you through getting your domain from "
+ '<a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">Njal.la</a>'
+ " and connecting your services. Just follow the steps below.</p>";
}
// Every variant above ends with a domain that points at the user's home
// connection. Say what that publishes (README: "Server + Desktop and your
// home IP address").
html += "<p>⚠️ <strong>Heads-up:</strong> your domain points at your home internet connection, "
+ "so anyone can look up your home IP address. Domain privacy does not hide it.</p>";
return html;
}
/* The shortest Njal.la steps that still tell the user exactly what to do. */
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>Get and connect your domain:</strong></p>';
html += '<ol style="margin:8px 0 0 16px;padding:0;line-height:1.7;">';
html += '<li>Open <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">Njal.la</a>, create an account, and get a domain.</li>';
html += "<li>Open your domain and add a <strong>Dynamic</strong> record for this service. "
+ "In the <strong>Name</strong> box, enter only the first part of the address. "
+ "For <code>" + hostExample + ".yourdomain.com</code>, enter <code>" + hostExample + "</code>. "
+ "If you are using the whole domain, leave the box blank or enter <code>@</code>.</li>";
html += "<li>Copy the update command Njal.la gives you and paste it " + pasteHint + ".</li>";
html += "</ol>";
return html;
}
/* One router task shared by all normal domain-based services. */
function renderRouterPortsHtml(opts) {
opts = opts || {};
var ipPart = opts.internalIp
? " to this computer at <strong>" + dpEsc(opts.internalIp) + "</strong>"
: " to this computer";
var html = "";
html += "🔌 <strong>One router task:</strong> in your router&rsquo;s <strong>Port Forwarding</strong> settings, "
+ "forward <strong>80 (TCP)</strong> and <strong>443 (TCP)</strong>"
+ ipPart + ". If your router asks for outside and inside port numbers, use the same number for both. "
+ "You only need to do this once. All your services share these two ports.";
if (opts.extraNote) {
html += " " + dpEsc(opts.extraNote);
}
return html;
}
/* Show generic wording immediately, then add the computer's address when found. */
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 is already shown. */ });
}
@@ -0,0 +1,273 @@
"use strict";
// ── Event listeners ───────────────────────────────────────────────
// if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); // moved to sidebar in tiles.js
if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal);
if ($btnCheckAgain) $btnCheckAgain.addEventListener("click", function() { openUpdateModal(); });
if ($updateCloseBtn) $updateCloseBtn.addEventListener("click", closeUpdateModal);
if ($btnReboot) $btnReboot.addEventListener("click", doReboot);
if ($btnSave) $btnSave.addEventListener("click", saveErrorReport);
if ($btnRetryUpdate) $btnRetryUpdate.addEventListener("click", retryUpdateStatus);
if ($btnRetryRun) $btnRetryRun.addEventListener("click", retryUpdateRun);
// Browser timers and requests may be suspended while an RDP session/tab is in
// the background. Reconcile immediately when the user returns instead of
// waiting for the next interval.
window.addEventListener("focus", resumeUpdateStatusAfterInterruption);
window.addEventListener("online", resumeUpdateStatusAfterInterruption);
document.addEventListener("visibilitychange", function() {
if (document.visibilityState === "visible") resumeUpdateStatusAfterInterruption();
});
if ($credsCloseBtn) $credsCloseBtn.addEventListener("click", closeCredsModal);
if ($supportCloseBtn) $supportCloseBtn.addEventListener("click", closeSupportModal);
// Logout button
if ($logoutBtn) $logoutBtn.addEventListener("click", function () {
fetch("/api/logout", { method: "POST", credentials: "same-origin" })
.finally(function () { window.location.replace("/login"); });
});
// Rebuild modal
if ($rebuildClose) $rebuildClose.addEventListener("click", closeRebuildModal);
if ($rebuildCloseHdr) $rebuildCloseHdr.addEventListener("click", closeRebuildModal);
if ($rebuildReboot) $rebuildReboot.addEventListener("click", doReboot);
if ($rebuildSave) $rebuildSave.addEventListener("click", saveRebuildErrorReport);
if ($rebuildModal) $rebuildModal.addEventListener("click", function(e) { if (e.target === $rebuildModal) closeRebuildModal(); });
// Domain setup modal
if ($domainSetupClose) $domainSetupClose.addEventListener("click", closeDomainSetupModal);
if ($domainSetupModal) $domainSetupModal.addEventListener("click", function(e) { if (e.target === $domainSetupModal) closeDomainSetupModal(); });
// SSL Email modal
if ($sslEmailClose) $sslEmailClose.addEventListener("click", closeSslEmailModal);
if ($sslEmailCancel) $sslEmailCancel.addEventListener("click", closeSslEmailModal);
if ($sslEmailModal) $sslEmailModal.addEventListener("click", function(e) { if (e.target === $sslEmailModal) closeSslEmailModal(); });
// Feature confirm modal
if ($featureConfirmClose) $featureConfirmClose.addEventListener("click", closeFeatureConfirm);
if ($featureConfirmCancel) $featureConfirmCancel.addEventListener("click", closeFeatureConfirm);
if ($featureConfirmModal) $featureConfirmModal.addEventListener("click", function(e) { if (e.target === $featureConfirmModal) closeFeatureConfirm(); });
if ($modal) $modal.addEventListener("click", function(e) { if (e.target === $modal) closeUpdateModal(); });
if ($credsModal) $credsModal.addEventListener("click", function(e) { if (e.target === $credsModal) closeCredsModal(); });
if ($supportModal) $supportModal.addEventListener("click", function(e) { if (e.target === $supportModal) closeSupportModal(); });
// Upgrade modal
if ($upgradeCloseBtn) $upgradeCloseBtn.addEventListener("click", closeUpgradeModal);
if ($upgradeCancelBtn) $upgradeCancelBtn.addEventListener("click", closeUpgradeModal);
if ($upgradeModal) $upgradeModal.addEventListener("click", function(e) { if (e.target === $upgradeModal) closeUpgradeModal(); });
// Restart confirm dialog
if ($restartConfirmCancel) $restartConfirmCancel.addEventListener("click", closeRestartConfirmDialog);
if ($restartConfirmModal) $restartConfirmModal.addEventListener("click", function(e) { if (e.target === $restartConfirmModal) closeRestartConfirmDialog(); });
if ($restartConfirmModal) $restartConfirmModal.addEventListener("keydown", function(e) { if (e.key === "Escape") closeRestartConfirmDialog(); });
// Header Reboot button
if ($headerRebootBtn) $headerRebootBtn.addEventListener("click", function() { openRestartConfirmDialog(); });
if ($restartConfirmOk) $restartConfirmOk.addEventListener("click", function() {
if ($restartConfirmOk.disabled) return;
$restartConfirmOk.disabled = true;
closeRestartConfirmDialog();
doReboot();
});
// Reboot error card buttons
var $rebootErrorCloseBtn = document.getElementById("reboot-error-close-btn");
var $rebootErrorRetryBtn = document.getElementById("reboot-error-retry-btn");
if ($rebootErrorCloseBtn) $rebootErrorCloseBtn.addEventListener("click", function() {
if ($rebootOverlay) $rebootOverlay.classList.remove("visible");
});
if ($rebootErrorRetryBtn) $rebootErrorRetryBtn.addEventListener("click", doReboot);
// ── Upgrade modal functions ───────────────────────────────────────
function openUpgradeModal() {
if ($upgradeModal) $upgradeModal.classList.add("open");
}
function closeUpgradeModal() {
if ($upgradeModal) $upgradeModal.classList.remove("open");
}
// ── Restart confirm dialog functions ─────────────────────────────
var _restartDialogOpener = null;
function openRestartConfirmDialog() {
if (!$restartConfirmModal) return;
_restartDialogOpener = document.activeElement;
// Detect conflicting operations
var isOperationInProgress = !!_updatePollTimer || !!_rebuildPollTimer;
if ($restartConflictBox) $restartConflictBox.style.display = isOperationInProgress ? "" : "none";
if ($restartConfirmOk) $restartConfirmOk.disabled = isOperationInProgress;
$restartConfirmModal.classList.add("open");
// Focus Cancel initially for safety
var cancelBtn = document.getElementById("restart-confirm-cancel-btn");
if (cancelBtn) setTimeout(function() { cancelBtn.focus(); }, 50);
}
function closeRestartConfirmDialog() {
if ($restartConfirmModal) $restartConfirmModal.classList.remove("open");
// Re-enable confirm button for next open
if ($restartConfirmOk) $restartConfirmOk.disabled = false;
// Return focus to the element that opened the dialog
if (_restartDialogOpener && _restartDialogOpener.focus) {
try { _restartDialogOpener.focus(); } catch (_) {}
_restartDialogOpener = null;
}
}
async function doUpgradeToServer() {
var confirmBtn = $upgradeConfirmBtn;
if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = "Upgrading…"; }
closeUpgradeModal();
// Reuse the rebuild modal to show reboot progress
_rebuildFeatureName = "Server + Desktop";
_rebuildIsEnabling = true;
openRebuildModal();
try {
await apiFetch("/api/role/upgrade-to-server", { method: "POST" });
// Server is rebooting — show message and wait for it to come back
if ($rebuildStatus) $rebuildStatus.textContent = "Rebooting — the setup wizard will guide you through domain and port configuration…";
if ($rebuildSpinner) $rebuildSpinner.classList.add("spinning");
// Poll until server comes back, then redirect to onboarding
var pollInterval = setInterval(async function() {
try {
await apiFetch("/api/ping");
clearInterval(pollInterval);
window.location.href = "/onboarding";
} catch (_) {
// Server still down — keep polling
}
}, 3000);
} catch (err) {
if ($rebuildStatus) $rebuildStatus.textContent = "✗ Upgrade failed: " + err.message;
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
if ($rebuildClose) $rebuildClose.disabled = false;
if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.textContent = "Yes, Upgrade"; }
}
}
if ($upgradeConfirmBtn) $upgradeConfirmBtn.addEventListener("click", doUpgradeToServer);
// ── First-login security banner ───────────────────────────────────
function showSecurityBanner() {
var existing = document.getElementById("security-first-login-banner");
if (existing) return;
var banner = document.createElement("div");
banner.id = "security-first-login-banner";
banner.className = "security-first-login-banner";
banner.innerHTML =
'<div class="security-banner-content">' +
'<span class="security-banner-icon">\uD83D\uDEE1</span>' +
'<span class="security-banner-text">' +
'<strong>Did someone else set up this machine?</strong> ' +
'If this computer was pre-configured by another person, go to ' +
'<strong>Menu \u2192 Security</strong> to reset all passwords and keys. ' +
'This ensures only you have access.' +
'</span>' +
'</div>' +
'<button class="security-banner-dismiss" id="security-banner-dismiss-btn" title="Dismiss">\u2715</button>';
var contentArea = document.querySelector(".content");
if (contentArea) {
contentArea.insertAdjacentElement("afterbegin", banner);
} else {
document.body.insertAdjacentElement("afterbegin", banner);
}
var dismissBtn = document.getElementById("security-banner-dismiss-btn");
if (dismissBtn) {
dismissBtn.addEventListener("click", async function() {
banner.remove();
try {
await apiFetch("/api/security/banner-dismiss", { method: "POST" });
} catch (_) {
// Non-fatal
}
});
}
}
// ── 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;
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];
} catch (_) {
// If config cannot be loaded, continue with the normal service fallback.
cfg = null;
}
if (onboardingStatus && !onboardingStatus.complete) {
window.location.href = "/onboarding";
return;
}
// 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 {
if (!cfg) throw new Error("Hub config unavailable");
_currentRole = cfg.role || "server_plus_desktop";
if (cfg.category_order) {
for (var i = 0; i < cfg.category_order.length; i++) {
_categoryLabels[cfg.category_order[i][0]] = cfg.category_order[i][1];
}
}
var badge = document.getElementById("role-badge");
if (badge && cfg.role_label) badge.textContent = cfg.role_label;
window._roleLabel = cfg.role_label || "";
loadNetwork();
checkUpdates();
await refreshServices();
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
if (cfg.feature_manager) {
loadFeatureManager();
}
loadAutolaunchToggle();
} catch (_) {
loadNetwork();
checkUpdates();
await refreshServices();
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
loadAutolaunchToggle();
}
// If the page was reloaded or the RDP/browser session resumed during an
// update, reopen the modal from the persisted backend state. This also
// surfaces a completed update that is waiting for its activation reboot.
await restoreUpdateModalIfNeeded();
}
document.addEventListener("DOMContentLoaded", init);
@@ -0,0 +1,680 @@
"use strict";
// ── Feature confirm modal ─────────────────────────────────────────
function openFeatureConfirm(message, onConfirm) {
if (!$featureConfirmModal) return;
if ($featureConfirmMsg) $featureConfirmMsg.textContent = message;
$featureConfirmModal.classList.add("open");
// Replace ok handler
var newOk = $featureConfirmOk.cloneNode(true);
$featureConfirmOk.parentNode.replaceChild(newOk, $featureConfirmOk);
newOk.addEventListener("click", function() {
closeFeatureConfirm();
onConfirm();
});
}
function closeFeatureConfirm() {
if ($featureConfirmModal) $featureConfirmModal.classList.remove("open");
}
// ── SSL Email modal ───────────────────────────────────────────────
function openSslEmailModal(onSaved) {
if (!$sslEmailModal) return;
if ($sslEmailInput) $sslEmailInput.value = "";
$sslEmailModal.classList.add("open");
// Replace save handler
var newSave = $sslEmailSave.cloneNode(true);
$sslEmailSave.parentNode.replaceChild(newSave, $sslEmailSave);
newSave.addEventListener("click", async function() {
var email = $sslEmailInput ? $sslEmailInput.value.trim() : "";
if (!email) { alert("Please enter an email address."); return; }
newSave.disabled = true;
newSave.textContent = "Saving…";
try {
await apiFetch("/api/domains/set-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email }),
});
closeSslEmailModal();
onSaved();
} catch (err) {
newSave.disabled = false;
newSave.textContent = "Save";
alert("Failed to save email. Please try again.");
}
});
}
function closeSslEmailModal() {
if ($sslEmailModal) $sslEmailModal.classList.remove("open");
}
// ── Domain Setup modal ────────────────────────────────────────────
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 isBtcpay = feat.id === "btcpay-web";
var npubField = "";
if (feat.id === "haven") {
var currentNpub = "";
if (feat.extra_fields && feat.extra_fields.length > 0) {
for (var i = 0; i < feat.extra_fields.length; i++) {
if (feat.extra_fields[i].id === "nostr_npub") {
currentNpub = feat.extra_fields[i].current_value || "";
break;
}
}
}
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 needs its own address.</strong> ' +
'Use a new address such as <code>lightning.yourdomain.com</code>. Do not use an address already connected to another service.' +
'</div>'
: '';
var domainPlaceholder = isWalletConnections ? "lightning.yourdomain.com" : (isBtcpay ? "btcpay.yourdomain.com" : "myservice.example.com");
var domainLabelExample = isWalletConnections ? "lightning.yourdomain.com" : (isBtcpay ? "btcpay.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" : (isBtcpay ? "btcpay" : "call");
var purpose = isBtcpay ? "btcpay" : "";
$domainSetupBody.innerHTML =
'<div class="domain-setup-intro">' +
nwcWarning +
renderDomainNeedsHtml({ serviceName: feat.name, hostExample: hostExample, purpose: purpose }) +
renderNjallaStepsHtml({ hostExample: hostExample, pasteHint: "below" }) +
'<p style="margin-top:10px;">Enter the address for this service and paste the update command from Njal.la.</p>' +
'</div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service address (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 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 update command from Njal.la</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>';
document.getElementById("domain-setup-cancel-btn").addEventListener("click", closeDomainSetupModal);
document.getElementById("domain-setup-save-btn").addEventListener("click", async function() {
var subdomain = (document.getElementById("domain-subdomain-input") || {}).value || "";
var ddnsUrl = (document.getElementById("domain-ddns-input") || {}).value || "";
var npub = document.getElementById("domain-npub-input") ? (document.getElementById("domain-npub-input").value || "") : "";
subdomain = subdomain.trim();
ddnsUrl = ddnsUrl.trim();
npub = npub.trim();
if (!subdomain) { alert("Please enter a domain."); return; }
if (feat.id === "haven" && !npub) { alert("Please enter your Nostr public key."); return; }
var saveBtn = document.getElementById("domain-setup-save-btn");
saveBtn.disabled = true;
saveBtn.textContent = "Saving…";
try {
await apiFetch("/api/domains/set", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
domain_name: feat.domain_name,
domain: subdomain,
ddns_url: ddnsUrl,
}),
});
closeDomainSetupModal();
onSaved(npub);
} 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);
}
});
$domainSetupModal.classList.add("open");
}
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 = "";
if (feat.extra_fields && feat.extra_fields.length > 0) {
for (var i = 0; i < feat.extra_fields.length; i++) {
if (feat.extra_fields[i].id === "nostr_npub") {
currentNpub = feat.extra_fields[i].current_value || "";
break;
}
}
}
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 needs its own address.</strong> ' +
'Use a new address such as <code>lightning.yourdomain.com</code>. Do not use an address already connected to another service.' +
'</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>' +
'<li>Log into your Njal.la dashboard at <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">https://njal.la</a></li>' +
'<li>Find the DNS record for <strong>' + escHtml(currentDomain || "your domain") + '</strong>. In Njal.la\'s Name field, note that only the host part is stored (the word before the domain) — not the full domain.</li>' +
'<li>Verify it has a <strong>Dynamic</strong> record pointing to your current external IP:<br>' +
'<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>' +
'</ol>' +
'</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>' +
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>';
document.getElementById("domain-setup-cancel-btn").addEventListener("click", closeDomainSetupModal);
document.getElementById("domain-setup-save-btn").addEventListener("click", async function() {
var subdomain = (document.getElementById("domain-subdomain-input") || {}).value || "";
var ddnsUrl = (document.getElementById("domain-ddns-input") || {}).value || "";
var npub = document.getElementById("domain-npub-input") ? (document.getElementById("domain-npub-input").value || "") : "";
subdomain = subdomain.trim();
ddnsUrl = ddnsUrl.trim();
npub = npub.trim();
if (!subdomain) { alert("Please enter a subdomain."); return; }
if (feat.id === "haven" && !npub) { alert("Please enter your Nostr public key."); return; }
var saveBtn = document.getElementById("domain-setup-save-btn");
saveBtn.disabled = true;
saveBtn.textContent = "Saving…";
try {
await apiFetch("/api/domains/set", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
domain_name: feat.domain_name,
domain: subdomain,
ddns_url: ddnsUrl,
}),
});
closeDomainSetupModal();
onSaved(npub);
} 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);
}
});
$domainSetupModal.classList.add("open");
}
function closeDomainSetupModal() {
if ($domainSetupModal) $domainSetupModal.classList.remove("open");
}
// ── Port Requirements modal ───────────────────────────────────────
function openPortRequirementsModal(featureName, ports, onContinue) {
if (!$portReqModal || !$portReqBody) return;
var continueBtn = onContinue
? '<button class="btn btn-primary" id="port-req-continue-btn">I Understand — Continue</button>'
: '';
function renderPortRequirements(internalIp) {
$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>' +
'<div class="domain-field-actions">' +
'<button class="btn btn-close-modal" id="port-req-dismiss-btn">Dismiss</button>' +
continueBtn +
'</div>';
document.getElementById("port-req-dismiss-btn").onclick = function() {
closePortRequirementsModal();
};
if (onContinue) {
document.getElementById("port-req-continue-btn").onclick = function() {
closePortRequirementsModal();
onContinue();
};
}
}
$portReqModal.classList.add("open");
renderPortRequirements(null);
fetch("/api/network")
.then(function(r) { return r.json(); })
.then(function(data) {
if (!$portReqModal.classList.contains("open")) return;
var internalIp = (data.internal_ip && data.internal_ip !== "unavailable")
? data.internal_ip : null;
renderPortRequirements(internalIp);
})
.catch(function(err) {
console.warn("Failed to fetch network info for port requirements modal:", err);
});
}
function closePortRequirementsModal() {
if ($portReqModal) $portReqModal.classList.remove("open");
}
if ($portReqClose) {
$portReqClose.addEventListener("click", closePortRequirementsModal);
}
// ── Feature toggle logic ──────────────────────────────────────────
async function performFeatureToggle(featId, enabled, extra) {
// Look up feature name for the rebuild modal
_rebuildIsEnabling = enabled;
_rebuildFeatureName = featId;
if (_featuresData) {
var found = _featuresData.features.find(function(f) { return f.id === featId; });
if (found) _rebuildFeatureName = found.name;
}
try {
var res = await fetch("/api/features/toggle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ feature: featId, enabled: enabled, extra: extra || {} }),
});
var body = await res.json();
if (!res.ok) {
if (body && body.error === "domain_required") {
alert("Domain not configured for this feature. Please configure it first.");
} else {
alert("Error: " + (body.detail || body.error || "Unknown error"));
}
loadFeatureManager();
return;
}
openRebuildModal();
} catch (err) {
alert("Failed to toggle feature: " + err);
loadFeatureManager();
}
}
function handleFeatureToggle(feat, newEnabled) {
if (!newEnabled) {
// Disable: ask confirmation
var disableMessage = (feat.id === "bitcoin-tor-gossip")
? "This will stop advertising your Bitcoin Core onion address to other peers. Nodes that already know the address can still connect through Tor, and no clearnet port will be opened. The system will rebuild. Continue?"
: "This will disable " + feat.name + ". The system will rebuild. Continue?";
openFeatureConfirm(
disableMessage,
function() { performFeatureToggle(feat.id, false, {}); }
);
return;
}
// Enabling
var conflictNames = [];
if (feat.conflicts_with && feat.conflicts_with.length > 0 && _featuresData) {
feat.conflicts_with.forEach(function(cid) {
var cf = _featuresData.features.find(function(f) { return f.id === cid; });
if (cf && cf.enabled) conflictNames.push(cf.name);
});
}
function proceedAfterPortCheck() {
// Check SSL email first
if (!_featuresData || !_featuresData.ssl_email_configured) {
if (feat.needs_domain) {
openSslEmailModal(function() {
// After ssl email saved, check domain
checkDomainAndEnable(feat, {});
});
return;
}
}
if (feat.needs_domain && !feat.domain_configured) {
checkDomainAndEnable(feat, {});
return;
}
if (feat.id === "haven") {
var npub = "";
if (feat.extra_fields) {
var ef = feat.extra_fields.find(function(e) { return e.id === "nostr_npub"; });
if (ef) npub = ef.current_value || "";
}
if (!npub) {
// Need to collect npub via domain modal
openDomainSetupModal(feat, function(collectedNpub) {
performFeatureToggle(feat.id, true, { nostr_npub: collectedNpub });
});
return;
}
}
performFeatureToggle(feat.id, true, {});
}
function proceedAfterConflictCheck() {
var ports = feat.port_requirements || [];
if (ports.length === 0) {
proceedAfterPortCheck();
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);
}
if (feat.id === "bitcoin-tor-gossip") {
var torGossipConfirmMsg = "This will advertise your Bitcoin Core .onion P2P address through Bitcoin peer gossip. More Tor-capable nodes may discover your node and request historical blocks during IBD, which can use significant upload bandwidth. Your home IP remains hidden and no clearnet port or router forwarding is opened. Continue?";
openFeatureConfirm(torGossipConfirmMsg, proceedAfterConflictCheck);
} else if (conflictNames.length > 0) {
openFeatureConfirm("This will disable " + conflictNames.join(", ") + ". Continue?", proceedAfterConflictCheck);
} else {
proceedAfterConflictCheck();
}
}
function checkDomainAndEnable(feat, extra) {
openDomainSetupModal(feat, function(collectedNpub) {
var extraData = {};
if (collectedNpub) extraData.nostr_npub = collectedNpub;
performFeatureToggle(feat.id, true, extraData);
});
}
// ── Feature Manager rendering ─────────────────────────────────────
async function loadFeatureManager() {
try {
var data = await apiFetch("/api/features");
_featuresData = data;
// Feature Manager is now integrated into tile modals; sidebar rendering removed.
} catch (err) {
console.warn("Failed to load features:", err);
}
}
function _checkFeatureManagerDomains(data) {
// Collect all features with a configured domain
var featsWithDomain = (data.features || []).filter(function(f) {
return f.needs_domain && f.domain_configured;
});
if (!featsWithDomain.length) return;
// Get the actual domain values from /api/domains/status, then check them
fetch("/api/domains/status")
.then(function(r) { return r.json(); })
.then(function(statusData) {
var domainFileMap = statusData.domains || {};
// Build list of domains to check and a map from domain value → feature id
var domainsToCheck = [];
var domainToFeatIds = {};
featsWithDomain.forEach(function(feat) {
var domainName = feat.domain_name;
var domainVal = domainName ? domainFileMap[domainName] : null;
if (domainVal) {
domainsToCheck.push(domainVal);
if (!domainToFeatIds[domainVal]) domainToFeatIds[domainVal] = [];
domainToFeatIds[domainVal].push(feat.id);
} else {
// Domain file missing — update badge to warn
_updateFeatureDomainBadge(feat.id, null, "unresolvable");
}
});
if (!domainsToCheck.length) return;
return fetch("/api/domains/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ domains: domainsToCheck }),
})
.then(function(r) { return r.json(); })
.then(function(checkData) {
(checkData.domains || []).forEach(function(d) {
var featIds = domainToFeatIds[d.domain] || [];
featIds.forEach(function(featId) {
_updateFeatureDomainBadge(featId, d.domain, d.status);
});
});
});
})
.catch(function() {});
}
function _updateFeatureDomainBadge(featId, domainVal, status) {
var section = $sidebarFeatures.querySelector(".feature-manager-section");
if (!section) return;
// Find the card — cards don't have a data-feat-id, so find via name match
var badges = section.querySelectorAll(".feature-domain-badge.configured");
badges.forEach(function(badge) {
var domainNameAttr = badge.getAttribute("data-domain-name");
// Match by domain_name attribute — we need to look up the feat's domain_name
var feat = _featuresData && _featuresData.features
? _featuresData.features.find(function(f) { return f.id === featId; })
: null;
if (!feat) return;
if (domainNameAttr !== (feat.domain_name || "")) return;
var lbl = badge.querySelector(".feature-domain-label");
if (!lbl) return;
lbl.classList.remove("feature-domain-label--checking");
if (status === "connected") {
lbl.className = "feature-domain-label feature-domain-label--ok";
lbl.textContent = (domainVal || "Domain") + " ✓";
} else if (status === "dns_mismatch") {
lbl.className = "feature-domain-label feature-domain-label--warn";
lbl.textContent = (domainVal || "Domain") + " (IP mismatch)";
} else if (status === "unresolvable") {
lbl.className = "feature-domain-label feature-domain-label--error";
lbl.textContent = (domainVal || "Domain") + " (DNS error)";
} else {
lbl.className = "feature-domain-label feature-domain-label--warn";
lbl.textContent = (domainVal || "Domain") + " (unknown)";
}
});
}
function renderFeatureManager(data) {
// Remove old feature manager section if it exists
var old = $sidebarFeatures.querySelector(".feature-manager-section");
if (old) old.parentNode.removeChild(old);
var section = document.createElement("div");
section.className = "category-section feature-manager-section";
section.dataset.category = "feature-manager";
section.innerHTML = '<div class="section-header">Feature Manager</div><hr class="section-divider" />';
// Group by sub-category
var grouped = {};
for (var i = 0; i < data.features.length; i++) {
var f = data.features[i];
var cat = f.category || "other";
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push(f);
}
var orderedCats = FEATURE_SUBCATEGORY_ORDER.filter(function(k) { return grouped[k]; });
Object.keys(grouped).forEach(function(k) {
if (orderedCats.indexOf(k) === -1) orderedCats.push(k);
});
for (var j = 0; j < orderedCats.length; j++) {
var catKey = orderedCats[j];
var feats = grouped[catKey];
if (!feats || feats.length === 0) continue;
var subcat = document.createElement("div");
subcat.className = "feature-subcategory";
var subcatLabel = FEATURE_SUBCATEGORY_LABELS[catKey] || catKey;
subcat.innerHTML = '<div class="feature-subcategory-header">' + escHtml(subcatLabel) + '</div>';
var cardsWrap = document.createElement("div");
cardsWrap.className = "feature-cards-wrap";
for (var k = 0; k < feats.length; k++) {
cardsWrap.appendChild(buildFeatureCard(feats[k]));
}
subcat.appendChild(cardsWrap);
section.appendChild(subcat);
}
$sidebarFeatures.appendChild(section);
}
function buildFeatureCard(feat) {
var card = document.createElement("div");
card.className = "feature-card";
var conflictHtml = "";
if (feat.conflicts_with && feat.conflicts_with.length > 0) {
var conflictNames = feat.conflicts_with.map(function(cid) {
if (!_featuresData) return cid;
var cf = _featuresData.features.find(function(f) { return f.id === cid; });
return cf ? cf.name : cid;
});
conflictHtml = '<div class="feature-conflict-warning">⚠ Conflicts with: ' + escHtml(conflictNames.join(", ")) + '</div>';
}
var domainHtml = "";
if (feat.needs_domain) {
if (feat.domain_configured) {
domainHtml = '<div class="feature-domain-badge configured" data-domain-name="' + escHtml(feat.domain_name || '') + '">'
+ '<span class="feature-domain-icon">🌐</span>'
+ '<span class="feature-domain-label feature-domain-label--checking">Domain: Checking\u2026</span>'
+ '</div>';
} else {
domainHtml = '<div class="feature-domain-badge not-configured">'
+ '<span class="feature-domain-icon">🌐</span>'
+ '<span class="feature-domain-label feature-domain-label--warn">Domain: Not configured</span>'
+ '</div>';
}
}
var statusText = feat.enabled ? "Enabled" : "Disabled";
card.innerHTML =
'<div class="feature-card-top">' +
'<div class="feature-card-info">' +
'<div class="feature-card-name">' + escHtml(feat.name) + '</div>' +
'<div class="feature-card-desc">' + escHtml(feat.description) + '</div>' +
'</div>' +
'<label class="feature-toggle' + (feat.enabled ? " active" : "") + '" title="Toggle ' + escHtml(feat.name) + '">' +
'<input type="checkbox" class="feature-toggle-input"' + (feat.enabled ? " checked" : "") + ' />' +
'<span class="feature-toggle-slider"></span>' +
'</label>' +
'</div>' +
domainHtml +
conflictHtml +
'<div class="feature-card-status">Status: ' + escHtml(statusText) + '</div>';
var toggle = card.querySelector(".feature-toggle-input");
var toggleLabel = card.querySelector(".feature-toggle");
toggle.addEventListener("change", function() {
var newEnabled = toggle.checked;
// Revert visually until confirmed
toggle.checked = feat.enabled;
if (newEnabled) { toggleLabel.classList.remove("active"); } else { toggleLabel.classList.add("active"); }
handleFeatureToggle(feat, newEnabled);
});
return card;
}
// ── Auto-launch toggle ────────────────────────────────────────────
async function loadAutolaunchToggle() {
try {
var data = await apiFetch("/api/autolaunch/status");
renderAutolaunchToggle(data.enabled);
} catch (err) {
console.warn("Failed to load autolaunch status:", err);
}
}
function renderAutolaunchToggle(enabled) {
// The preference lives in the Systems Operational modal (moved from the sidebar)
var slot = document.getElementById("autolaunch-slot");
if (!slot) return;
var old = slot.querySelector(".autolaunch-section");
if (old) old.parentNode.removeChild(old);
var section = document.createElement("div");
section.className = "category-section autolaunch-section";
section.innerHTML =
'<div class="feature-card">' +
'<div class="feature-card-top">' +
'<div class="feature-card-info">' +
'<div class="feature-card-name">Auto-launch Hub on Login</div>' +
'<div class="feature-card-desc">Automatically open the Sovran Hub dashboard in your browser when you log in to the desktop.</div>' +
'</div>' +
'<label class="feature-toggle' + (enabled ? " active" : "") + '" id="autolaunch-toggle-label" title="Toggle auto-launch">' +
'<input type="checkbox" class="feature-toggle-input" id="autolaunch-toggle-input"' + (enabled ? " checked" : "") + ' />' +
'<span class="feature-toggle-slider"></span>' +
'</label>' +
'</div>' +
'</div>';
slot.appendChild(section);
var input = document.getElementById("autolaunch-toggle-input");
var label = document.getElementById("autolaunch-toggle-label");
if (!input || !label) return;
input.addEventListener("change", async function() {
var newEnabled = input.checked;
// Revert visually until confirmed
input.checked = !newEnabled;
if (newEnabled) { label.classList.remove("active"); } else { label.classList.add("active"); }
input.disabled = true;
try {
await apiFetch("/api/autolaunch/toggle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: newEnabled }),
});
input.checked = newEnabled;
if (newEnabled) { label.classList.add("active"); } else { label.classList.remove("active"); }
} catch (err) {
alert("Failed to update auto-launch setting. Please try again.");
} finally {
input.disabled = false;
}
});
}
@@ -0,0 +1,156 @@
"use strict";
// ── Helpers ───────────────────────────────────────────────────────
function tileId(svc) { return svc.unit + "::" + svc.name; }
function statusClass(health) {
if (!health) return "unknown";
if (health === "healthy") return "active";
if (health === "needs_attention") return "needs-attention";
if (health === "active") return "active"; // backwards compat
if (health === "inactive") return "inactive";
if (health === "failed") return "failed";
if (health === "disabled") return "disabled";
if (health === "syncing") return "syncing";
if (STATUS_LOADING_STATES.has(health)) return "loading";
if (health === "checking_reachability") return "checking-reachability";
return "unknown";
}
function statusText(health, enabled) {
if (!enabled) return "Disabled";
if (health === "healthy") return "Active";
if (health === "needs_attention") return "Needs Attention";
if (health === "active") return "Active";
if (health === "inactive") return "Inactive";
if (health === "failed") return "Failed";
if (health === "syncing") return "Syncing\u2026";
if (!health || health === "unknown") return "Unknown";
if (STATUS_LOADING_STATES.has(health)) return health;
if (health === "checking_reachability") return "Checking\u2026";
return health;
}
function escHtml(str) {
return String(str).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");
}
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>';
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
? 'To make <strong>' + escHtml(opts.serviceName) + '</strong> reachable from outside your home, forward these ports to ' + ipHtml + ':'
: 'Forward these ports to ' + ipHtml + ':';
return '<p class="' + introClass + '">' + forWhat + '</p>' +
'<p class="port-req-steps" style="margin-top:6px;margin-bottom:10px;font-size:0.92em;color:#555;">' +
'Set the internal and external port to the <strong>same number</strong>. Match <strong>TCP</strong> or <strong>UDP</strong> exactly. For ranges like <strong>40000-40099</strong>, use your router&rsquo;s range fields (start 40000, end 40099).' +
'</p>' +
'<table class="' + tableClass + '">' +
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Used for</th></tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>';
}
function formatDuration(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return h + "h " + m + "m " + s + "s";
if (m > 0) return m + "m " + s + "s";
return s + "s";
}
// ── Fetch wrappers ────────────────────────────────────────────────
// Avoid issuing multiple redirects when several startup requests discover an
// expired session at the same time.
let _authRedirectInProgress = false;
async function apiFetch(path, options) {
const res = await fetch(path, options || {});
if (res.status === 401) {
if (!_authRedirectInProgress) {
_authRedirectInProgress = true;
window.location.replace("/login");
}
throw new Error("Unauthenticated");
}
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) {}
throw new Error(detail);
}
return res.json();
}
async function apiFetchWithTimeout(path, options, timeoutMs) {
var controller = new AbortController();
var fetchOptions = Object.assign({}, options || {});
fetchOptions.signal = controller.signal;
var timer = setTimeout(function() { controller.abort(); }, timeoutMs);
try {
return await apiFetch(path, fetchOptions);
} catch (err) {
if (controller.signal.aborted) {
var timeoutError = new Error("Request timed out");
timeoutError.name = "TimeoutError";
throw timeoutError;
}
throw err;
} finally {
clearTimeout(timer);
}
}
@@ -0,0 +1,143 @@
"use strict";
// ── Rebuild modal ─────────────────────────────────────────────────
// Status line + header pill for the rebuild dialog (same presentation
// contract as the update dialog's _setUpdateStatus).
function _setRebuildStatus(text) {
if ($rebuildStatus) $rebuildStatus.textContent = text;
if ($rebuildPill) {
var cls = "upd-pill";
var html;
if (text.charAt(0) === "✓") {
if (text.indexOf("restart required") !== -1) {
cls += " st-needs-attention"; html = '<span class="status-dot needs-attention pulse"></span>Restart required';
} else {
cls += " st-active"; html = '<span class="status-dot active"></span>Done';
}
} else if (text.charAt(0) === "✗") {
cls += " st-failed"; html = '<span class="status-dot failed"></span>Failed';
} else {
cls += " st-loading"; html = '<span class="status-dot loading pulse"></span>Applying…';
}
$rebuildPill.className = cls;
$rebuildPill.innerHTML = html;
}
if ($rebuildStatus) {
var msg = "update-status-msg";
if (text.charAt(0) === "✓") $rebuildStatus.className = (text.indexOf("restart required") !== -1) ? msg + " st-warn" : msg + " st-ok";
else if (text.charAt(0) === "✗") $rebuildStatus.className = msg + " st-err";
else $rebuildStatus.className = msg;
}
}
function openRebuildModal() {
if (!$rebuildModal) return;
_rebuildLog = "";
_rebuildLogOffset = 0;
_rebuildServerDown = false;
_rebuildFinished = false;
_rebuildPollInFlight = false;
_rebuildPollFailures = 0;
if ($rebuildLog) { $rebuildLog.textContent = ""; $rebuildLog.style.display = "none"; }
var action = _rebuildIsEnabling ? "Enabling" : "Disabling";
var label = _rebuildFeatureName || "feature";
_setRebuildStatus(action + " " + label + "…");
if ($rebuildSpinner) $rebuildSpinner.classList.add("spinning");
if ($rebuildReboot) $rebuildReboot.style.display = "none";
if ($rebuildSave) $rebuildSave.style.display = "none";
if ($rebuildClose) $rebuildClose.disabled = true;
if ($rebuildCloseHdr) $rebuildCloseHdr.disabled = true;
$rebuildModal.classList.add("open");
// Delay first poll slightly to let the rebuild service start and clear stale log
setTimeout(startRebuildPoll, 1500);
}
function closeRebuildModal() {
if ($rebuildModal) $rebuildModal.classList.remove("open");
stopRebuildPoll();
}
function appendRebuildLog(text) {
if (!text) return;
_rebuildLog += text;
// Log is collected silently for error reports — not displayed to user
}
function startRebuildPoll() {
if (_rebuildPollTimer) clearInterval(_rebuildPollTimer);
pollRebuildStatus();
_rebuildPollTimer = setInterval(pollRebuildStatus, UPDATE_POLL_INTERVAL);
}
function stopRebuildPoll() {
if (_rebuildPollTimer) { clearInterval(_rebuildPollTimer); _rebuildPollTimer = null; }
}
async function pollRebuildStatus() {
if (_rebuildFinished || _rebuildPollInFlight) return;
_rebuildPollInFlight = true;
try {
var data = await apiFetchWithTimeout(
"/api/rebuild/status?offset=" + _rebuildLogOffset,
{ cache: "no-store" },
STATUS_POLL_FETCH_TIMEOUT
);
_rebuildPollFailures = 0;
if (_rebuildServerDown) { _rebuildServerDown = false; }
if (data.log) appendRebuildLog(data.log);
_rebuildLogOffset = data.offset;
if (data.running) return;
_rebuildFinished = true;
stopRebuildPoll();
if (data.result === "reboot_required") {
onRebuildDone("reboot_required");
} else {
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, reload to
// re-authenticate and show the resulting feature state.
if (_rebuildPollFailures >= STATUS_POLL_MAX_FAILURES) {
_rebuildFinished = true;
stopRebuildPoll();
window.location.reload();
return;
}
if (!_rebuildServerDown) { _rebuildServerDown = true; _setRebuildStatus("Applying changes…"); }
} finally {
_rebuildPollInFlight = false;
}
}
function onRebuildDone(result) {
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
if ($rebuildClose) $rebuildClose.disabled = false;
if ($rebuildCloseHdr) $rebuildCloseHdr.disabled = false;
if (result === true) {
_setRebuildStatus("✓ Done");
// Auto-reload the page after a short delay so tiles and toggles reflect the new state
setTimeout(function() { window.location.reload(); }, 1200);
} else if (result === "reboot_required") {
_setRebuildStatus("✓ Done — restart required");
if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex";
} else {
_setRebuildStatus("✗ Something went wrong");
if ($rebuildSave) $rebuildSave.style.display = "inline-flex";
if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex";
}
}
function saveRebuildErrorReport() {
var blob = new Blob([_rebuildLog], { type: "text/plain" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "sovran-rebuild-error-" + new Date().toISOString().split(".")[0].replace(/:/g, "-") + ".txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
@@ -0,0 +1,249 @@
"use strict";
// ── Security Modal ────────────────────────────────────────────────
function openSecurityModal() {
if ($supportModal) $supportModal.classList.add("open");
var title = document.getElementById("support-modal-title");
if (title) title.textContent = "Security";
var chip = document.getElementById("support-modal-chip");
if (chip) {
var use = chip.querySelector("use");
if (use) use.setAttribute("href", "#g-shield-check");
}
if ($supportBody) {
$supportBody.innerHTML =
// ── Section A: Security Reset ──────────────────────────────
'<div class="security-section">' +
'<h3 class="security-section-title">Security Reset</h3>' +
'<p class="security-section-desc">' +
'Run this if you are using this physical computer for the first time <strong>AND</strong> ' +
'it was not set up by you. This will complete the security setup by resetting all passwords ' +
'and your Bitcoin Lightning Node\u2019s private keys.' +
'</p>' +
'<p class="security-section-desc">' +
'You can also run this if you wish to reset all your passwords and your Bitcoin Lightning ' +
'Node\u2019s private keys. If you have not transferred the Bitcoin out of this node and did ' +
'not back up the private keys, <strong>you will lose your Bitcoin.</strong>' +
'</p>' +
'<button class="btn btn-primary" id="security-reset-open-btn">Proceed with Security Reset</button>' +
'<div id="security-reset-confirm" style="display:none;margin-top:16px;">' +
'<div class="security-warning-box">' +
'<p class="security-warning-text">' +
'<strong>\u26A0\uFE0F This will permanently delete:</strong>' +
'</p>' +
'<ul class="security-warning-list">' +
'<li>All generated passwords and SSH keys</li>' +
'<li>LND wallet data (seed words, channels, macaroons)</li>' +
'<li>Application databases</li>' +
'<li>Vaultwarden data</li>' +
'</ul>' +
'<p class="security-warning-text">You will go through onboarding again. <strong>This cannot be undone.</strong></p>' +
'</div>' +
'<div class="security-erase-group">' +
'<label class="security-erase-label" for="security-erase-input">Type <strong>ERASE</strong> to confirm:</label>' +
'<input class="security-erase-input" type="text" id="security-erase-input" autocomplete="off" placeholder="ERASE" />' +
'</div>' +
'<div class="security-reset-actions">' +
'<button class="btn btn-close-modal" id="security-reset-cancel-btn">Cancel</button>' +
'<button class="btn btn-danger" id="security-reset-confirm-btn" disabled>Erase &amp; Reset</button>' +
'</div>' +
'<div id="security-reset-status" class="security-status-msg"></div>' +
'</div>' +
'</div>' +
'<hr class="security-divider" />' +
// ── Section B: Verify System Integrity ────────────────────
'<div class="security-section">' +
'<h3 class="security-section-title">Verify System Integrity</h3>' +
'<p class="security-section-desc">' +
'Your Sovran_SystemsOS is built with NixOS \u2014 a system designed for complete transparency ' +
'and reproducibility. Every piece of software on this machine is built from publicly auditable ' +
'source code and verified using cryptographic hashes.' +
'</p>' +
'<p class="security-section-desc">This verification confirms three things:</p>' +
'<ol class="security-verify-list">' +
'<li>' +
'<strong>Source Code Match</strong> \u2014 The system configuration on this machine matches ' +
'the exact commit published in the public repository. No hidden changes were added.' +
'</li>' +
'<li>' +
'<strong>Binary Integrity</strong> \u2014 Every installed package in the system store is ' +
'verified against its expected cryptographic hash. If any binary, library, or config file ' +
'was tampered with, it will be detected.' +
'</li>' +
'<li>' +
'<strong>Running System Match</strong> \u2014 The currently running system matches what the ' +
'configuration says it should be. No unauthorized modifications are active.' +
'</li>' +
'</ol>' +
'<p class="security-section-desc">' +
'In short: if this verification passes, you can be confident that the software running on ' +
'your machine is exactly what is published \u2014 nothing more, nothing less.' +
'</p>' +
'<button class="btn btn-primary" id="security-verify-btn">Verify Now</button>' +
'<div id="security-verify-results" style="display:none;margin-top:16px;"></div>' +
'</div>';
// ── Wire Security Reset flow
var resetOpenBtn = document.getElementById("security-reset-open-btn");
var resetConfirmDiv = document.getElementById("security-reset-confirm");
var eraseInput = document.getElementById("security-erase-input");
var resetConfirmBtn = document.getElementById("security-reset-confirm-btn");
var resetCancelBtn = document.getElementById("security-reset-cancel-btn");
var resetStatus = document.getElementById("security-reset-status");
if (resetOpenBtn) {
resetOpenBtn.addEventListener("click", function() {
resetOpenBtn.style.display = "none";
if (resetConfirmDiv) resetConfirmDiv.style.display = "";
if (eraseInput) eraseInput.focus();
});
}
if (eraseInput && resetConfirmBtn) {
eraseInput.addEventListener("input", function() {
resetConfirmBtn.disabled = eraseInput.value.trim() !== "ERASE";
});
}
if (resetCancelBtn) {
resetCancelBtn.addEventListener("click", function() {
if (resetConfirmDiv) resetConfirmDiv.style.display = "none";
if (resetOpenBtn) resetOpenBtn.style.display = "";
if (eraseInput) eraseInput.value = "";
if (resetConfirmBtn) resetConfirmBtn.disabled = true;
if (resetStatus) { resetStatus.textContent = ""; resetStatus.className = "security-status-msg"; }
});
}
if (resetConfirmBtn) {
resetConfirmBtn.addEventListener("click", async function() {
if (!eraseInput || eraseInput.value.trim() !== "ERASE") return;
resetConfirmBtn.disabled = true;
resetConfirmBtn.textContent = "Erasing\u2026";
// Show the full-screen blocking overlay immediately so the user knows
// the wipe is in progress even while the API call runs synchronously.
var $secResetOverlay = document.getElementById("security-reset-overlay");
var $secResetStep = document.getElementById("security-reset-overlay-step");
if ($secResetOverlay) $secResetOverlay.classList.add("visible");
// Close the support modal so its content doesn't bleed through the overlay
if ($supportModal) $supportModal.classList.remove("open");
if (resetStatus) { resetStatus.textContent = "Running security reset\u2026"; resetStatus.className = "security-status-msg security-status-info"; }
try {
var data = await apiFetch("/api/security/reset", { method: "POST" });
// Switch to Phase 2: show the new password and wait for user confirmation
var phase1 = document.getElementById("security-reset-phase1");
var phase2 = document.getElementById("security-reset-phase2");
var passwordBox = document.getElementById("security-reset-new-password");
var rebootBtn = document.getElementById("security-reset-reboot-btn");
if (phase1) phase1.style.display = "none";
if (phase2) phase2.style.display = "";
if (passwordBox && data.new_password) passwordBox.textContent = data.new_password;
if (rebootBtn) {
// Keep button disabled for 5 seconds to prevent accidental clicks
var countdown = 5;
rebootBtn.textContent = "I have written down my new password \u2014 Restart Entire System (" + countdown + ")";
var timer = setInterval(function() {
countdown--;
if (countdown <= 0) {
clearInterval(timer);
rebootBtn.disabled = false;
rebootBtn.textContent = "I have written down my new password \u2014 Restart Entire System";
} else {
rebootBtn.textContent = "I have written down my new password \u2014 Restart Entire System (" + countdown + ")";
}
}, 1000);
rebootBtn.addEventListener("click", function() {
rebootBtn.disabled = true;
rebootBtn.textContent = "Restarting\u2026";
// Hide the security reset overlay so the shared reboot overlay is visible
var $secResetOverlay2 = document.getElementById("security-reset-overlay");
if ($secResetOverlay2) $secResetOverlay2.classList.remove("visible");
doReboot();
}, { once: true });
}
} catch (err) {
if ($secResetOverlay) $secResetOverlay.classList.remove("visible");
if (resetStatus) { resetStatus.textContent = "\u2717 Error: " + (err.message || "Reset failed."); resetStatus.className = "security-status-msg security-status-error"; }
resetConfirmBtn.disabled = false;
resetConfirmBtn.textContent = "Erase & Reset";
}
});
}
// ── Wire Verify System Integrity
var verifyBtn = document.getElementById("security-verify-btn");
var verifyResults = document.getElementById("security-verify-results");
if (verifyBtn && verifyResults) {
verifyBtn.addEventListener("click", async function() {
verifyBtn.disabled = true;
verifyBtn.textContent = "Verifying\u2026";
verifyResults.style.display = "";
verifyResults.innerHTML = '<p class="security-verify-loading">\u231B Running verification checks\u2026 This may take a few minutes.</p>';
try {
var data = await apiFetch("/api/security/verify-integrity", { method: "POST" });
var html = '<div class="security-verify-result-card">';
// Flake commit
html += '<div class="security-verify-row">';
html += '<span class="security-verify-label">Source Commit:</span>';
html += '<span class="security-verify-value security-verify-mono">' + escHtml(data.flake_commit || "unknown") + '</span>';
if (data.repo_url) {
html += '<a class="security-verify-link" href="' + escHtml(data.repo_url) + '" target="_blank" rel="noopener noreferrer">View on Gitea \u2197</a>';
}
html += '</div>';
// Store verification
var storeOk = data.store_verified === true;
html += '<div class="security-verify-row">';
html += '<span class="security-verify-label">Binary Integrity:</span>';
html += '<span class="security-verify-badge ' + (storeOk ? "security-verify-pass" : "security-verify-fail") + '">';
html += storeOk ? "\u2705 PASS" : "\u274C FAIL";
html += '</span>';
html += '</div>';
if (!storeOk && data.store_errors && data.store_errors.length > 0) {
html += '<details class="security-verify-errors"><summary>Show errors (' + data.store_errors.length + ')</summary>';
html += '<pre class="security-verify-pre">' + escHtml(data.store_errors.join("\n")) + '</pre>';
html += '</details>';
}
// System match
var sysOk = data.system_matches === true;
html += '<div class="security-verify-row">';
html += '<span class="security-verify-label">Running System Match:</span>';
html += '<span class="security-verify-badge ' + (sysOk ? "security-verify-pass" : "security-verify-fail") + '">';
html += sysOk ? "\u2705 PASS" : "\u274C FAIL";
html += '</span>';
html += '</div>';
if (!sysOk) {
html += '<div class="security-verify-path-row">';
html += '<span class="security-verify-path-label">Current:</span><code class="security-verify-mono">' + escHtml(data.current_system_path || "") + '</code>';
html += '</div>';
html += '<div class="security-verify-path-row">';
html += '<span class="security-verify-path-label">Expected:</span><code class="security-verify-mono">' + escHtml(data.expected_system_path || "") + '</code>';
html += '</div>';
}
html += '</div>';
verifyResults.innerHTML = html;
} catch (err) {
verifyResults.innerHTML = '<p class="security-status-msg security-status-error">\u274C Verification failed: ' + escHtml(err.message || "Unknown error") + '</p>';
}
verifyBtn.disabled = false;
verifyBtn.textContent = "Verify Now";
});
}
}
}
File diff suppressed because it is too large. Load diff
+131
View File
@@ -0,0 +1,131 @@
"use strict";
// ── State ─────────────────────────────────────────────────────────
let _servicesCache = [];
let _categoryLabels = {};
let _updateLog = "";
let _updatePollTimer = null;
let _updatePollInFlight = false;
let _updateLogOffset = 0;
let _updateVisibleLogChars = 0;
let _serverWasDown = false;
let _updateFinished = false;
let _updateStatusUnavailable = false;
let _updatePollFailures = 0; // consecutive failed update-status polls
let _supportTimerInt = null;
let _supportEnabledAt = null;
let _supportStatus = null; // last fetched /api/support/status payload
let _walletUnlockTimerInt = null;
let _cachedExternalIp = null;
// Current role (set during init from /api/config)
let _currentRole = "server_plus_desktop";
// Feature Manager state
let _featuresData = null;
let _rebuildLog = "";
let _rebuildLogOffset = 0;
let _rebuildPollTimer = null;
let _rebuildPollInFlight = false;
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;
// ── DOM refs ──────────────────────────────────────────────────────
const $tilesArea = document.getElementById("tiles-area");
const $sidebarSupport = document.getElementById("sidebar-support");
const $sidebarFeatures = document.getElementById("sidebar-features");
// No longer needed — Update System moved to sidebar
// const $updateBtn = document.getElementById("btn-update");
// const $updateBadge = document.getElementById("update-badge");
const $internalIp = document.getElementById("ip-internal");
const $externalIp = document.getElementById("ip-external");
const $modal = document.getElementById("update-modal");
const $modalSpinner = document.getElementById("modal-spinner");
const $modalStatus = document.getElementById("modal-status");
const $modalLog = document.getElementById("modal-log");
const $btnReboot = document.getElementById("btn-reboot");
const $btnSave = document.getElementById("btn-save-report");
const $btnRetryUpdate = document.getElementById("btn-retry-update-status");
const $btnRetryRun = document.getElementById("btn-retry-update");
const $btnCloseModal = document.getElementById("btn-close-modal");
const $btnCheckAgain = document.getElementById("btn-check-again");
const $updateCloseBtn = document.getElementById("update-close-btn");
const $updPill = document.getElementById("upd-pill");
const $updLastChecked = document.getElementById("upd-last-checked");
const $rebootOverlay = document.getElementById("reboot-overlay");
const $rebootMainCard = document.getElementById("reboot-main-card");
const $rebootErrorCard = document.getElementById("reboot-error-card");
const $rebootSubmessage = document.getElementById("reboot-submessage");
const $credsModal = document.getElementById("creds-modal");
const $credsTitle = document.getElementById("creds-modal-title");
const $credsBody = document.getElementById("creds-body");
const $credsCloseBtn = document.getElementById("creds-close-btn");
const $supportModal = document.getElementById("support-modal");
const $supportBody = document.getElementById("support-body");
const $supportCloseBtn = document.getElementById("support-close-btn");
const $logoutBtn = document.getElementById("btn-logout");
// Feature Manager — rebuild modal
const $rebuildModal = document.getElementById("rebuild-modal");
const $rebuildSpinner = document.getElementById("rebuild-spinner");
const $rebuildStatus = document.getElementById("rebuild-status");
const $rebuildLog = document.getElementById("rebuild-log");
const $rebuildReboot = document.getElementById("rebuild-reboot-btn");
const $rebuildSave = document.getElementById("rebuild-save-report");
const $rebuildClose = document.getElementById("rebuild-close-btn");
const $rebuildCloseHdr = document.getElementById("rebuild-close-hdr");
const $rebuildPill = document.getElementById("rebuild-pill");
// Feature Manager — domain setup modal
const $domainSetupModal = document.getElementById("domain-setup-modal");
const $domainSetupTitle = document.getElementById("domain-setup-title");
const $domainSetupBody = document.getElementById("domain-setup-body");
const $domainSetupClose = document.getElementById("domain-setup-close-btn");
// Feature Manager — SSL email modal
const $sslEmailModal = document.getElementById("ssl-email-modal");
const $sslEmailInput = document.getElementById("ssl-email-input");
const $sslEmailSave = document.getElementById("ssl-email-save-btn");
const $sslEmailCancel = document.getElementById("ssl-email-cancel-btn");
const $sslEmailClose = document.getElementById("ssl-email-close-btn");
// Feature Manager — confirm modal
const $featureConfirmModal = document.getElementById("feature-confirm-modal");
const $featureConfirmMsg = document.getElementById("feature-confirm-message");
const $featureConfirmOk = document.getElementById("feature-confirm-ok-btn");
const $featureConfirmCancel = document.getElementById("feature-confirm-cancel-btn");
const $featureConfirmClose = document.getElementById("feature-confirm-close-btn");
// Port Requirements modal
const $portReqModal = document.getElementById("port-requirements-modal");
const $portReqBody = document.getElementById("port-req-body");
const $portReqClose = document.getElementById("port-req-close-btn");
// Upgrade modal (Node → Server+Desktop)
const $upgradeModal = document.getElementById("upgrade-modal");
const $upgradeConfirmBtn = document.getElementById("upgrade-confirm-btn");
const $upgradeCancelBtn = document.getElementById("upgrade-cancel-btn");
const $upgradeCloseBtn = document.getElementById("upgrade-close-btn");
// Restart confirm dialog
const $restartConfirmModal = document.getElementById("restart-confirm-modal");
const $restartConfirmOk = document.getElementById("restart-confirm-ok-btn");
const $restartConfirmCancel = document.getElementById("restart-confirm-cancel-btn");
const $restartConflictBox = document.getElementById("restart-conflict-box");
// Header reboot button
const $headerRebootBtn = document.getElementById("btn-header-reboot");
// System status banner
// (removed — health is now shown per-tile via the composite health field)
@@ -0,0 +1,734 @@
"use strict";
// ── Tech Support modal ────────────────────────────────────────────
async function openSupportModal() {
if (!$supportModal) return;
$supportModal.classList.add("open");
// The dialog is shared with Security — always restore its identity
var title = document.getElementById("support-modal-title");
if (title) title.textContent = "Tech Support";
var chip = document.getElementById("support-modal-chip");
if (chip) {
var use = chip.querySelector("use");
if (use) use.setAttribute("href", "#g-lifebuoy");
}
$supportBody.innerHTML = '<p class="creds-loading">Checking support status…</p>';
try {
var status = await apiFetch("/api/support/status");
_supportStatus = status;
if (status.active) { _supportEnabledAt = status.enabled_at; renderSupportActive(status); }
else if (!status.sshd_enabled) { renderSupportSshdOff(); }
else { renderSupportInactive(); }
} catch (err) {
$supportBody.innerHTML = '<p class="creds-empty">Could not check support status.</p>';
}
}
function renderSupportSshdOff() {
stopSupportTimer();
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">🛟</div>',
'<h3 class="support-heading">Need help from Sovran Systems?</h3>',
'<p class="support-desc">To get Tech Support, SSH must be enabled first. SSH is <strong>off by default</strong> for maximum security — it only needs to be on during a support session.</p>',
'<div class="support-wallet-box support-wallet-protected">',
'<div class="support-wallet-header"><span class="support-wallet-icon">🔐</span><span class="support-wallet-title">SSH is Off</span></div>',
'<p class="support-wallet-desc">SSH (remote login) is <strong>disabled by default</strong> on your Sovran Pro. Clicking the button below will enable SSH and trigger a system rebuild. Once complete, you can then grant support access.</p>',
'<p class="support-wallet-desc">When you end the support session, you\'ll be able to disable SSH to return to the default secure state.</p>',
'</div>',
'<div class="support-steps"><div class="support-steps-title">Steps:</div><ol>',
'<li>Enable SSH (triggers a system rebuild — takes a few minutes)</li>',
'<li>Grant Sovran Systems temporary support access</li>',
'<li>End the session when done — you\'ll be prompted to disable SSH</li>',
'</ol></div>',
'<button class="btn support-btn-enable" id="btn-sshd-enable">Enable SSH</button>',
'<p class="support-fine-print">This will trigger a NixOS rebuild. Your machine will remain operational during the rebuild.</p>',
'</div>',
].join("");
document.getElementById("btn-sshd-enable").addEventListener("click", enableSshd);
}
async function enableSshd() {
var btn = document.getElementById("btn-sshd-enable");
if (btn) { btn.disabled = true; btn.textContent = "Enabling SSH…"; }
try {
await apiFetch("/api/features/toggle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ feature: "sshd", enabled: true }),
});
// Poll until rebuild completes and sshd_enabled is true
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">⚙️</div>',
'<h3 class="support-heading">Enabling SSH…</h3>',
'<p class="support-desc">A system rebuild is in progress. This may take a few minutes. The page will update automatically when SSH is ready.</p>',
'<p class="creds-loading" id="sshd-rebuild-status">Rebuilding system…</p>',
'</div>',
].join("");
pollForSshdReady();
} catch (err) {
if (btn) { btn.disabled = false; btn.textContent = "Enable SSH"; }
alert("Failed to enable SSH. Please try again.");
}
}
function pollForSshdReady() {
var attempts = 0;
var maxAttempts = 60; // 5 minutes (5s interval)
var interval = setInterval(async function() {
attempts++;
try {
var status = await apiFetch("/api/support/status");
var el = document.getElementById("sshd-rebuild-status");
if (status.sshd_enabled) {
clearInterval(interval);
_supportStatus = status;
renderSupportInactive();
} else if (attempts >= maxAttempts) {
clearInterval(interval);
if (el) el.textContent = "Rebuild is taking longer than expected. Please close this dialog and try again.";
} else {
if (el) el.textContent = "Rebuilding system… (" + attempts * 5 + "s)";
}
} catch (_) {}
}, 5000);
}
function renderSupportInactive() {
stopSupportTimer();
var ip = _cachedExternalIp || "loading…";
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">🛟</div>',
'<h3 class="support-heading">Need help from Sovran Systems?</h3>',
'<p class="support-desc">This will temporarily grant our support team SSH access to your machine so we can help diagnose and fix issues.</p>',
'<div class="support-wallet-box support-wallet-protected">',
'<div class="support-wallet-header"><span class="support-wallet-icon">✅</span><span class="support-wallet-title">SSH is Active</span></div>',
'<p class="support-wallet-desc">SSH is enabled on your machine. You can now grant Sovran Systems temporary access below.</p>',
'</div>',
'<div class="support-info-box">',
'<div class="support-info-row"><span class="support-info-label">Your IP</span><span class="support-info-value">' + escHtml(ip) + '</span></div>',
'<div class="support-info-hint">This IP will be shared with Sovran Systems support</div>',
'</div>',
'<div class="support-wallet-box support-wallet-protected">',
'<div class="support-wallet-header"><span class="support-wallet-icon">🔒</span><span class="support-wallet-title">Wallet Protection</span></div>',
'<p class="support-wallet-desc">Wallet files (LND, Sparrow, Bisq) are <strong>protected by default</strong>. Support staff cannot access your private keys unless you explicitly grant access.</p>',
'</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>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("");
document.getElementById("btn-support-enable").addEventListener("click", enableSupport);
}
function renderSupportActive(status) {
var ip = _cachedExternalIp || "loading…";
var walletProtected = status && status.wallet_protected;
var walletUnlocked = status && status.wallet_unlocked;
var unlockUntil = status && status.wallet_unlocked_until_human ? status.wallet_unlocked_until_human : "";
var protectedPaths = (status && status.protected_paths && status.protected_paths.length)
? status.protected_paths : [];
var walletSection;
if (walletProtected) {
if (walletUnlocked) {
walletSection = [
'<div class="support-wallet-box support-wallet-unlocked">',
'<div class="support-wallet-header"><span class="support-wallet-icon">🔓</span><span class="support-wallet-title">Wallet Access: UNLOCKED</span></div>',
'<p class="support-wallet-desc">You have granted support temporary access to wallet files' + (unlockUntil ? ' until <strong>' + escHtml(unlockUntil) + '</strong>' : '') + '.</p>',
'<button class="btn support-btn-wallet-lock" id="btn-wallet-lock">Re-lock Wallet Now</button>',
'</div>',
].join("");
} else {
var pathList = protectedPaths.length
? '<ul class="support-wallet-paths">' + protectedPaths.map(function(p){ return '<li>' + escHtml(p) + '</li>'; }).join("") + '</ul>'
: '';
walletSection = [
'<div class="support-wallet-box support-wallet-protected">',
'<div class="support-wallet-header"><span class="support-wallet-icon">🔒</span><span class="support-wallet-title">Wallet Files: Protected</span></div>',
'<p class="support-wallet-desc">Support cannot access your wallet files. Grant temporary access only if needed for wallet troubleshooting.</p>',
pathList,
'<div class="support-wallet-unlock-row">',
'<select id="wallet-unlock-duration" class="support-unlock-select">',
'<option value="3600">1 hour</option>',
'<option value="1800">30 minutes</option>',
'<option value="7200">2 hours</option>',
'</select>',
'<button class="btn support-btn-wallet-unlock" id="btn-wallet-unlock">Grant Wallet Access</button>',
'</div>',
'</div>',
].join("");
}
} else {
walletSection = [
'<div class="support-wallet-box support-wallet-warning">',
'<div class="support-wallet-header"><span class="support-wallet-icon">⚠️</span><span class="support-wallet-title">Wallet Protection Unavailable</span></div>',
'<p class="support-wallet-desc">The restricted support user could not be created. Support is running with root access — wallet files may be accessible. End the session if you are concerned.</p>',
'</div>',
].join("");
}
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big support-active-icon">🔓</div>',
'<h3 class="support-heading support-active-heading">Support Access is Active</h3>',
'<p class="support-active-note">Sovran Systems can currently connect to your machine via SSH.</p>',
'<div class="support-info-box support-active-box">',
'<div class="support-info-row"><span class="support-info-label">Your IP</span><span class="support-info-value">' + escHtml(ip) + '</span></div>',
'<div class="support-info-row"><span class="support-info-label">Duration</span><span class="support-info-value" id="support-timer">…</span></div>',
'</div>',
walletSection,
'<button class="btn support-btn-disable" id="btn-support-disable">End Support Session</button>',
'<p class="support-fine-print">This will remove the SSH key and revoke all wallet access immediately.</p>',
'<button class="btn support-btn-auditlog" id="btn-support-audit">View Audit Log</button>',
'</div>',
'<div id="support-audit-container" class="support-audit-container" style="display:none;"></div>',
].join("");
document.getElementById("btn-support-disable").addEventListener("click", disableSupport);
document.getElementById("btn-support-audit").addEventListener("click", toggleAuditLog);
if (walletProtected && !walletUnlocked) {
document.getElementById("btn-wallet-unlock").addEventListener("click", walletUnlock);
}
if (walletProtected && walletUnlocked) {
document.getElementById("btn-wallet-lock").addEventListener("click", walletLock);
}
startSupportTimer();
if (walletUnlocked && status.wallet_unlocked_until) {
startWalletUnlockTimer(status.wallet_unlocked_until);
}
}
function renderSupportRemoved(verified) {
stopSupportTimer();
stopWalletUnlockTimer();
var icon = verified ? "✅" : "⚠️";
var msg = verified ? "The Sovran Systems SSH key has been completely removed from your machine. We no longer have any access." : "The key removal was requested but could not be fully verified. Please reboot to ensure it is gone.";
var vclass = verified ? "verified-gone" : "verify-warning";
var vlabel = verified ? "✓ Removed — No access" : "⚠ Verify by rebooting";
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">' + icon + '</div>',
'<h3 class="support-heading">Support Session Ended</h3>',
'<p class="support-desc">' + escHtml(msg) + '</p>',
'<div class="support-verify-box"><span class="support-verify-label">SSH Key Status:</span><span class="support-verify-value ' + vclass + '">' + vlabel + '</span></div>',
'<div class="support-wallet-box support-wallet-protected" style="margin-top:12px;">',
'<div class="support-wallet-header"><span class="support-wallet-icon">🔐</span><span class="support-wallet-title">Disable SSH When Done</span></div>',
'<p class="support-wallet-desc">SSH is still enabled on your machine. Click below to turn it off and return to the default secure state.</p>',
'<button class="btn support-btn-enable" id="btn-sshd-disable">Disable SSH</button>',
'</div>',
'<button class="btn support-btn-done" id="btn-support-done">Done</button>',
'</div>',
].join("");
document.getElementById("btn-support-done").addEventListener("click", closeSupportModal);
document.getElementById("btn-sshd-disable").addEventListener("click", disableSshd);
}
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 }),
});
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); }
}
}
async function disableSupport() {
var btn = document.getElementById("btn-support-disable");
if (btn) { btn.disabled = true; btn.textContent = "Removing key…"; }
try {
var result = await apiFetch("/api/support/disable", { method: "POST" });
renderSupportRemoved(result.verified);
} catch (err) {
if (btn) { btn.disabled = false; btn.textContent = "End Support Session"; }
alert("Failed to disable support access. Please try again.");
}
}
async function disableSshd() {
var btn = document.getElementById("btn-sshd-disable");
if (btn) { btn.disabled = true; btn.textContent = "Disabling SSH…"; }
try {
await apiFetch("/api/features/toggle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ feature: "sshd", enabled: false }),
});
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">⚙️</div>',
'<h3 class="support-heading">Disabling SSH…</h3>',
'<p class="support-desc">A system rebuild is in progress to turn off SSH. This may take a few minutes.</p>',
'<p class="creds-loading" id="sshd-disable-status">Rebuilding system…</p>',
'</div>',
].join("");
pollForSshdDisabled();
} catch (err) {
if (btn) { btn.disabled = false; btn.textContent = "Disable SSH"; }
alert("Failed to disable SSH. Please try again.");
}
}
function pollForSshdDisabled() {
var attempts = 0;
var maxAttempts = 60; // 5 minutes (5s interval)
var interval = setInterval(async function() {
attempts++;
try {
var status = await apiFetch("/api/support/status");
var el = document.getElementById("sshd-disable-status");
if (!status.sshd_enabled) {
clearInterval(interval);
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">🔐</div>',
'<h3 class="support-heading">SSH is Off</h3>',
'<p class="support-desc">SSH has been disabled. Your machine is back to its default secure state.</p>',
'<button class="btn support-btn-done" id="btn-support-done">Done</button>',
'</div>',
].join("");
document.getElementById("btn-support-done").addEventListener("click", closeSupportModal);
} else if (attempts >= maxAttempts) {
clearInterval(interval);
if (el) el.textContent = "Rebuild is taking longer than expected. Please close this dialog and try again.";
} else {
if (el) el.textContent = "Rebuilding system… (" + attempts * 5 + "s)";
}
} catch (_) {}
}, 5000);
}
async function walletUnlock() {
var btn = document.getElementById("btn-wallet-unlock");
var sel = document.getElementById("wallet-unlock-duration");
var duration = sel ? parseInt(sel.value, 10) : 3600;
if (btn) { btn.disabled = true; btn.textContent = "Unlocking…"; }
try {
var result = await apiFetch("/api/support/wallet-unlock", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ duration: duration }),
});
var status = await apiFetch("/api/support/status");
_supportStatus = status;
renderSupportActive(status);
} catch (err) {
if (btn) { btn.disabled = false; btn.textContent = "Grant Wallet Access"; }
alert("Failed to unlock wallet access: " + (err.message || "Unknown error"));
}
}
async function walletLock() {
var btn = document.getElementById("btn-wallet-lock");
if (btn) { btn.disabled = true; btn.textContent = "Locking…"; }
try {
await apiFetch("/api/support/wallet-lock", { method: "POST" });
var status = await apiFetch("/api/support/status");
_supportStatus = status;
renderSupportActive(status);
} catch (err) {
if (btn) { btn.disabled = false; btn.textContent = "Re-lock Wallet Now"; }
alert("Failed to re-lock wallet: " + (err.message || "Unknown error"));
}
}
async function toggleAuditLog() {
var container = document.getElementById("support-audit-container");
if (!container) return;
if (container.style.display !== "none") {
container.style.display = "none";
return;
}
container.style.display = "block";
container.innerHTML = '<p class="creds-loading">Loading audit log…</p>';
try {
var data = await apiFetch("/api/support/audit-log");
if (!data.entries || data.entries.length === 0) {
container.innerHTML = '<p class="support-audit-empty">No audit events recorded yet.</p>';
} else {
container.innerHTML = '<div class="support-audit-log">' +
data.entries.map(function(e) { return '<div class="support-audit-entry">' + escHtml(e) + '</div>'; }).join("") +
'</div>';
}
} catch (err) {
container.innerHTML = '<p class="creds-empty">Could not load audit log.</p>';
}
}
function startSupportTimer() {
stopSupportTimer();
updateSupportTimer();
_supportTimerInt = setInterval(updateSupportTimer, SUPPORT_TIMER_INTERVAL);
}
function stopSupportTimer() {
if (_supportTimerInt) { clearInterval(_supportTimerInt); _supportTimerInt = null; }
}
function updateSupportTimer() {
var el = document.getElementById("support-timer");
if (!el || !_supportEnabledAt) return;
var elapsed = (Date.now() / 1000) - _supportEnabledAt;
el.textContent = formatDuration(Math.max(0, elapsed));
}
function startWalletUnlockTimer(expiresAt) {
stopWalletUnlockTimer();
_walletUnlockTimerInt = setInterval(function() {
if (Date.now() / 1000 >= expiresAt) {
stopWalletUnlockTimer();
// Refresh the support modal to show re-locked state
apiFetch("/api/support/status").then(function(status) {
_supportStatus = status;
renderSupportActive(status);
}).catch(function() {});
}
}, 10000);
}
function stopWalletUnlockTimer() {
if (_walletUnlockTimerInt) { clearInterval(_walletUnlockTimerInt); _walletUnlockTimerInt = null; }
}
function closeSupportModal() {
if ($supportModal) $supportModal.classList.remove("open");
stopSupportTimer();
stopWalletUnlockTimer();
}
// ── Manual Backup modal ───────────────────────────────────────────
var _backupPollTimer = null;
var _backupLogOffset = 0;
function openBackupModal() {
if (!$supportModal) return;
$supportModal.classList.add("open");
$supportBody.innerHTML = '<p class="creds-loading">Detecting external drives\u2026</p>';
detectDrivesAndRender();
}
async function detectDrivesAndRender() {
try {
// Check whether a backup is already in progress
var status = await apiFetch("/api/backup/status?offset=0");
if (status.running) {
renderBackupRunning();
_backupLogOffset = status.offset || 0;
if (status.log) {
var logDiv = document.getElementById("backup-log");
if (logDiv) { logDiv.insertAdjacentText("beforeend", status.log); logDiv.scrollTop = logDiv.scrollHeight; }
}
startBackupPoll();
return;
}
} catch (_) {}
try {
var data = await apiFetch("/api/backup/drives");
renderBackupReady(data.drives || []);
} catch (err) {
$supportBody.innerHTML = '<p class="creds-empty">Could not detect drives. Please try again.</p>';
}
}
function renderBackupReady(drives) {
var driveSelector = "";
if (drives.length > 0) {
driveSelector = [
'<label class="support-info-label" style="display:block;margin-bottom:6px;">Select drive:</label>',
'<div style="display:flex;gap:8px;align-items:center;margin-bottom:14px;">',
'<select id="backup-drive-select" class="support-unlock-select" style="flex:1;">',
].join("");
for (var i = 0; i < drives.length; i++) {
var d = drives[i];
driveSelector += '<option value="' + escHtml(d.path) + '">' +
escHtml(d.name) + ' \u2014 ' + d.free_gb + ' GB free / ' + d.total_gb + ' GB total' +
'</option>';
}
driveSelector += '</select>';
driveSelector += '<button class="btn support-btn-auditlog" id="btn-backup-refresh" style="white-space:nowrap;">&#x21bb; Refresh</button>';
driveSelector += '</div>';
driveSelector += '<button class="btn support-btn-enable" id="btn-start-backup">Start Backup</button>';
} else {
driveSelector = [
'<div class="support-wallet-box support-wallet-warning">',
'<div class="support-wallet-header">',
'<span class="support-wallet-icon">\u26a0\ufe0f</span>',
'<span class="support-wallet-title">No External Drive Detected</span>',
'</div>',
'<p class="support-wallet-desc">',
'No USB drive was found under /run/media/. ',
'Make sure the drive is plugged in and mounted, then click Refresh.',
'</p>',
'</div>',
'<button class="btn support-btn-auditlog" id="btn-backup-refresh">&#x21bb; Refresh</button>',
].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-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>',
'</ol>',
'</div>',
'<div class="support-steps">',
'<div class="support-steps-title">What gets backed up</div>',
'<ol class="support-backup-steps">',
backupItemsHtml,
'</ol>',
'</div>',
dbNoteHtml,
'<div class="support-wallet-box support-wallet-protected">',
'<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>',
'</div>',
driveSelector,
'</div>',
].join("");
if (drives.length > 0) {
document.getElementById("btn-start-backup").addEventListener("click", startBackup);
document.getElementById("btn-backup-refresh").addEventListener("click", function() {
$supportBody.innerHTML = '<p class="creds-loading">Scanning for external drives\u2026</p>';
detectDrivesAndRender();
});
} else {
document.getElementById("btn-backup-refresh").addEventListener("click", function() {
$supportBody.innerHTML = '<p class="creds-loading">Scanning for external drives\u2026</p>';
detectDrivesAndRender();
});
}
}
async function startBackup() {
var btn = document.getElementById("btn-start-backup");
if (btn) { btn.disabled = true; btn.textContent = "Starting\u2026"; }
var sel = document.getElementById("backup-drive-select");
var target = sel ? sel.value : "";
try {
_backupLogOffset = 0;
await apiFetch("/api/backup/run" + (target ? "?target=" + encodeURIComponent(target) : ""), { method: "POST" });
renderBackupRunning();
startBackupPoll();
} catch (err) {
if (btn) { btn.disabled = false; btn.textContent = "Start Backup"; }
alert("Failed to start backup: " + (err.message || "Unknown error"));
}
}
function renderBackupRunning() {
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big support-active-icon">\ud83d\udcbe</div>',
'<h3 class="support-heading support-active-heading">Backup In Progress</h3>',
'<div class="support-wallet-box support-wallet-warning">',
'<div class="support-wallet-header">',
'<span class="support-wallet-icon">\u26a0\ufe0f</span>',
'<span class="support-wallet-title">Do Not Unplug</span>',
'</div>',
'<p class="support-wallet-desc">Do not remove the USB drive while the backup is running. This could corrupt the backup and your drive.</p>',
'</div>',
'<div class="modal-log" id="backup-log" style="text-align:left;"></div>',
'</div>',
].join("");
}
function startBackupPoll() {
stopBackupPoll();
_backupPollTimer = setInterval(pollBackupStatus, 2000);
pollBackupStatus();
}
function stopBackupPoll() {
if (_backupPollTimer) { clearInterval(_backupPollTimer); _backupPollTimer = null; }
}
async function pollBackupStatus() {
try {
var data = await apiFetch("/api/backup/status?offset=" + _backupLogOffset);
var logDiv = document.getElementById("backup-log");
if (logDiv && data.log) {
logDiv.insertAdjacentText("beforeend", data.log);
logDiv.scrollTop = logDiv.scrollHeight;
}
_backupLogOffset = data.offset;
const result = (data.result || "").toLowerCase();
if (result === "success" || result === "failed") {
stopBackupPoll();
renderBackupDone(result === "success");
}
} catch (_) {}
}
function renderBackupDone(success) {
var logDiv = document.getElementById("backup-log");
var logContent = logDiv ? logDiv.textContent : "";
if (success) {
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">\u2705</div>',
'<h3 class="support-heading">All Finished!</h3>',
'<div class="support-wallet-box support-wallet-protected">',
'<div class="support-wallet-header">',
'<span class="support-wallet-icon">\u23cf\ufe0f</span>',
'<span class="support-wallet-title">Eject Your Drive</span>',
'</div>',
'<p class="support-wallet-desc">Please eject the drive before removing it from your Sovran Pro.</p>',
'</div>',
'<div class="modal-log" id="backup-log-done" style="text-align:left;"></div>',
'<button class="btn support-btn-done" id="btn-backup-close">Close</button>',
'</div>',
].join("");
var doneLog = document.getElementById("backup-log-done");
if (doneLog) { doneLog.textContent = logContent; doneLog.scrollTop = doneLog.scrollHeight; }
} else {
$supportBody.innerHTML = [
'<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>',
'<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>',
].join("");
var failLog = document.getElementById("backup-log-fail");
if (failLog) { failLog.textContent = logContent; failLog.scrollTop = failLog.scrollHeight; }
}
document.getElementById("btn-backup-close").addEventListener("click", closeSupportModal);
}
+323
View File
@@ -0,0 +1,323 @@
"use strict";
// ── Bitcoin IBD sync state (for ETA calculation) ──────────────────
// Keyed by tileId: { progress: float, timestamp: ms }
var _btcSyncPrev = {};
function _firstElementFromHtml(html) {
var tmp = document.createElement("div");
tmp.innerHTML = html;
return tmp.firstElementChild || null;
}
// ── Render: initial build ─────────────────────────────────────────
function buildTiles(services, categoryLabels) {
_servicesCache = services;
var grouped = {};
var supportServices = [];
for (var i = 0; i < services.length; i++) {
var svc = services[i];
// Support tiles go to the sidebar, not the main grid
if (svc.category === "support" || svc.type === "support") {
supportServices.push(svc);
continue;
}
var cat = svc.category || "other";
if (CATEGORY_ALIASES[cat]) cat = CATEGORY_ALIASES[cat];
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push(svc);
}
renderSidebarSupport(supportServices);
$tilesArea.innerHTML = "";
var orderedKeys = CATEGORY_ORDER.filter(function(k) { return grouped[k]; });
Object.keys(grouped).forEach(function(k) {
if (orderedKeys.indexOf(k) === -1) orderedKeys.push(k);
});
for (var j = 0; j < orderedKeys.length; j++) {
var catKey = orderedKeys[j];
var entries = grouped[catKey];
if (!entries || entries.length === 0) continue;
var label = categoryLabels[catKey] || catKey;
var section = document.createElement("div");
section.className = "category-section";
section.dataset.category = catKey;
section.innerHTML = '<div class="section-header">' + escHtml(label) + '</div><hr class="section-divider" /><div class="tiles-grid" data-cat="' + escHtml(catKey) + '"></div>';
var grid = section.querySelector(".tiles-grid");
for (var k = 0; k < entries.length; k++) {
grid.appendChild(buildTile(entries[k]));
}
$tilesArea.appendChild(section);
}
if ($tilesArea.children.length === 0) {
$tilesArea.innerHTML = '<div class="empty-state"><p>No services configured.</p></div>';
}
if (typeof window.dashboardServicesUpdated === "function") window.dashboardServicesUpdated();
}
function renderSidebarSupport(supportServices) {
$sidebarSupport.innerHTML = "";
// ── Update System button (above Tech Help)
var sidebarUpdateBtn = document.createElement("button");
sidebarUpdateBtn.className = "sidebar-support-btn";
sidebarUpdateBtn.id = "sidebar-btn-update";
sidebarUpdateBtn.innerHTML =
'<span class="sidebar-support-icon"><svg><use href="#g-update"/></svg></span>' +
'<span class="sidebar-support-text">' +
'<span class="sidebar-support-title">Update System</span>' +
'<span class="sidebar-support-hint" id="sidebar-update-hint">Check for updates</span>' +
'</span>';
sidebarUpdateBtn.addEventListener("click", function() { openUpdateModal(); });
$sidebarSupport.appendChild(sidebarUpdateBtn);
// checkUpdates may already have run before the sidebar was built
applyUpdateSidebarState(window._lastUpdateCheck);
for (var i = 0; i < supportServices.length; i++) {
var svc = supportServices[i];
var btn = document.createElement("button");
btn.className = "sidebar-support-btn";
btn.innerHTML =
'<span class="sidebar-support-icon"><svg><use href="#g-lifebuoy"/></svg></span>' +
'<span class="sidebar-support-text">' +
'<span class="sidebar-support-title">' + escHtml(svc.name || "Tech Support") + '</span>' +
'<span class="sidebar-support-hint">Click for help</span>' +
'</span>';
btn.addEventListener("click", function() { openSupportModal(); });
$sidebarSupport.appendChild(btn);
}
// ── Manual Backup button
var backupBtn = document.createElement("button");
backupBtn.className = "sidebar-support-btn";
backupBtn.innerHTML =
'<span class="sidebar-support-icon"><svg><use href="#g-box"/></svg></span>' +
'<span class="sidebar-support-text">' +
'<span class="sidebar-support-title">Manual Backup</span>' +
'<span class="sidebar-support-hint">Back up to external drive</span>' +
'</span>';
backupBtn.addEventListener("click", function() { openBackupModal(); });
$sidebarSupport.appendChild(backupBtn);
// ── Security button
var securityBtn = document.createElement("button");
securityBtn.className = "sidebar-support-btn";
securityBtn.innerHTML =
'<span class="sidebar-support-icon"><svg><use href="#g-shield-check"/></svg></span>' +
'<span class="sidebar-support-text">' +
'<span class="sidebar-support-title">Security</span>' +
'<span class="sidebar-support-hint">Reset &amp; verify system</span>' +
'</span>';
securityBtn.addEventListener("click", function() { openSecurityModal(); });
$sidebarSupport.appendChild(securityBtn);
// ── Upgrade button (Node role only)
if (_currentRole === "node") {
var upgradeBtn = document.createElement("button");
upgradeBtn.className = "sidebar-support-btn";
upgradeBtn.innerHTML =
'<span class="sidebar-support-icon"><svg><use href="#g-antenna"/></svg></span>' +
'<span class="sidebar-support-text">' +
'<span class="sidebar-support-title">Upgrade to Full Server</span>' +
'<span class="sidebar-support-hint">Unlock all services</span>' +
'</span>';
upgradeBtn.addEventListener("click", function() { openUpgradeModal(); });
$sidebarSupport.appendChild(upgradeBtn);
}
var hr = document.createElement("hr");
hr.className = "sidebar-divider";
$sidebarSupport.appendChild(hr);
}
function buildTile(svc) {
var isSupport = svc.type === "support";
var sc = statusClass(svc.health || svc.status);
var st = statusText(svc.health || svc.status, svc.enabled);
var dis = !svc.enabled;
var tile = document.createElement("div");
tile.className = "service-tile" + (dis ? " disabled" : "") + (isSupport ? " support-tile" : "");
tile.dataset.unit = svc.unit;
tile.dataset.tileId = tileId(svc);
if (isSupport) {
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><div class="tile-status"><span class="support-status-label">Click for help</span></div>';
tile.style.cursor = "pointer";
tile.addEventListener("click", function() { openSupportModal(); });
return tile;
}
if (svc.sync_ibd && svc.enabled) {
var pct = Math.round((svc.sync_progress || 0) * 100);
var id = tileId(svc);
var eta = _calcBtcEta(id, svc.sync_progress || 0);
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>' +
'<div class="tile-sync-container">' +
'<div class="tile-sync-label">\u23F3 Syncing Timechain</div>' +
'<div class="tile-sync-bar-row">' +
'<div class="tile-sync-bar-track"><div class="tile-sync-bar-fill" style="width:' + pct + '%"></div></div>' +
'<span class="tile-sync-percent">' + pct + '%</span>' +
'</div>' +
'<div class="tile-sync-eta">' + escHtml(eta) + '</div>' +
'</div>';
tile.style.cursor = "pointer";
tile.addEventListener("click", function() {
openServiceDetailModal(svc.unit, svc.name, svc.icon);
});
return tile;
}
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><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() {
openServiceDetailModal(svc.unit, svc.name, svc.icon);
});
return tile;
}
// ── Render: live update ───────────────────────────────────────────
// Calculate ETA text for Bitcoin IBD and track progress history.
function _calcBtcEta(id, progress) {
var now = Date.now();
var prev = _btcSyncPrev[id];
// Only update the cache when progress has actually advanced
if (!prev || prev.progress < progress) {
_btcSyncPrev[id] = { progress: progress, timestamp: now };
}
if (!prev || prev.progress >= progress) return "Estimating\u2026";
var elapsed = (now - prev.timestamp) / 1000; // seconds
if (elapsed <= 0) return "Estimating\u2026";
var rate = (progress - prev.progress) / elapsed; // progress per second
if (rate <= 0) return "Estimating\u2026";
var remaining = (1.0 - progress) / rate;
return "\u007E" + formatDuration(remaining) + " remaining";
}
function updateTiles(services) {
_servicesCache = services;
for (var i = 0; i < services.length; i++) {
var svc = services[i];
if (svc.type === "support") continue;
var id = CSS.escape(tileId(svc));
var tile = $tilesArea.querySelector('.service-tile[data-tile-id="' + id + '"]');
if (!tile) continue;
if (svc.sync_ibd && svc.enabled) {
// If tile was previously normal, rebuild it with the sync layout
if (!tile.querySelector(".tile-sync-container")) {
var newTile = buildTile(svc);
tile.parentNode.replaceChild(newTile, tile);
continue;
}
// Update progress bar values in-place
var pct = Math.round((svc.sync_progress || 0) * 100);
var etaText = _calcBtcEta(tileId(svc), svc.sync_progress || 0);
var fill = tile.querySelector(".tile-sync-bar-fill");
var pctEl = tile.querySelector(".tile-sync-percent");
var etaEl = tile.querySelector(".tile-sync-eta");
if (fill) fill.style.width = pct + "%";
if (pctEl) pctEl.textContent = pct + "%";
if (etaEl) etaEl.textContent = etaText;
} else {
// IBD finished or not syncing — if tile had sync layout rebuild it normally
if (tile.querySelector(".tile-sync-container")) {
delete _btcSyncPrev[tileId(svc)];
var normalTile = buildTile(svc);
tile.parentNode.replaceChild(normalTile, tile);
continue;
}
var sc = statusClass(svc.health || svc.status);
var st = statusText(svc.health || svc.status, svc.enabled);
var dot = tile.querySelector(".status-dot");
var text = tile.querySelector(".status-text");
if (dot) dot.className = "status-dot " + sc;
if (text) text.textContent = st;
}
}
if (typeof window.dashboardServicesUpdated === "function") window.dashboardServicesUpdated();
}
// ── Service polling ───────────────────────────────────────────────
var _firstLoad = true;
async function refreshServices() {
try {
var services = await apiFetch("/api/services");
if (_firstLoad) { buildTiles(services, _categoryLabels); _firstLoad = false; }
else { updateTiles(services); }
// Service data has rendered — the dashboard is ready; lift the boot
// splash (no-op once lifted). Note: dashboardServicesUpdated may also
// fire from checkUpdates before this resolves, so the lift lives here.
if (typeof window.__liftAppSplash === "function") window.__liftAppSplash();
} catch (err) { console.warn("Failed to fetch services:", err); }
}
// ── Network IPs ───────────────────────────────────────────────────
async function loadNetwork() {
try {
var data = await apiFetch("/api/network");
if ($internalIp) $internalIp.textContent = data.internal_ip || "—";
if ($externalIp) $externalIp.textContent = data.external_ip || "—";
_cachedExternalIp = data.external_ip || "unavailable";
} catch (_) {
if ($internalIp) $internalIp.textContent = "—";
if ($externalIp) $externalIp.textContent = "—";
}
}
// ── Update check ──────────────────────────────────────────────────
// Paint the sidebar Update button from the last known update state.
// Called after each check AND when the button is (re)built, so the hint
// is correct regardless of which finishes first at startup.
function applyUpdateSidebarState(data) {
if (!data) return;
var sidebarUpdateBtn = document.getElementById("sidebar-btn-update");
var sidebarUpdateHint = document.getElementById("sidebar-update-hint");
if (!sidebarUpdateBtn) return;
var hasUpdates = !!data.available;
var updateStatus = data.status || "idle";
if (updateStatus === "failed") {
// Last update errored and did not apply — surface it as a persistent
// red banner that re-opens the failed run with a "Retry Update" action.
sidebarUpdateBtn.style.borderColor = "#f66151";
sidebarUpdateBtn.style.backgroundColor = "rgba(246, 97, 81, 0.10)";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update failed — click to retry";
} else if (updateStatus === "reboot_required") {
sidebarUpdateBtn.style.borderColor = "#e9b64a";
sidebarUpdateBtn.style.backgroundColor = "rgba(233, 182, 74, 0.10)";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Restart required";
} else if (updateStatus === "running") {
sidebarUpdateBtn.style.borderColor = "#78aeed";
sidebarUpdateBtn.style.backgroundColor = "rgba(120, 174, 237, 0.10)";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update in progress…";
} else if (hasUpdates) {
sidebarUpdateBtn.style.borderColor = "#3ecf8e";
sidebarUpdateBtn.style.backgroundColor = "rgba(62, 207, 142, 0.10)";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Updates available!";
} else {
sidebarUpdateBtn.style.borderColor = "";
sidebarUpdateBtn.style.backgroundColor = "";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "System is up to date";
}
}
async function checkUpdates() {
try {
var data = await apiFetch("/api/updates/check");
window._lastUpdateCheck = data;
if (typeof markUpdateChecked === "function") markUpdateChecked();
applyUpdateSidebarState(data);
// The welcome dashboard's updates card reads this state
if (typeof window.dashboardServicesUpdated === "function") window.dashboardServicesUpdated();
} catch (_) {}
}
@@ -0,0 +1,500 @@
"use strict";
// ── Update modal ──────────────────────────────────────────────────
// ── Update dialog presentation (pill, status line, last-checked) ──
var _updateLastCheckTs = 0;
function markUpdateChecked() {
_updateLastCheckTs = Date.now();
}
function _updateLastCheckedText() {
if (!$updLastChecked) return;
if (!_updateLastCheckTs) { $updLastChecked.textContent = "—"; return; }
var s = Math.floor((Date.now() - _updateLastCheckTs) / 1000);
if (s < 30) { $updLastChecked.textContent = "just now"; return; }
var m = Math.floor(s / 60);
if (m < 60) { $updLastChecked.textContent = m + (m === 1 ? " minute ago" : " minutes ago"); return; }
var h = Math.floor(m / 60);
$updLastChecked.textContent = h + (h === 1 ? " hour ago" : " hours ago");
}
function setUpdatePill(state) {
if (!$updPill) return;
var cls = "upd-pill";
var html;
if (state === "checking") {
cls += " st-loading"; html = '<span class="status-dot loading pulse"></span>Checking…';
} else if (state === "uptodate") {
cls += " st-active"; html = '<span class="status-dot active"></span>Up to date';
} else if (state === "complete") {
cls += " st-active"; html = '<span class="status-dot active"></span>Update complete';
} else if (state === "reboot") {
cls += " st-needs-attention"; html = '<span class="status-dot needs-attention pulse"></span>Restart required';
} else if (state === "failed") {
cls += " st-failed"; html = '<span class="status-dot failed"></span>Update failed';
} else if (state === "unavailable") {
cls += " st-needs-attention"; html = '<span class="status-dot needs-attention pulse"></span>Status unknown';
} else {
cls += " st-loading"; html = '<span class="status-dot loading pulse"></span>Updating…';
}
$updPill.className = cls;
$updPill.innerHTML = html;
}
// Single place that reflects update state in the dialog: status message
// text + color, header pill, and the Close / Check again availability.
function _setUpdateStatus(text) {
if ($modalStatus) $modalStatus.textContent = text;
var state;
if (text.charAt(0) === "✗") state = "failed";
else if (text.indexOf("restart required") !== -1) state = "reboot";
else if (text.charAt(0) === "✓") state = (text.indexOf("already up to date") !== -1) ? "uptodate" : "complete";
else if (text.indexOf("unavailable") !== -1) state = "unavailable";
else if (text.indexOf("Checking") === 0) state = "checking";
else state = "updating";
if ($modalStatus) {
var msg = "update-status-msg";
if (state === "failed") $modalStatus.className = msg + " st-err";
else if (state === "reboot" || state === "unavailable") $modalStatus.className = msg + " st-warn";
else if (state === "uptodate" || state === "complete") $modalStatus.className = msg + " st-ok";
else $modalStatus.className = msg;
// In the up-to-date state the green console line already says it —
// the mockup shows it exactly once.
$modalStatus.style.display = (state === "uptodate") ? "none" : "";
}
setUpdatePill(state);
var interactive = (state !== "updating");
if ($updateCloseBtn) $updateCloseBtn.disabled = !interactive;
if ($btnCheckAgain) {
$btnCheckAgain.disabled = (state === "checking" || state === "updating");
$btnCheckAgain.style.display = interactive ? "inline-flex" : "none";
}
_updateLastCheckedText();
}
async function openUpdateModal() {
if (!$modal) return;
// Open immediately in a checking state; the status/check requests below
// fill in the real result (mockup: auto-check on open).
$modal.classList.add("open");
if ($modalLog) $modalLog.innerHTML = '<span class="dim">Checking for updates…</span>';
_setUpdateStatus("Checking for updates…");
// Reattach before checking for new updates. This makes a browser reload,
// RDP reconnect, or suspended tab recover the authoritative systemd-backed
// state instead of starting over or claiming the system is merely up to date.
try {
var current = await apiFetchWithTimeout(
"/api/updates/status?offset=0",
{ cache: "no-store" },
STATUS_POLL_FETCH_TIMEOUT
);
if (current.running || current.result === "reboot_required" || current.result === "failed") {
// An in-progress update, a staged update awaiting reboot, or a prior
// failed update — reattach to the persisted systemd/log state instead of
// starting over or wrongly reporting "up to date".
showExistingUpdate(current);
return;
}
} catch (_) {
// The normal start path below has its own visible error handling.
}
apiFetchWithTimeout(
"/api/updates/check",
{ cache: "no-store" },
STATUS_POLL_FETCH_TIMEOUT
)
.then(function(data) {
markUpdateChecked();
if (!data.available) {
stopUpdatePoll();
_updateLog = "";
_updateLogOffset = 0;
_updateVisibleLogChars = 0;
_updateFinished = true;
_updateStatusUnavailable = false;
if ($modalLog) $modalLog.innerHTML = '<span class="ok">✓ System is already up to date</span>';
_setUpdateStatus("✓ System is already up to date");
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
if ($btnReboot) $btnReboot.style.display = "none";
if ($btnSave) $btnSave.style.display = "none";
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
if ($btnRetryRun) $btnRetryRun.style.display = "none";
if ($btnCloseModal) $btnCloseModal.disabled = false;
$modal.classList.add("open");
return;
}
_doOpenUpdateModal();
})
.catch(function() {
_doOpenUpdateModal();
});
}
function prepareUpdateModal() {
if (!$modal) return;
stopUpdatePoll();
_updateLog = "";
_updateLogOffset = 0;
_updateVisibleLogChars = 0;
_updatePollInFlight = false;
_serverWasDown = false;
_updateFinished = false;
_updateStatusUnavailable = false;
_updatePollFailures = 0;
if ($modalLog) $modalLog.textContent = "";
_setUpdateStatus("Starting update…");
if ($modalSpinner) $modalSpinner.classList.add("spinning");
if ($btnReboot) $btnReboot.style.display = "none";
if ($btnSave) $btnSave.style.display = "none";
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
if ($btnRetryRun) $btnRetryRun.style.display = "none";
if ($btnCloseModal) $btnCloseModal.disabled = true;
$modal.classList.add("open");
}
function _doOpenUpdateModal() {
prepareUpdateModal();
startUpdate();
}
function showExistingUpdate(data) {
prepareUpdateModal();
if (data.log) appendLog(data.log);
_updateLogOffset = Number(data.offset) || 0;
if (data.running) {
_setUpdateStatus("Updating…");
startUpdatePoll();
return;
}
_updateFinished = true;
if (data.result === "reboot_required") {
onUpdateDone("reboot_required");
} else if (data.result === "success") {
onUpdateDone(true);
} else {
onUpdateDone(false);
}
}
async function restoreUpdateModalIfNeeded() {
if (!$modal || $modal.classList.contains("open")) return;
try {
var data = await apiFetchWithTimeout(
"/api/updates/status?offset=0",
{ cache: "no-store" },
STATUS_POLL_FETCH_TIMEOUT
);
if (data.running || data.result === "reboot_required") {
showExistingUpdate(data);
}
} catch (_) {
// Dashboard startup must remain usable when status cannot be reached.
}
}
function closeUpdateModal() {
if (!$modal) return;
$modal.classList.remove("open");
stopUpdatePoll();
}
function appendLog(text) {
if (!text) return;
_updateLog += text;
if ($modalLog) {
// Appending a text node avoids reparsing/replacing the complete log on
// every two-second poll. Trim only occasionally once the visible log is
// large; the complete _updateLog remains available for error reports.
if (_updateVisibleLogChars + text.length > UPDATE_VISIBLE_LOG_MAX_CHARS) {
var tail = _updateLog.slice(-UPDATE_VISIBLE_LOG_TRIM_CHARS);
var notice = "[Earlier update output hidden from this view; it remains in the saved report.]\n\n";
$modalLog.textContent = notice + tail;
_updateVisibleLogChars = notice.length + tail.length;
} else {
$modalLog.appendChild(document.createTextNode(text));
_updateVisibleLogChars += text.length;
}
$modalLog.scrollTop = $modalLog.scrollHeight;
}
}
function startUpdate() {
apiFetchWithTimeout(
"/api/updates/run",
{ method: "POST" },
STATUS_POLL_FETCH_TIMEOUT * 2
)
.then(function(data) {
if (data.status === "no_updates") {
if ($modalLog) $modalLog.innerHTML = '<span class="ok">✓ System is already up to date</span>';
_setUpdateStatus("✓ System is already up to date");
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
if ($btnReboot) $btnReboot.style.display = "none";
if ($btnSave) $btnSave.style.display = "none";
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
if ($btnRetryRun) $btnRetryRun.style.display = "none";
if ($btnCloseModal) $btnCloseModal.disabled = false;
_updateFinished = true;
return;
}
if (data.status === "already_running") appendLog("[Update already in progress, attaching…]\n\n");
_setUpdateStatus("Updating…");
startUpdatePoll();
})
.catch(function(err) {
appendLog("[Error: failed to start update — " + err + "]\n");
onUpdateDone(false);
});
}
function startUpdatePoll() {
if (_updatePollTimer) clearInterval(_updatePollTimer);
pollUpdateStatus();
_updatePollTimer = setInterval(pollUpdateStatus, UPDATE_POLL_INTERVAL);
}
function stopUpdatePoll() {
if (_updatePollTimer) { clearInterval(_updatePollTimer); _updatePollTimer = null; }
}
async function pollUpdateStatus() {
// setInterval does not wait for an async callback. The guard prevents a slow
// request from creating overlapping, out-of-order status polls.
if (_updateFinished || _updatePollInFlight) return;
_updatePollInFlight = true;
try {
var data = await apiFetchWithTimeout(
"/api/updates/status?offset=" + _updateLogOffset,
{ cache: "no-store" },
STATUS_POLL_FETCH_TIMEOUT
);
_updatePollFailures = 0;
if (_serverWasDown) {
_serverWasDown = false;
if (!data.running) {
// The update finished while the server or browser connection was away.
// Re-fetch from offset 0 so the final result and complete tail agree.
_updateLog = "";
_updateLogOffset = 0;
_updateVisibleLogChars = 0;
if ($modalLog) $modalLog.textContent = "";
try {
var fullData = await apiFetchWithTimeout(
"/api/updates/status?offset=0",
{ cache: "no-store" },
STATUS_POLL_FETCH_TIMEOUT
);
if (fullData.log) appendLog(fullData.log);
_updateLogOffset = fullData.offset;
data = fullData;
} catch (_) {
if (data.log) appendLog(data.log);
_updateLogOffset = data.offset;
}
if (data.result === "reboot_required") {
appendLog("[Reconnected — update completed, reboot required.]\n");
} else if (data.result === "success") {
appendLog("[Reconnected — update completed successfully.]\n");
} else {
appendLog("[Reconnected — update encountered an error.]\n");
}
_updateFinished = true;
stopUpdatePoll();
if (data.result === "reboot_required") {
onUpdateDone("reboot_required");
} else {
onUpdateDone(data.result === "success");
}
return;
}
appendLog("[Update status reconnected]\n");
_setUpdateStatus("Updating…");
}
if (data.log) appendLog(data.log);
_updateLogOffset = data.offset;
if (data.running) return;
_updateFinished = true;
stopUpdatePoll();
if (data.result === "reboot_required") {
onUpdateDone("reboot_required");
} else if (data.result === "success") {
onUpdateDone(true);
} else {
onUpdateDone(false);
}
} catch (err) {
_updatePollFailures += 1;
if (_updatePollFailures >= STATUS_POLL_MAX_FAILURES) {
showUpdateStatusUnavailable();
return;
}
if (!_serverWasDown) {
_serverWasDown = true;
appendLog("\n[Update status connection interrupted — retrying…]\n");
_setUpdateStatus("Reconnecting to update…");
}
} finally {
_updatePollInFlight = false;
}
}
function showUpdateStatusUnavailable() {
_updateFinished = true;
_updateStatusUnavailable = true;
stopUpdatePoll();
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
_setUpdateStatus("Update status unavailable — update may still be running");
appendLog("\n[The Hub could not confirm update status. The background update was not stopped. Select Retry Status after reconnecting.]\n");
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "inline-flex";
if ($btnCloseModal) $btnCloseModal.disabled = false;
}
function retryUpdateStatus() {
if (!$modal) return;
_updateFinished = false;
_updateStatusUnavailable = false;
_updatePollFailures = 0;
_serverWasDown = true;
if ($modalSpinner) $modalSpinner.classList.add("spinning");
_setUpdateStatus("Reconnecting to update…");
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
if ($btnCloseModal) $btnCloseModal.disabled = true;
startUpdatePoll();
}
// Re-run a failed (or never-applied) update from scratch. The backend always
// allows this after a FAILED attempt even though flake.lock may already be
// advanced (the previous build never staged a bootable generation).
function retryUpdateRun() {
if ($btnRetryRun) $btnRetryRun.style.display = "none";
if ($btnSave) $btnSave.style.display = "none";
if ($btnReboot) $btnReboot.style.display = "none";
_doOpenUpdateModal();
}
function resumeUpdateStatusAfterInterruption() {
if (!$modal || !$modal.classList.contains("open")) return;
if (_updateStatusUnavailable) {
retryUpdateStatus();
} else if (!_updateFinished) {
pollUpdateStatus();
}
}
function onUpdateDone(result) {
_updateStatusUnavailable = false;
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
if ($btnCloseModal) $btnCloseModal.disabled = false;
if (result === true) {
_setUpdateStatus("✓ Update complete");
if ($btnReboot) $btnReboot.style.display = "inline-flex";
} else if (result === "reboot_required") {
_setUpdateStatus("✓ Update complete — restart required");
if ($btnReboot) $btnReboot.style.display = "inline-flex";
} else {
_setUpdateStatus("✗ Update failed — your system was not changed. Run the update again or save the error report for support.");
if ($btnRetryRun) $btnRetryRun.style.display = "inline-flex";
if ($btnSave) $btnSave.style.display = "inline-flex";
if ($btnReboot) $btnReboot.style.display = "none";
}
}
function saveErrorReport() {
var blob = new Blob([_updateLog], { type: "text/plain" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "sovran-update-error-" + new Date().toISOString().split(".")[0].replace(/:/g, "-") + ".txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// ── Reboot ────────────────────────────────────────────────────────
var _rebootStartTime = 0;
var _serverWentDown = false;
var _rebootFailed = false;
function _setRebootStatus(msg) {
if ($rebootSubmessage) $rebootSubmessage.textContent = msg;
}
function doReboot() {
if ($modal) $modal.classList.remove("open");
if ($rebuildModal) $rebuildModal.classList.remove("open");
stopUpdatePoll();
stopRebuildPoll();
// Reset overlay to main card
if ($rebootMainCard) $rebootMainCard.style.display = "";
if ($rebootErrorCard) $rebootErrorCard.style.display = "none";
_setRebootStatus("Sending restart request\u2026");
if ($rebootOverlay) $rebootOverlay.classList.add("visible");
_rebootStartTime = Date.now();
_serverWentDown = false;
_rebootFailed = false;
var rebootCtrl = new AbortController();
setTimeout(function() { rebootCtrl.abort(); }, REBOOT_REQUEST_TIMEOUT);
fetch("/api/reboot", { method: "POST", signal: rebootCtrl.signal })
.then(function(res) {
if (!res.ok) {
// Definitive HTTP error — server rejected the request before going down
_rebootFailed = true;
if ($rebootMainCard) $rebootMainCard.style.display = "none";
if ($rebootErrorCard) $rebootErrorCard.style.display = "";
// Leave overlay visible so the error card is shown
}
// HTTP 2xx: request accepted, proceed with polling
})
.catch(function() {
// Connection dropped or request aborted — the server is likely already going
// down as part of the restart. Treat as success and continue polling.
});
// Wait before the first check — NixOS shutdown after an update can take 20-40s
setTimeout(waitForServerReboot, REBOOT_INITIAL_DELAY);
}
function waitForServerReboot() {
if (_rebootFailed) return;
// Update status on first check (server hasn't gone down yet)
if (!_serverWentDown) _setRebootStatus("Waiting for the computer to shut down\u2026");
var controller = new AbortController();
var timeoutId = setTimeout(function() { controller.abort(); }, REBOOT_FETCH_TIMEOUT);
fetch("/api/ping", { cache: "no-store", signal: controller.signal, headers: { "Connection": "close" } })
.then(function(res) {
clearTimeout(timeoutId);
if (_serverWentDown) {
// Server is responding after having been down — reboot is complete.
// Any response (even 401/500) means the server process is back.
_setRebootStatus("System is back online. Reconnecting\u2026");
window.location.reload();
} else if ((Date.now() - _rebootStartTime) < 90000) {
// Server still responding but hasn't gone down yet — keep waiting
setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL);
} else {
// Been over 90 seconds and server is responding — just reload
_setRebootStatus("System is back online. Reconnecting\u2026");
window.location.reload();
}
})
.catch(function() {
clearTimeout(timeoutId);
if (!_serverWentDown) {
_serverWentDown = true;
_setRebootStatus("The computer is restarting\u2026");
}
setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL);
});
}
+121
View File
@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 218.44057 109.75845"
xml:space="preserve"
sodipodi:docname="sovran_systems_2.svg"
width="218.44057"
height="109.75845"
inkscape:export-filename="sovran_systems_2.svg"
inkscape:export-xdpi="169.6163"
inkscape:export-ydpi="169.6163"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs47" /><sodipodi:namedview
id="namedview45"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
showgrid="false" />
<style
type="text/css"
id="style2">
.st0{fill:#1C9954;}
.st1{fill:#077233;}
</style>
<g
id="g30"
transform="matrix(0.32162395,0,0,0.33123626,-75.234275,-114.64087)"
style="fill:#dedede;fill-opacity:1">
<path
d="m 354.93,540.02 h -18.69 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -6.92 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 15.91 c 0.51,0 0.9,-0.17 1.15,-0.5 0.26,-0.33 0.38,-0.74 0.38,-1.21 0,-0.67 -0.13,-1.16 -0.38,-1.48 -0.26,-0.31 -0.64,-0.49 -1.15,-0.53 l -8.87,-1.24 c -2.76,-0.39 -4.98,-1.3 -6.65,-2.72 -1.68,-1.42 -2.51,-3.78 -2.51,-7.1 v -6.21 c 0,-3.35 1.08,-5.92 3.25,-7.72 2.17,-1.79 5.16,-2.69 8.99,-2.69 h 16.56 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 7.04 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -13.78 c -0.51,0 -0.91,0.17 -1.18,0.5 -0.28,0.34 -0.41,0.76 -0.41,1.27 0,0.51 0.14,0.95 0.41,1.3 0.28,0.35 0.67,0.55 1.18,0.59 l 8.81,1.18 c 2.76,0.39 4.99,1.3 6.68,2.72 1.69,1.42 2.54,3.78 2.54,7.1 v 6.21 c 0,3.35 -1.09,5.92 -3.28,7.72 -2.19,1.8 -5.18,2.69 -8.96,2.69 z"
id="path4"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 412.05,528.85 c 0,1.81 -0.27,3.46 -0.8,4.94 -0.53,1.48 -1.48,2.74 -2.84,3.78 -1.36,1.04 -3.23,1.86 -5.62,2.45 -2.39,0.59 -5.41,0.89 -9.08,0.89 -3.67,0 -6.7,-0.29 -9.11,-0.89 -2.4,-0.59 -4.29,-1.41 -5.65,-2.45 -1.36,-1.04 -2.31,-2.31 -2.84,-3.78 -0.53,-1.48 -0.8,-3.12 -0.8,-4.94 v -20.17 c 0,-1.81 0.27,-3.46 0.8,-4.94 0.53,-1.48 1.48,-2.75 2.84,-3.81 1.36,-1.06 3.24,-1.89 5.65,-2.48 2.4,-0.59 5.44,-0.89 9.11,-0.89 3.67,0 6.69,0.3 9.08,0.89 2.39,0.59 4.26,1.42 5.62,2.48 1.36,1.06 2.31,2.34 2.84,3.81 0.53,1.48 0.8,3.13 0.8,4.94 z m -23.3,-2.13 c 0,0.79 0.3,1.45 0.89,1.98 0.59,0.53 1.95,0.8 4.08,0.8 2.13,0 3.49,-0.27 4.08,-0.8 0.59,-0.53 0.89,-1.19 0.89,-1.98 v -15.91 c 0,-0.75 -0.3,-1.39 -0.89,-1.92 -0.59,-0.53 -1.95,-0.8 -4.08,-0.8 -2.13,0 -3.49,0.27 -4.08,0.8 -0.59,0.53 -0.89,1.17 -0.89,1.92 z"
id="path6"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 447.12,540.02 h -15.38 c -0.75,0 -1.37,-0.14 -1.86,-0.41 -0.49,-0.28 -0.88,-0.79 -1.15,-1.54 l -5.8,-14.49 c -0.35,-0.87 -0.65,-1.63 -0.89,-2.28 -0.24,-0.65 -0.43,-1.27 -0.59,-1.86 -0.16,-0.59 -0.27,-1.21 -0.33,-1.86 -0.06,-0.65 -0.09,-1.45 -0.09,-2.4 v -15.61 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 9.4 c 0.75,0 1.32,0.17 1.71,0.5 0.39,0.34 0.59,0.88 0.59,1.63 v 16.32 c 0,0.39 0.04,0.79 0.12,1.18 0.08,0.39 0.2,0.81 0.35,1.24 l 2.78,8.28 c 0.12,0.39 0.26,0.66 0.41,0.8 0.16,0.14 0.39,0.21 0.71,0.21 h 0.65 c 0.31,0 0.55,-0.07 0.71,-0.21 0.16,-0.14 0.3,-0.4 0.41,-0.8 l 2.78,-8.34 c 0.16,-0.43 0.28,-0.85 0.35,-1.24 0.08,-0.39 0.12,-0.79 0.12,-1.18 v -16.26 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 9.28 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 15.61 c 0,0.95 -0.03,1.74 -0.09,2.4 -0.06,0.65 -0.17,1.27 -0.33,1.86 -0.16,0.59 -0.35,1.21 -0.59,1.86 -0.24,0.65 -0.53,1.41 -0.89,2.28 l -5.8,14.49 c -0.28,0.75 -0.66,1.26 -1.15,1.54 -0.45,0.28 -1.07,0.41 -1.82,0.41 z"
id="path8"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 478.69,540.02 h -9.11 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -38.32 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 19.69 c 4.41,0 7.46,0.92 9.14,2.75 1.68,1.83 2.51,4.21 2.51,7.13 v 2.72 c 0,1.66 -0.25,3.07 -0.74,4.23 -0.49,1.16 -1.35,2 -2.57,2.51 2.13,0.24 3.85,1.1 5.17,2.6 1.32,1.5 1.98,3.49 1.98,5.97 v 12.54 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -9.17 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -9.05 c 0,-0.87 -0.17,-1.51 -0.5,-1.92 -0.34,-0.41 -0.92,-0.62 -1.74,-0.62 h -8.28 v 11.59 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.32,0.34 -0.87,0.5 -1.62,0.5 z m 2.13,-31.93 v 7.57 h 4.44 c 1.02,0 1.71,-0.27 2.07,-0.8 0.36,-0.53 0.53,-1.19 0.53,-1.98 v -2.01 c 0,-0.79 -0.18,-1.45 -0.53,-1.98 -0.35,-0.53 -1.04,-0.8 -2.07,-0.8 z"
id="path10"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 525.83,537.89 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 H 515 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -15.61 c 0,-1.18 0.19,-2.54 0.56,-4.08 0.38,-1.54 0.96,-3.33 1.75,-5.38 l 5.15,-13.42 c 0.24,-0.67 0.6,-1.16 1.09,-1.48 0.49,-0.31 1.13,-0.47 1.92,-0.47 h 15.91 c 0.75,0 1.37,0.16 1.86,0.47 0.49,0.32 0.86,0.81 1.09,1.48 l 5.14,13.42 c 0.79,2.05 1.37,3.84 1.75,5.38 0.37,1.54 0.56,2.9 0.56,4.08 v 15.61 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -8.93 c -0.79,0 -1.37,-0.17 -1.74,-0.5 -0.38,-0.33 -0.56,-0.88 -0.56,-1.63 v -8.28 h -10.47 v 8.28 z m 3.37,-28.03 -2.78,8.99 h 9.29 l -2.78,-8.99 c -0.16,-0.35 -0.33,-0.61 -0.5,-0.77 -0.18,-0.16 -0.38,-0.24 -0.62,-0.24 h -1.48 c -0.24,0 -0.44,0.08 -0.62,0.24 -0.19,0.16 -0.36,0.42 -0.51,0.77 z"
id="path12"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 570.29,540.02 h -8.87 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -38.32 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 6.15 c 0.75,0 1.39,0.12 1.92,0.35 0.53,0.24 1.05,0.65 1.57,1.24 l 11.47,13.13 v -12.6 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 8.87 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 38.32 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -8.87 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -7.27 l -10.11,-12.24 v 19.51 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.35 -0.88,0.51 -1.63,0.51 z"
id="path14"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 641.61,540.02 h -18.69 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -6.92 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.34 0.88,-0.5 1.63,-0.5 h 15.91 c 0.51,0 0.9,-0.17 1.15,-0.5 0.26,-0.33 0.38,-0.74 0.38,-1.21 0,-0.67 -0.13,-1.16 -0.38,-1.48 -0.26,-0.31 -0.64,-0.49 -1.15,-0.53 l -8.87,-1.24 c -2.76,-0.39 -4.98,-1.3 -6.65,-2.72 -1.68,-1.42 -2.51,-3.78 -2.51,-7.1 v -6.21 c 0,-3.35 1.08,-5.92 3.25,-7.72 2.17,-1.79 5.16,-2.69 8.99,-2.69 h 16.56 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 7.04 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -13.78 c -0.51,0 -0.91,0.17 -1.18,0.5 -0.28,0.34 -0.41,0.76 -0.41,1.27 0,0.51 0.14,0.95 0.41,1.3 0.28,0.35 0.67,0.55 1.18,0.59 l 8.81,1.18 c 2.76,0.39 4.99,1.3 6.68,2.72 1.7,1.42 2.54,3.78 2.54,7.1 v 6.21 c 0,3.35 -1.09,5.92 -3.28,7.72 -2.19,1.8 -5.17,2.69 -8.96,2.69 z"
id="path16"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 683.66,540.02 h -9.58 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -7.57 L 662.9,518.2 c -0.91,-1.22 -1.51,-2.29 -1.8,-3.19 -0.3,-0.91 -0.44,-2.27 -0.44,-4.08 v -11.35 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 9.11 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 9.7 c 0,0.39 0.02,0.81 0.06,1.24 0.04,0.43 0.2,0.85 0.47,1.24 l 2.72,4.26 c 0.2,0.35 0.4,0.61 0.62,0.77 0.22,0.16 0.48,0.24 0.8,0.24 h 0.59 c 0.31,0 0.58,-0.08 0.8,-0.24 0.22,-0.16 0.42,-0.41 0.62,-0.77 l 2.72,-4.26 c 0.28,-0.39 0.43,-0.81 0.47,-1.24 0.04,-0.43 0.06,-0.85 0.06,-1.24 v -9.7 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 8.81 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 11.35 c 0,1.81 -0.16,3.17 -0.47,4.08 -0.32,0.91 -0.91,1.97 -1.77,3.19 l -8.99,12.18 v 7.51 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.35,0.34 -0.9,0.5 -1.64,0.5 z"
id="path18"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 725.88,540.02 h -18.69 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -6.92 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.34 0.88,-0.5 1.63,-0.5 h 15.91 c 0.51,0 0.9,-0.17 1.15,-0.5 0.26,-0.33 0.38,-0.74 0.38,-1.21 0,-0.67 -0.13,-1.16 -0.38,-1.48 -0.26,-0.31 -0.64,-0.49 -1.15,-0.53 l -8.87,-1.24 c -2.76,-0.39 -4.98,-1.3 -6.65,-2.72 -1.68,-1.42 -2.51,-3.78 -2.51,-7.1 v -6.21 c 0,-3.35 1.08,-5.92 3.25,-7.72 2.17,-1.79 5.16,-2.69 8.99,-2.69 h 16.56 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 7.04 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -13.78 c -0.51,0 -0.91,0.17 -1.18,0.5 -0.28,0.34 -0.41,0.76 -0.41,1.27 0,0.51 0.14,0.95 0.41,1.3 0.28,0.35 0.67,0.55 1.18,0.59 l 8.81,1.18 c 2.76,0.39 4.99,1.3 6.68,2.72 1.7,1.42 2.54,3.78 2.54,7.1 v 6.21 c 0,3.35 -1.09,5.92 -3.28,7.72 -2.19,1.8 -5.18,2.69 -8.96,2.69 z"
id="path20"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 766.44,540.02 h -9.58 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -29.04 h -8.69 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -7.16 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 31.22 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 7.16 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.33,0.34 -0.88,0.5 -1.63,0.5 h -8.69 v 29.04 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.33,0.34 -0.88,0.5 -1.63,0.5 z"
id="path22"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 817.06,540.02 h -27.44 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -38.32 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 27.44 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 6.92 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -16.32 v 4.55 h 11.53 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.33 0.5,0.88 0.5,1.63 v 6.33 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.33 -0.88,0.5 -1.63,0.5 h -11.53 v 5.08 h 16.32 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.33 0.5,0.88 0.5,1.63 v 6.92 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 z"
id="path24"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 839.48,540.02 h -8.81 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -38.32 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.33 0.88,-0.5 1.63,-0.5 h 9.52 c 0.63,0 1.15,0.14 1.57,0.41 0.41,0.28 0.8,0.73 1.15,1.36 l 5.32,9.64 c 0.2,0.35 0.36,0.61 0.5,0.77 0.14,0.16 0.33,0.24 0.56,0.24 h 0.53 c 0.24,0 0.42,-0.08 0.56,-0.24 0.14,-0.16 0.3,-0.41 0.5,-0.77 l 5.26,-9.64 c 0.36,-0.63 0.74,-1.08 1.15,-1.36 0.41,-0.28 0.94,-0.41 1.57,-0.41 h 9.58 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 38.32 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -9.11 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.34,-0.33 -0.5,-0.88 -0.5,-1.63 v -20.82 l -3.49,6.45 c -0.35,0.67 -0.78,1.15 -1.27,1.45 -0.49,0.3 -1.12,0.44 -1.86,0.44 h -2.37 c -0.75,0 -1.37,-0.15 -1.86,-0.44 -0.49,-0.29 -0.92,-0.78 -1.27,-1.45 l -3.49,-6.45 v 20.82 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.32,0.34 -0.87,0.5 -1.61,0.5 z"
id="path26"
style="fill:#dedede;fill-opacity:1" />
<path
d="m 900.86,540.02 h -18.69 c -0.75,0 -1.29,-0.17 -1.63,-0.5 -0.33,-0.33 -0.5,-0.88 -0.5,-1.63 v -6.92 c 0,-0.75 0.17,-1.29 0.5,-1.63 0.33,-0.34 0.88,-0.5 1.63,-0.5 h 15.91 c 0.51,0 0.9,-0.17 1.15,-0.5 0.26,-0.33 0.38,-0.74 0.38,-1.21 0,-0.67 -0.13,-1.16 -0.38,-1.48 -0.26,-0.31 -0.64,-0.49 -1.15,-0.53 l -8.87,-1.24 c -2.76,-0.39 -4.98,-1.3 -6.65,-2.72 -1.68,-1.42 -2.51,-3.78 -2.51,-7.1 v -6.21 c 0,-3.35 1.08,-5.92 3.25,-7.72 2.17,-1.79 5.16,-2.69 8.99,-2.69 h 16.56 c 0.75,0 1.29,0.17 1.63,0.5 0.33,0.34 0.5,0.88 0.5,1.63 v 7.04 c 0,0.75 -0.17,1.29 -0.5,1.63 -0.34,0.34 -0.88,0.5 -1.63,0.5 h -13.78 c -0.51,0 -0.91,0.17 -1.18,0.5 -0.28,0.34 -0.41,0.76 -0.41,1.27 0,0.51 0.14,0.95 0.41,1.3 0.28,0.35 0.67,0.55 1.18,0.59 l 8.81,1.18 c 2.76,0.39 4.99,1.3 6.68,2.72 1.7,1.42 2.54,3.78 2.54,7.1 v 6.21 c 0,3.35 -1.09,5.92 -3.28,7.72 -2.19,1.8 -5.18,2.69 -8.96,2.69 z"
id="path28"
style="fill:#dedede;fill-opacity:1" />
</g>
<g
id="g34"
transform="matrix(0.32162395,0,0,0.33123626,-75.234275,-114.64087)">
<path
class="st0"
d="m 399.59,375.57 c -75.1,0 -136.2,61.1 -136.2,136.2 0,75.1 61.1,136.2 136.2,136.2 27.74,0 53.37,-8.55 74.84,-22.9 -1.35,-1.17 -2.61,-2.4 -3.8,-3.72 -20.44,13.4 -44.79,21.29 -71.04,21.29 -72.16,0 -130.87,-58.71 -130.87,-130.87 0,-72.16 58.71,-130.87 130.87,-130.87 47.14,0 88.35,25.15 111.36,62.66 h 6.25 C 493.61,402.98 449.81,375.57 399.59,375.57 Z"
id="path32" />
</g>
<g
id="g38"
transform="matrix(0.32162395,0,0,0.33123626,-75.234275,-114.64087)">
<path
class="st1"
d="m 535.7,566.96 c -21.87,53.75 -74.6,91.78 -136.11,91.78 -81.04,0 -146.98,-65.94 -146.98,-146.98 0,-81.04 65.94,-146.98 146.98,-146.98 29.26,0 56.5,8.66 79.43,23.45 l 3.7,-3.7 c -23.92,-15.69 -52.44,-24.91 -83.13,-24.91 -83.89,0 -152.14,68.24 -152.14,152.14 0,83.9 68.24,152.14 152.14,152.14 64.42,0 119.58,-40.26 141.72,-96.94 z"
id="path36" />
</g>
<g
id="g42"
transform="matrix(0.32162395,0,0,0.33123626,-75.234275,-114.64087)"
inkscape:export-filename="./g42.svg"
inkscape:export-xdpi="169.6163"
inkscape:export-ydpi="169.6163">
<path
class="st0"
d="m 399.59,672.5 c -88.63,0 -160.73,-72.11 -160.73,-160.73 0,-88.62 72.11,-160.73 160.73,-160.73 75.66,0 139.05,52.63 156.04,123.16 h 5.06 C 543.61,400.93 478,346.1 399.6,346.1 c -91.36,0 -165.68,74.32 -165.68,165.68 0,91.36 74.32,165.68 165.68,165.68 56.18,0 105.83,-28.17 135.77,-71.07 -1.21,-1.21 -2.42,-2.42 -3.64,-3.63 -29,42.03 -77.32,69.74 -132.14,69.74 z"
id="path40" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,623 @@
/* Sovran_SystemsOS Hub — First-Boot Onboarding Wizard
Drives the 4-step post-install setup flow. */
"use strict";
// ── Constants ─────────────────────────────────────────────────────
const TOTAL_STEPS = 4;
// Steps to skip per role (step 3 involves domain setup)
// Step 2 (timezone/locale) is NEVER skipped — all roles need it.
const ROLE_SKIP_STEPS = {
"desktop": [3],
"node": [3],
};
// ── Role state (loaded at init) ───────────────────────────────────
var _onboardingRole = "server_plus_desktop";
// Domains that may need configuration, with service unit mapping for enabled check
const DOMAIN_DEFS = [
{ name: "matrix", label: "Matrix (Synapse)", unit: "matrix-synapse.service", needsDdns: true },
{ name: "haven", label: "Haven Nostr Relay", unit: "haven-relay.service", needsDdns: true },
{ name: "element-calling", label: "Element Video/Audio Calling", unit: "livekit.service", needsDdns: true },
{ name: "vaultwarden", label: "Vaultwarden (Password Vault)", unit: "vaultwarden.service", needsDdns: true },
{ name: "btcpayserver", label: "BTCPay Server", unit: "btcpayserver.service", needsDdns: true },
{ name: "nextcloud", label: "Nextcloud", unit: "phpfpm-nextcloud.service", needsDdns: true },
{ name: "wordpress", label: "WordPress", unit: "phpfpm-wordpress.service", needsDdns: true },
];
// ── State ─────────────────────────────────────────────────────────
var _currentStep = 1;
var _servicesData = null;
var _domainsData = null;
var _migrationOccurred = false;
// ── Helpers ───────────────────────────────────────────────────────
function escHtml(str) {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
async function apiFetch(path, options) {
var res = await fetch(path, options || {});
if (!res.ok) {
var detail = res.status + " " + res.statusText;
try {
var body = await res.json();
if (body && body.detail) detail = body.detail;
} catch (e) {}
throw new Error(detail);
}
return res.json();
}
function setStatus(elId, msg, type) {
var el = document.getElementById(elId);
if (!el) return;
el.textContent = msg;
el.className = "onboarding-save-status" + (type ? " onboarding-save-status--" + type : "");
}
function updateCompleteChecklist() {
var checklist = document.getElementById("onboarding-checklist");
if (!checklist) return;
var existing = document.getElementById("onboarding-migration-check");
if (_migrationOccurred) {
if (!existing) {
var li = document.createElement("li");
li.id = "onboarding-migration-check";
li.textContent = "✅ Migration password noted";
checklist.appendChild(li);
}
return;
}
if (existing) existing.remove();
}
function showMigrationStep(password) {
for (var i = 1; i <= TOTAL_STEPS; i++) {
var panel = document.getElementById("step-" + i);
if (panel) panel.style.display = "none";
}
var migrationPanel = document.getElementById("step-migration");
if (migrationPanel) migrationPanel.style.display = "";
var pw = document.getElementById("migration-password-value");
if (pw) pw.textContent = password || "";
var progressBar = document.getElementById("onboarding-progress-bar");
if (progressBar) progressBar.style.display = "none";
var nav = document.getElementById("onboarding-steps-nav");
if (nav) nav.style.display = "none";
}
function showStep1FromMigration() {
var migrationPanel = document.getElementById("step-migration");
if (migrationPanel) migrationPanel.style.display = "none";
var progressBar = document.getElementById("onboarding-progress-bar");
if (progressBar) progressBar.style.display = "";
var nav = document.getElementById("onboarding-steps-nav");
if (nav) nav.style.display = "";
showStep(1);
loadStep1();
}
// ── Progress / step navigation ────────────────────────────────────
function updateProgress(step) {
var fill = document.getElementById("onboarding-progress-fill");
if (fill) {
fill.style.width = Math.round(((step - 1) / (TOTAL_STEPS - 1)) * 100) + "%";
}
var dots = document.querySelectorAll(".onboarding-step-dot");
dots.forEach(function(dot) {
var ds = parseInt(dot.dataset.step, 10);
dot.classList.remove("active", "completed");
if (ds < step) dot.classList.add("completed");
if (ds === step) dot.classList.add("active");
});
}
function showStep(step) {
for (var i = 1; i <= TOTAL_STEPS; i++) {
var panel = document.getElementById("step-" + i);
if (panel) panel.style.display = (i === step) ? "" : "none";
}
_currentStep = step;
updateProgress(step);
// Lazy-load step content
if (step === 2) loadStep2();
if (step === 3) loadStep3();
// Step 4 (Complete) is static — no lazy-load needed
}
// Return the next step number, skipping over role-excluded steps
function nextStep(current) {
var skip = ROLE_SKIP_STEPS[_onboardingRole] || [];
var next = current + 1;
while (next < TOTAL_STEPS && skip.indexOf(next) !== -1) next++;
return next;
}
// Return the previous step number, skipping over role-excluded steps
function prevStep(current) {
var skip = ROLE_SKIP_STEPS[_onboardingRole] || [];
var prev = current - 1;
while (prev > 1 && skip.indexOf(prev) !== -1) prev--;
return prev;
}
// ── Step 1: Welcome ───────────────────────────────────────────────
async function loadStep1() {
try {
var cfg = await apiFetch("/api/config");
var badge = document.getElementById("onboarding-role-badge");
if (badge && cfg.role_label) badge.textContent = cfg.role_label;
} catch (_) {}
}
// ── Step 2: Timezone & Locale ─────────────────────────────────────
async function loadStep2() {
var body = document.getElementById("step-2-body");
if (!body || body._tzLoaded) return;
body._tzLoaded = true;
body.innerHTML = '<p class="onboarding-loading">Loading timezone data…</p>';
var timezones = [];
var currentTz = null;
var locales = [];
var currentLocale = null;
try {
var results = await Promise.all([
apiFetch("/api/system/timezones"),
apiFetch("/api/system/locales"),
]);
timezones = results[0].timezones || [];
currentTz = results[0].current || null;
locales = results[1].locales || [];
currentLocale = results[1].current || null;
} catch (err) {
body.innerHTML = '<p class="onboarding-error">⚠ Could not load timezone data: ' + escHtml(err.message) + '</p>';
return;
}
// Try to auto-detect timezone from browser
var browserTz = null;
try { browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (_) {}
var selectedTz = currentTz || browserTz || "";
var html = '';
// Timezone section
html += '<div class="onboarding-tz-group">';
html += '<label class="onboarding-domain-label" for="tz-search">🕐 Timezone</label>';
html += '<input class="onboarding-tz-search" type="text" id="tz-search" placeholder="Search timezones…" autocomplete="off" value="' + escHtml(selectedTz) + '" />';
html += '<select class="onboarding-tz-select" id="tz-select" size="5">';
timezones.forEach(function(tz) {
var sel = tz === selectedTz ? ' selected' : '';
html += '<option value="' + escHtml(tz) + '"' + sel + '>' + escHtml(tz) + '</option>';
});
html += '</select>';
html += '<p class="onboarding-hint">Current: <span id="tz-current-display">' + escHtml(selectedTz || 'Not set') + '</span></p>';
html += '</div>';
// Locale section
html += '<div class="onboarding-tz-group">';
html += '<label class="onboarding-domain-label" for="locale-select">🌐 Language / Locale</label>';
html += '<select class="onboarding-tz-select onboarding-locale-select" id="locale-select">';
locales.forEach(function(loc) {
var sel = loc === (currentLocale || 'en_US.UTF-8') ? ' selected' : '';
html += '<option value="' + escHtml(loc) + '"' + sel + '>' + escHtml(loc) + '</option>';
});
html += '</select>';
html += '</div>';
body.innerHTML = html;
// Wire timezone search filter
var tzSearch = document.getElementById('tz-search');
var tzSelect = document.getElementById('tz-select');
var tzCurrentDisplay = document.getElementById('tz-current-display');
if (tzSearch && tzSelect) {
// When typing in search box, filter the dropdown options
tzSearch.addEventListener('input', function() {
var q = tzSearch.value.toLowerCase();
var opts = tzSelect.options;
var firstVisible = null;
for (var i = 0; i < opts.length; i++) {
var match = opts[i].value.toLowerCase().indexOf(q) !== -1;
opts[i].style.display = match ? '' : 'none';
if (match && firstVisible === null) firstVisible = i;
}
if (firstVisible !== null) {
tzSelect.selectedIndex = firstVisible;
if (tzCurrentDisplay) tzCurrentDisplay.textContent = tzSelect.options[firstVisible].value;
}
});
// When selecting from dropdown, update search box
tzSelect.addEventListener('change', function() {
if (tzSelect.value) {
tzSearch.value = tzSelect.value;
if (tzCurrentDisplay) tzCurrentDisplay.textContent = tzSelect.value;
}
});
// Scroll selected option into view
if (tzSelect.selectedIndex >= 0) {
tzSelect.options[tzSelect.selectedIndex].scrollIntoView({ block: 'nearest' });
}
}
}
async function saveStep2() {
var tzSelect = document.getElementById('tz-select');
var tzSearch = document.getElementById('tz-search');
var localeSelect = document.getElementById('locale-select');
// Determine selected timezone: prefer dropdown selection, fall back to search text
var tz = (tzSelect && tzSelect.value) ? tzSelect.value : (tzSearch ? tzSearch.value.trim() : '');
var locale = localeSelect ? localeSelect.value : '';
if (!tz) {
setStatus('step-2-status', '⚠ Please select a timezone.', 'error');
return false;
}
setStatus('step-2-status', 'Saving…', 'info');
var errors = [];
try {
await apiFetch('/api/system/timezone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ timezone: tz }),
});
} catch (err) {
errors.push('Timezone: ' + err.message);
}
if (locale) {
try {
await apiFetch('/api/system/locale', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ locale: locale }),
});
} catch (err) {
errors.push('Locale: ' + err.message);
}
}
if (errors.length > 0) {
setStatus('step-2-status', '⚠ ' + errors.join('; '), 'error');
return false;
}
setStatus('step-2-status', '✓ Timezone & locale saved', 'ok');
return true;
}
// ── Step 3: Domain Configuration ─────────────────────────────────
async function loadStep3() {
var body = document.getElementById("step-3-body");
if (!body) return;
try {
// Fetch services and domains in parallel (network info is best-effort below)
var results = await Promise.all([
apiFetch("/api/services"),
apiFetch("/api/domains/status"),
]);
_servicesData = results[0];
_domainsData = results[1];
} 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) {
if (svc.enabled) enabledUnits.add(svc.unit);
});
// Filter domain defs to only those whose service is enabled
var relevantDomains = DOMAIN_DEFS.filter(function(d) {
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" })
+ '</div>';
html += '<p class="onboarding-hint">Enter the address for each service and paste its update command from Njal.la.</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 })
+ '</div>';
relevantDomains.forEach(function(d) {
var currentVal = domainValues[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) + '" />';
html += '<label class="onboarding-domain-label onboarding-domain-label--sub">Njal.la Update Command</label>';
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="ddns-input-' + escHtml(d.name) + '" data-ddns="' + escHtml(d.name) + '" placeholder="curl &quot;https://njal.la/update/?h=...&amp;k=...&amp;auto&quot;" />';
html += '<p class="onboarding-hint" style="margin-top:4px;">Paste the update command from Njal.la</p>';
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-domain="' + escHtml(d.name) + '" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
html += '<span class="onboarding-domain-save-status" id="domain-save-status-' + escHtml(d.name) + '" style="font-size:0.82rem;min-height:1.2em;"></span>';
html += '</div>';
});
}
// Contact email section
var emailVal = domainValues["sslemail"] || "";
html += '<div class="onboarding-domain-group onboarding-domain-group--email">';
html += '<label class="onboarding-domain-label">📧 Email Address</label>';
html += '<p class="onboarding-hint onboarding-hint--inline">Used to send important notices about your services.</p>';
html += '<input class="onboarding-domain-input domain-field-input" type="email" id="ssl-email-input" placeholder="you@example.com" value="' + escHtml(emailVal) + '" />';
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-email="true" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
html += '<span class="onboarding-domain-save-status" id="domain-save-status-email" style="font-size:0.82rem;min-height:1.2em;"></span>';
html += '</div>';
body.innerHTML = html;
// Wire per-field save buttons for domains
body.querySelectorAll('[data-save-domain]').forEach(function(btn) {
btn.addEventListener('click', async function() {
var domainName = btn.dataset.saveDomain;
var domainInput = document.getElementById('domain-input-' + domainName);
var ddnsInput = document.getElementById('ddns-input-' + domainName);
var statusEl = document.getElementById('domain-save-status-' + domainName);
var domainVal = domainInput ? domainInput.value.trim() : '';
var ddnsVal = ddnsInput ? ddnsInput.value.trim() : '';
if (!domainVal) {
if (statusEl) { statusEl.textContent = '⚠ Enter a domain first'; statusEl.style.color = 'var(--red)'; }
return;
}
btn.disabled = true;
btn.textContent = 'Saving…';
if (statusEl) { statusEl.textContent = ''; }
try {
await apiFetch('/api/domains/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain_name: domainName, domain: domainVal, ddns_url: ddnsVal }),
});
if (statusEl) { statusEl.textContent = '✓ Saved'; statusEl.style.color = 'var(--green)'; }
} catch (err) {
if (statusEl) { statusEl.textContent = '⚠ ' + err.message; statusEl.style.color = 'var(--red)'; }
}
btn.disabled = false;
btn.textContent = 'Save';
});
});
// Wire save button for SSL email
body.querySelectorAll('[data-save-email]').forEach(function(btn) {
btn.addEventListener('click', async function() {
var emailInput = document.getElementById('ssl-email-input');
var statusEl = document.getElementById('domain-save-status-email');
var emailVal = emailInput ? emailInput.value.trim() : '';
if (!emailVal) {
if (statusEl) { statusEl.textContent = '⚠ Enter an email first'; statusEl.style.color = 'var(--red)'; }
return;
}
btn.disabled = true;
btn.textContent = 'Saving…';
if (statusEl) { statusEl.textContent = ''; }
try {
await apiFetch('/api/domains/set-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: emailVal }),
});
if (statusEl) { statusEl.textContent = '✓ Saved'; statusEl.style.color = 'var(--green)'; }
} catch (err) {
if (statusEl) { statusEl.textContent = '⚠ ' + err.message; statusEl.style.color = 'var(--red)'; }
}
btn.disabled = false;
btn.textContent = 'Save';
});
});
}
async function saveStep3() {
setStatus("step-3-status", "Saving domains…", "info");
var errors = [];
// Save each domain input
var domainInputs = document.querySelectorAll("[data-domain]");
for (var i = 0; i < domainInputs.length; i++) {
var inp = domainInputs[i];
var domainName = inp.dataset.domain;
var domainVal = inp.value.trim();
if (!domainVal) continue; // skip empty — not required
var ddnsInput = document.getElementById("ddns-input-" + domainName);
var ddnsVal = ddnsInput ? ddnsInput.value.trim() : "";
try {
await apiFetch("/api/domains/set", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ domain_name: domainName, domain: domainVal, ddns_url: ddnsVal }),
});
} catch (err) {
errors.push(domainName + ": " + err.message);
}
}
// Save SSL email
var emailInput = document.getElementById("ssl-email-input");
if (emailInput && emailInput.value.trim()) {
try {
await apiFetch("/api/domains/set-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: emailInput.value.trim() }),
});
} catch (err) {
errors.push("SSL email: " + err.message);
}
}
if (errors.length > 0) {
setStatus("step-3-status", "⚠ Some errors: " + errors.join("; "), "error");
return false;
}
setStatus("step-3-status", "✓ Saved", "ok");
return true;
}
// ── Step 4: Complete ──────────────────────────────────────────────
async function completeOnboarding() {
var btn = document.getElementById("step-4-finish");
if (btn) { btn.disabled = true; btn.textContent = "Finishing…"; }
try {
await apiFetch("/api/onboarding/complete", { method: "POST" });
} catch (_) {
// Even if this fails, navigate to dashboard
}
window.location.href = "/";
}
// ── Event wiring ──────────────────────────────────────────────────
function wireNavButtons() {
var migrationContinue = document.getElementById("migration-password-continue");
if (migrationContinue) migrationContinue.addEventListener("click", async function() {
migrationContinue.disabled = true;
migrationContinue.textContent = "Continuing…";
setStatus("migration-password-status", "Saving acknowledgement…", "info");
try {
await apiFetch("/api/migration/password-acknowledge", { method: "POST" });
_migrationOccurred = true;
updateCompleteChecklist();
showStep1FromMigration();
} catch (err) {
setStatus("migration-password-status", "⚠ " + err.message, "error");
migrationContinue.disabled = false;
migrationContinue.textContent = "I've written it down — Continue →";
}
});
// Step 1 → next
var s1next = document.getElementById("step-1-next");
if (s1next) s1next.addEventListener("click", function() { showStep(nextStep(1)); });
// Step 2 → 3 (save timezone/locale first)
var s2next = document.getElementById("step-2-next");
if (s2next) s2next.addEventListener("click", async function() {
s2next.disabled = true;
var origText = s2next.textContent;
s2next.textContent = "Saving…";
var ok = await saveStep2();
s2next.disabled = false;
s2next.textContent = origText;
if (ok) showStep(nextStep(2));
});
// Step 3 → 4 (save domains first)
var s3next = document.getElementById("step-3-next");
if (s3next) s3next.addEventListener("click", async function() {
s3next.disabled = true;
s3next.textContent = "Saving…";
await saveStep3();
s3next.disabled = false;
s3next.textContent = "Save & Continue →";
showStep(nextStep(3));
});
// Step 4: finish
var s4finish = document.getElementById("step-4-finish");
if (s4finish) s4finish.addEventListener("click", completeOnboarding);
// Back buttons
document.querySelectorAll(".onboarding-btn-back").forEach(function(btn) {
var prev = parseInt(btn.dataset.prev, 10);
btn.addEventListener("click", function() { showStep(prevStep(prev + 1)); });
});
}
// ── Init ──────────────────────────────────────────────────────────
document.addEventListener("DOMContentLoaded", async function() {
// If onboarding is already complete, go to dashboard
try {
var status = await apiFetch("/api/onboarding/status");
if (status.complete) {
window.location.href = "/";
return;
}
} catch (_) {}
// Load role so step-skipping is applied before wiring nav buttons
try {
var cfg = await apiFetch("/api/config");
if (cfg.role) _onboardingRole = cfg.role;
} catch (_) {}
wireNavButtons();
try {
var migration = await apiFetch("/api/migration/password-status");
if (migration && migration.pending) {
updateCompleteChecklist();
showMigrationStep(migration.password || "");
return;
}
} catch (_) {}
updateCompleteChecklist();
showStep(1);
loadStep1();
});
@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#153126"/>
<stop offset="55%" stop-color="#0F241B"/>
<stop offset="100%" stop-color="#091C14"/>
</linearGradient>
<linearGradient id="outerArc" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#42F39A"/>
<stop offset="45%" stop-color="#28D978"/>
<stop offset="100%" stop-color="#1AA45D"/>
</linearGradient>
<linearGradient id="innerArc" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#27C86F"/>
<stop offset="100%" stop-color="#157E49"/>
</linearGradient>
<filter id="innerShade" x="-10%" y="-10%" width="120%" height="120%">
<feOffset dx="0" dy="2"/>
<feGaussianBlur stdDeviation="5" result="blur"/>
<feComposite in="blur" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 .18 0"/>
</filter>
</defs>
<rect width="256" height="256" rx="48" ry="48" fill="url(#bg)"/>
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5"
fill="none" stroke="rgba(255,255,255,0.08)"/>
<rect x="6" y="6" width="244" height="244" rx="42" ry="42"
fill="none" filter="url(#innerShade)"/>
<path d="M128 32 A96 96 0 1 1 58 196"
fill="none"
stroke="url(#outerArc)"
stroke-width="12"
stroke-linecap="round"/>
<path d="M128 56 A72 72 0 1 1 76 178"
fill="none"
stroke="url(#innerArc)"
stroke-width="10"
stroke-linecap="round"/>
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+283
View File
@@ -0,0 +1,283 @@
"""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
+231
View File
@@ -0,0 +1,231 @@
"""system_password.py -- the one place that knows about the 'free' (and root) password.
Rules
* /etc/shadow is the ONLY source of truth for authentication: Hub login == GDM login.
* /var/lib/secrets/free-password (0600 root) is a DISPLAY copy for the Hub's credentials
view. Only set_free_password() writes it, and it is verified against /etc/shadow before
it is shown, so the Hub can never show a stale password.
* Legacy / unknown-password machines go through the *migration* flow the onboarding already
has: generate a new diceware password, show it, and apply it on acknowledgement. Until
then the old password keeps working everywhere (shadow is untouched).
* No keyring code anywhere: the GNOME login keyring is decoupled from the login password
(blank-password keyring, pam_gnome_keyring off), so nothing has to be wiped or re-keyed.
Needs libxcrypt. On NixOS the Hub service gets: environment.LIBCRYPT = "${pkgs.libxcrypt}/lib/libcrypt.so.1"
"""
from __future__ import annotations
import ctypes
import ctypes.util
import hmac
import os
import secrets
import subprocess
import threading
from typing import Callable
USER = "free"
SECRETS_DIR = "/var/lib/secrets"
DISPLAY_FILE = f"{SECRETS_DIR}/free-password"
ROOT_DISPLAY_FILE = f"{SECRETS_DIR}/root-password"
MIGRATION_FILE = f"{SECRETS_DIR}/free-password-migration-newpass" # same path the onboarding UI already uses
LIVE_ISO_MARKER = "/iso" # NixOS live media mounts the image here; never start a migration there
_lock = threading.Lock() # crypt() returns a static buffer; serialise calls
_libcrypt = None
# ── low level ──────────────────────────────────────────────────────────────
def _crypt():
global _libcrypt
if _libcrypt is None:
path = os.environ.get("LIBCRYPT") or ctypes.util.find_library("crypt") or "libcrypt.so.1"
lib = ctypes.CDLL(path)
lib.crypt.restype = ctypes.c_char_p
lib.crypt.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
_libcrypt = lib
return _libcrypt
def shadow_hash(user: str = USER) -> str | None:
"""Password hash from /etc/shadow, or None if the account has none / is locked."""
try:
with open("/etc/shadow", encoding="utf-8") as f:
for line in f:
fields = line.rstrip("\n").split(":")
if fields[0] == user:
h = fields[1]
return h if h and h[0] not in "!*" else None
except OSError:
return None
return None
def verify_system_password(password: str, user: str = USER) -> bool:
"""True iff `password` is the account's current login password
(yescrypt, sha512crypt, ... whatever chpasswd produced)."""
h = shadow_hash(user)
if not h or not password:
return False
with _lock:
out = _crypt().crypt(password.encode(), h.encode())
return out is not None and hmac.compare_digest(out, h.encode())
def _write_secret(path: str, value: str) -> None:
os.makedirs(SECRETS_DIR, mode=0o700, exist_ok=True)
tmp = path + ".tmp"
with open(os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), "w", encoding="utf-8") as f:
f.write(value + "\n")
os.replace(tmp, path)
def _read_secret(path: str) -> str | None:
try:
with open(path, encoding="utf-8") as f:
v = f.read().rstrip("\n")
return v or None
except OSError:
return None
def _chpasswd(user: str, password: str) -> None:
import shutil
bin_ = shutil.which("chpasswd") or "/run/current-system/sw/bin/chpasswd"
subprocess.run([bin_], input=f"{user}:{password}\n", text=True, check=True, capture_output=True)
# ── the single writers ─────────────────────────────────────────────────────
def set_free_password(new_password: str) -> None:
"""System password first, then the display copy. Used by: change-password, reset,
migration acknowledgement, the CLI."""
if not new_password:
raise ValueError("empty password")
_chpasswd(USER, new_password)
_write_secret(DISPLAY_FILE, new_password)
def set_root_password(new_password: str) -> None:
"""Same contract for root (the credentials view lists it too)."""
if not new_password:
raise ValueError("empty password")
_chpasswd("root", new_password)
_write_secret(ROOT_DISPLAY_FILE, new_password)
# ── what the Hub shows ─────────────────────────────────────────────────────
def read_display_password() -> str | None:
"""The 'free' password to SHOW -- only if it really is the current one."""
pw = _read_secret(DISPLAY_FILE)
return pw if pw and verify_system_password(pw) else None
def read_root_display_password() -> str | None:
pw = _read_secret(ROOT_DISPLAY_FILE)
if not pw or ":" in pw: # ':' = scrypt hash written by older reset code -> not showable
return None
return pw if verify_system_password(pw, "root") else None
def _display_state(path: str) -> str:
""""missing" | "legacy" | "present" -- "present" only means a plaintext copy
exists; it can still be stale (use read_display_password() to check that)."""
pw = _read_secret(path)
if pw is None:
return "missing"
return "legacy" if ":" in pw else "present"
# ── migration flow (legacy machines) ────────────────────────────────────────
def migration_pending() -> str | None:
"""The generated password awaiting acknowledgement, if any."""
return _read_secret(MIGRATION_FILE)
def ensure_migration_pending(generate: Callable[[], str]) -> bool:
"""Legacy detection. Call it where onboarding status is computed.
Condition: the account HAS a password (so this is not the first boot) but the Hub
has NO display copy of it at all (file missing or still the old hash format).
Then generate a new password once and park it for the onboarding "migration
password" step. Nothing is applied until the user acknowledges, so the old
password keeps working at GDM and in the Hub. Returns True when a migration is
pending (caller re-opens onboarding).
A copy that exists but merely fails to verify (password changed in GNOME
Settings, `passwd`, or the pre-upgrade Hub) is DRIFT, not a legacy install:
it never triggers a migration and never changes any password -- it is healed
automatically at the next login via record_known_password(pw, only_if_stale=True).
Never fires on the live ISO (/iso) or while shadow is still locked."""
if os.path.exists(LIVE_ISO_MARKER):
return False
if migration_pending():
return True
if shadow_hash() is None:
return False
if _display_state(DISPLAY_FILE) == "present":
return False # copy exists: current, or just stale (drift)
_write_secret(MIGRATION_FILE, generate())
return True
def acknowledge_migration() -> bool:
"""User has written the new password down: make it real, then clear the pending file."""
pw = migration_pending()
if not pw:
return False
set_free_password(pw)
os.remove(MIGRATION_FILE)
return True
def record_known_password(password: str, only_if_stale: bool = False) -> bool:
""""Keep my current password" (migration step): if the user types the correct
current password, store it as the display copy and cancel the migration.
only_if_stale=True is the login-handler heal: write ONLY when an existing
plaintext copy is stale, never create a missing one (a missing copy is the
legacy case and belongs to the migration flow)."""
if not verify_system_password(password):
return False
if only_if_stale and _display_state(DISPLAY_FILE) != "present":
return False
if _read_secret(DISPLAY_FILE) != password:
_write_secret(DISPLAY_FILE, password)
try:
os.remove(MIGRATION_FILE)
except FileNotFoundError:
pass
return True
def generate_diceware_password(words: list[str] | None = None, n: int = 4) -> str:
"""word-word-word-word-NN, same shape the installer/reset produce (CSPRNG)."""
words = words or ("apple barn brook cabin cedar cloud coral crane delta eagle ember fern field "
"flame flora flint frost grove haven hedge holly heron jade juniper kelp larch "
"lemon lilac linden loch lotus maple marsh meadow mist mossy mount oak ocean "
"olive petal pine pixel plum pond prism quartz raven ridge river robin rocky "
"rose rowan sage sand sierra silver slate snow solar spark spruce stone storm "
"summit swift thorn tide timber torch trout vale vault vine walnut wave willow "
"wren amber aspen birch blaze bloom bluff coast copper crest dune elder fjord "
"forge glade glen glow gulf").split()
return "-".join(secrets.choice(words) for _ in range(n)) + f"-{secrets.randbelow(100):02d}"
if __name__ == "__main__": # tiny CLI for credentials.nix: echo NEW | system_password.py set-free
import sys
cmd = sys.argv[1] if len(sys.argv) > 1 else ""
if cmd in ("set-free", "set-root"):
pw = sys.stdin.readline().rstrip("\n")
(set_free_password if cmd == "set-free" else set_root_password)(pw)
print("ok")
elif cmd == "verify":
sys.exit(0 if verify_system_password(sys.stdin.readline().rstrip("\n")) else 1)
else:
print("usage: system_password.py set-free|set-root|verify (password on stdin)", file=sys.stderr)
sys.exit(2)
+79
View File
@@ -0,0 +1,79 @@
"""Thin wrapper around the systemctl CLI for Sovran_SystemsOS_Hub."""
from __future__ import annotations
import subprocess
from typing import Literal
def _run(cmd: list[str]) -> str:
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
return result.stdout.strip()
except Exception:
return ""
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"
def run_action(
action: str,
unit: str,
scope: Literal["system", "user"] = "system",
method: str = "systemctl",
) -> bool:
base_cmd = ["systemctl", f"--{scope}", action, unit]
if scope == "system" and method == "pkexec":
cmd = ["pkexec", "--user", "root"] + base_cmd
else:
cmd = base_cmd
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return result.returncode == 0
except Exception:
return False
@@ -0,0 +1,598 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sovran_SystemsOS — The Hub</title>
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/header.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/layout.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/tiles.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/modals.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/features.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/onboarding.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/support.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/domain-setup.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/security.css?v={{ asset_version }}" />
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<!-- service glyphs -->
<!-- ═══ Real service logos — extracted verbatim from app/icons/*.svg ═══
(internal ids namespaced per icon to avoid collisions) -->
<!-- symbolic helpers (nav + modal internals) -->
<!-- Bitcoin nav icon: the OFFICIAL logo silhouette (paths taken verbatim
from app/icons/bitcoin-core.svg) rendered monochrome in currentColor
so it matches the symbolic sidebar set -->
<symbol id="g-btc-sym" viewBox="-34 -34 580 580">
<path fill="currentColor" d="M317.871 7.656c-137.12-34.192-276.024 49.28-310.2 186.44-34.208 137.136 49.256 276.048 186.36 310.24 137.16 34.199 276.063-49.265 310.256-186.408 34.192-137.152-49.264-276.08-186.416-310.272m50.936 211.872c-3.688 24.936-17.512 37.008-35.864 41.24 25.2 13.12 38.024 33.239 25.809 68.12-15.16 43.319-51.176 46.976-99.072 37.912l-11.624 46.584-28.088-7 11.472-45.96a1076 1076 0 0 1-22.384-5.809l-11.512 46.177-28.056-7 11.624-46.673c-6.561-1.68-13.225-3.464-20.024-5.168l-36.552-9.111 13.943-32.152s20.696 5.504 20.416 5.096c7.952 1.969 11.48-3.216 12.872-6.672l18.368-73.64.048-.2 13.104-52.568c.344-5.968-1.712-13.496-13.088-16.336.439-.296-20.4-5.072-20.4-5.072l7.472-30 38.736 9.673-.032.144c5.824 1.448 11.824 2.824 17.937 4.216L245.423 89.2l28.072 7-11.28 45.224c7.536 1.721 15.12 3.456 22.504 5.297l11.2-44.929 28.088 7-11.504 46.145c35.464 12.215 61.401 30.527 56.304 64.591"/>
<path fill="currentColor" d="m254.647 174.6-13.983 56.08c15.855 3.951 64.735 20.071 72.656-11.656 8.248-33.096-42.817-40.472-58.673-44.424"/>
<path fill="currentColor" d="m233.608 258.984-15.425 61.832c19.04 4.729 77.769 23.584 86.448-11.296 9.072-36.376-51.984-45.784-71.023-50.536"/>
</symbol>
<symbol id="g-chat" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 5.5v7a2.5 2.5 0 0 1-2.5 2.5H10l-4 3.2c-.6.5-1 .3-1-.5V5.5A2.5 2.5 0 0 1 7.5 3h10A2.5 2.5 0 0 1 20 5.5z"/>
<circle cx="8.7" cy="9.2" r="0.5" fill="currentColor"/><circle cx="12" cy="9.2" r="0.5" fill="currentColor"/><circle cx="15.3" cy="9.2" r="0.5" fill="currentColor"/>
</g>
</symbol>
<symbol id="g-antenna" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round">
<path d="M5.4 9.9a9.3 9.3 0 0 1 13.2 0"/>
<path d="M8.1 12.9a5.6 5.6 0 0 1 7.8 0"/>
<circle cx="12" cy="16.6" r="1.5" fill="currentColor" stroke="none"/>
<path d="M12 16.6V21"/>
</g>
</symbol>
<symbol id="g-lock" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
<rect x="5.5" y="10.5" width="13" height="9.5" rx="2.6"/>
<path d="M8.5 10.5V8a3.5 3.5 0 0 1 7 0v2.5"/>
<circle cx="12" cy="15" r="1.3" fill="currentColor" stroke="none"/>
</g>
</symbol>
<!-- nav + ui symbolic icons -->
<symbol id="g-home" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 11.2L12 4.5l8 6.7"/>
<path d="M6 10.5v8a1.2 1.2 0 0 0 1.2 1.2h9.6a1.2 1.2 0 0 0 1.2-1.2v-8"/>
<path d="M10 19.7v-5.4h4v5.4"/>
</g>
</symbol>
<symbol id="g-grid" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round">
<rect x="3.5" y="3.5" width="7.2" height="7.2" rx="2"/><rect x="13.3" y="3.5" width="7.2" height="7.2" rx="2"/>
<rect x="3.5" y="13.3" width="7.2" height="7.2" rx="2"/><rect x="13.3" y="13.3" width="7.2" height="7.2" rx="2"/>
</g>
</symbol>
<symbol id="g-server" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round">
<rect x="3.5" y="3.5" width="17" height="7.4" rx="2"/><rect x="3.5" y="13.1" width="17" height="7.4" rx="2"/>
<circle cx="7.2" cy="7.2" r="0.6" fill="currentColor"/><circle cx="7.2" cy="16.8" r="0.6" fill="currentColor"/>
</g>
</symbol>
<symbol id="g-dots" viewBox="0 0 24 24">
<g fill="currentColor">
<circle cx="5" cy="5" r="1.7"/><circle cx="12" cy="5" r="1.7"/><circle cx="19" cy="5" r="1.7"/>
<circle cx="5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="19" cy="12" r="1.7"/>
<circle cx="5" cy="19" r="1.7"/><circle cx="12" cy="19" r="1.7"/><circle cx="19" cy="19" r="1.7"/>
</g>
</symbol>
<symbol id="g-refresh" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M20 12a8 8 0 1 1-2.9-6.2"/>
<path d="M20.6 2.6l-.5 4.9-4.6-1.7z" fill="currentColor" stroke="none"/>
</g>
</symbol>
<!-- Update System: down arrow into a tray (get / install updates) -->
<symbol id="g-update" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 4v10.5"/>
<path d="M7.8 10.3L12 14.5l4.2-4.2"/>
<path d="M4.5 16.5v2a2.5 2.5 0 0 0 2.5 2.5h10a2.5 2.5 0 0 0 2.5-2.5v-2"/>
</g>
</symbol>
<symbol id="g-box" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
<rect x="3.5" y="4" width="17" height="4.4" rx="1.6"/>
<path d="M5.2 8.4v9a2.4 2.4 0 0 0 2.4 2.4h8.8a2.4 2.4 0 0 0 2.4-2.4v-9"/>
<path d="M10 12.5h4"/>
</g>
</symbol>
<symbol id="g-shield-check" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2.5 4.5 5.5v6c0 4.6 3.1 8 7.5 9.9 4.4-1.9 7.5-5.3 7.5-9.9v-6z"/>
<path d="M8.6 12l2.4 2.4 4.4-4.6"/>
</g>
</symbol>
<!-- Systems Operational: activity pulse (system health) — distinct from
the Security shield above -->
<symbol id="g-pulse" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M3 12h4l3-7 4 14 3-7h4"/>
</symbol>
<symbol id="g-lifebuoy" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
<circle cx="12" cy="12" r="8.6"/><circle cx="12" cy="12" r="3.6"/>
<path d="M6 6l3.2 3.2M18 6l-3.2 3.2M6 18l3.2-3.2M18 18l-3.2-3.2"/>
</g>
</symbol>
<symbol id="g-power" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M7.2 6.1a7 7 0 1 0 9.6 0"/>
<path d="M12 2.8v8.4"/>
</g>
</symbol>
<symbol id="g-logout" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 4h4.5A1.5 1.5 0 0 1 20 5.5v13a1.5 1.5 0 0 1-1.5 1.5H14"/>
<path d="M4 12h10M10.5 8.5 14 12l-3.5 3.5"/>
</g>
</symbol>
<symbol id="g-search" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="6.5"/>
<path d="M15.8 15.8 21 21"/>
</g>
</symbol>
<symbol id="g-copy" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="11" height="11" rx="2.2"/>
<path d="M5.5 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v.5"/>
</g>
</symbol>
<symbol id="g-check" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.5l5 5 10-11"/></symbol>
<symbol id="g-chev" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d="M9.5 5.5 16 12l-6.5 6.5"/></symbol>
<symbol id="g-x" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" d="M6 6l12 12M18 6 6 18"/></symbol>
<symbol id="g-wifi" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round">
<path d="M4.5 10.2a11 11 0 0 1 15 0"/>
<path d="M7.3 13.3a7 7 0 0 1 9.4 0"/>
<circle cx="12" cy="17" r="1.4" fill="currentColor" stroke="none"/>
</g>
</symbol>
<symbol id="g-key" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
<circle cx="7.5" cy="15.5" r="4"/>
<path d="M10.4 12.6 20 3M16.2 6.8l2.4 2.4M18.8 4.2l2.2 2.2"/>
</g>
</symbol>
<symbol id="g-alert" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3.6 22.2 20.4H1.8z"/>
<path d="M12 9.8v4.6"/><circle cx="12" cy="17.2" r="0.6" fill="currentColor" stroke="none"/>
</g>
</symbol>
</svg>
<!-- ═══ BOOT SPLASH ═══ covers the shell while the first data loads;
dashboard.js lifts it once the welcome cards are rendered -->
<div id="app-splash" role="status" aria-live="polite">
<div class="splash-card">
<div class="splash-ring">
<img src="/static/sovran-hub-icon.svg" alt="" width="64" height="64" />
<span class="splash-ring-arc" aria-hidden="true"></span>
</div>
<div class="splash-title">Starting The Hub</div>
<div class="splash-sub" id="splash-sub">Gathering your services…</div>
</div>
</div>
<script>
(function () {
var splash = document.getElementById("app-splash");
var sub = document.getElementById("splash-sub");
var lifted = false;
function lift() {
if (lifted || !splash) return;
lifted = true;
splash.classList.add("done");
setTimeout(function () {
if (splash && splash.parentNode) splash.parentNode.removeChild(splash);
}, 500);
}
window.__liftAppSplash = lift;
// Reassure the user if the backend is slow (e.g. right after a reboot)
setTimeout(function () {
if (!lifted && sub) sub.textContent = "Still starting\u2026 this can take a moment after a reboot.";
}, 8000);
// Never trap the user behind the splash
setTimeout(lift, 25000);
})();
</script>
<div class="app">
<!-- ═══ SIDEBAR ═══ -->
<aside class="sidebar" id="sidebar">
<div class="brand">
<img src="/static/sovran-hub-icon.svg" class="brand-logo" alt="The Hub" />
<div class="brand-text">
<div class="brand-title">The <em>Hub</em></div>
<div class="brand-sub">Sovran_SystemsOS v{{ sovran_version }}</div>
</div>
</div>
<nav id="sidebar-nav" aria-label="Service categories"></nav>
<div class="nav-label">System</div>
<div id="sidebar-support"></div>
<!-- Feature Manager + Preferences (features.js) -->
<div id="sidebar-features"></div>
<div class="sidebar-spacer"></div>
</aside>
<!-- ═══ MAIN ═══ -->
<div class="main">
<header class="topbar">
<div class="page-title" id="page-title">Dashboard</div>
<div class="search-box">
<svg><use href="#g-search"/></svg>
<input id="search-input" type="text" placeholder="Search services…" autocomplete="off" />
</div>
<div class="top-actions">
<button class="btn btn-header-reboot" id="btn-header-reboot" title="Restart the entire computer">Reboot</button>
<button class="btn btn-logout" id="btn-logout" title="Sign out">Sign Out</button>
</div>
</header>
<main class="content" id="content">
<!-- ═══ WELCOME DASHBOARD (default view) ═══ -->
<section class="welcome" id="welcome-view">
<div class="welcome-bg" aria-hidden="true">
<div class="orb orb-a"></div>
<div class="orb orb-b"></div>
<div class="orb orb-c"></div>
</div>
<div class="welcome-inner">
<div class="welcome-greeting" id="welcome-greeting">Hello</div>
<h1 class="welcome-title">Welcome to Your <em>Sovereign</em> Digital &amp; Financial Life</h1>
<div class="welcome-meta">Sovran_SystemsOS v{{ sovran_version }}<span class="welcome-meta-sep">·</span><span id="welcome-role"></span></div>
<div class="welcome-cards" id="welcome-cards">
<div class="welcome-dyn" id="wc-systems"></div>
<div class="widget w-network" id="w-network">
<div class="widget-chip chip-green"><svg><use href="#g-wifi"/></svg></div>
<div class="w-body">
<h3>Network</h3>
<div class="net-row"><span class="net-k">LAN</span><span class="ip-value" id="ip-internal">…</span></div>
<div class="net-row"><span class="net-k">WAN</span><span class="ip-value" id="ip-external">…</span></div>
<div class="sub">sovransystemsos.local:8937</div>
</div>
</div>
<div class="welcome-dyn" id="wc-more"></div>
</div>
<button class="btn btn-ghost welcome-browse" id="welcome-browse-btn">Browse all services →</button>
</div>
</section>
<!-- ═══ SERVICES GRID ═══ -->
<div id="tiles-area" style="display:none">
<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>
</main>
</div>
</div>
<!-- ═══ SYSTEMS OPERATIONAL MODAL (dashboard.js) ═══ -->
<div class="modal-overlay" id="systems-modal" role="dialog" aria-modal="true" aria-labelledby="systems-modal-title">
<div class="creds-dialog">
<div class="creds-header">
<span class="creds-title" id="systems-modal-title">
<span class="widget-chip chip-green" style="width:54px;height:54px;border-radius:16px"><svg style="width:27px;height:27px"><use href="#g-pulse"/></svg></span>
<span>Systems Operational</span>
</span>
<button class="creds-close-btn" id="systems-close-btn" title="Close">✕</button>
</div>
<div class="creds-body" id="systems-body">
<p class="creds-loading">Loading…</p>
</div>
<!-- Auto-launch preference (features.js) — moved here from the sidebar -->
<div class="sysprefs" id="autolaunch-slot"></div>
</div>
</div>
<!-- Update modal -->
<div class="modal-overlay" id="update-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title-text">
<div class="modal-dialog">
<div class="upd-header">
<span class="widget-chip chip-sovran upd-chip"><svg class="upd-chip-svg"><use href="#g-update"/></svg></span>
<span class="upd-title-wrap">
<span class="upd-title" id="modal-title-text">Sovran_SystemsOS Update</span>
<span class="upd-sub">
<span class="ver-chip">Sovran_SystemsOS v{{ sovran_version }}</span>
<span class="upd-pill" id="upd-pill"></span>
<span class="modal-spinner" id="modal-spinner"></span>
</span>
</span>
<button class="creds-close-btn" id="update-close-btn" title="Close">✕</button>
</div>
<div class="modal-body-scroll">
<div class="sysmodal-card">
<div class="sysmodal-card-title"><svg><use href="#g-server"/></svg>System Details</div>
<div class="sysstep"><div class="sysnum">1</div><div class="sysstep-x"><div class="sysstep-t">Current version</div><div class="sysval"><span class="sysval-text">{{ sovran_version }}</span></div></div></div>
<div class="sysstep"><div class="sysnum">2</div><div class="sysstep-x"><div class="sysstep-t">Release channel</div><div class="sysval"><span class="sysval-text">stable</span></div></div></div>
<div class="sysstep"><div class="sysnum">3</div><div class="sysstep-x"><div class="sysstep-t">Last checked</div><div class="sysval"><span class="sysval-text" id="upd-last-checked">—</span></div></div></div>
</div>
<div class="update-status-msg" id="modal-status">Checking for updates…</div>
<div class="modal-log" id="modal-log" aria-live="polite"></div>
</div>
<div class="modal-footer">
<button class="btn btn-save" id="btn-save-report" style="display:none">Save Error Report</button>
<button class="btn btn-save" id="btn-retry-update-status" style="display:none">Retry Status</button>
<button class="btn btn-reboot" id="btn-retry-update" style="display:none">Retry Update</button>
<button class="btn btn-reboot" id="btn-reboot" style="display:none">Restart Entire System</button>
<span class="modal-footer-spacer"></span>
<button class="btn btn-close-modal" id="btn-close-modal" disabled>Close</button>
<button class="btn btn-primary" id="btn-check-again"><svg><use href="#g-refresh"/></svg>Check again</button>
</div>
</div>
</div>
<!-- Credentials info modal -->
<div class="modal-overlay" id="creds-modal" role="dialog" aria-modal="true" aria-labelledby="creds-modal-title">
<div class="creds-dialog">
<div class="creds-header">
<span class="creds-title" id="creds-modal-title">Service Info</span>
<button class="creds-close-btn" id="creds-close-btn" title="Close">✕</button>
</div>
<div class="creds-body" id="creds-body">
<p class="creds-loading">Loading…</p>
</div>
</div>
</div>
<!-- Tech Support modal -->
<div class="modal-overlay" id="support-modal" role="dialog" aria-modal="true" aria-labelledby="support-modal-title">
<div class="creds-dialog">
<div class="creds-header">
<span class="creds-title">
<span class="widget-chip chip-neutral" id="support-modal-chip" style="width:54px;height:54px;border-radius:16px"><svg style="width:27px;height:27px"><use href="#g-lifebuoy"/></svg></span>
<span id="support-modal-title">Tech Support</span>
</span>
<button class="creds-close-btn" id="support-close-btn" title="Close">✕</button>
</div>
<div class="creds-body" id="support-body">
<p class="creds-loading">Loading…</p>
</div>
</div>
</div>
<!-- Domain Setup Modal -->
<div class="modal-overlay" id="domain-setup-modal" role="dialog" aria-modal="true" aria-labelledby="domain-setup-title">
<div class="creds-dialog">
<div class="creds-header">
<span class="creds-title" id="domain-setup-title">🌐 Domain Setup</span>
<button class="creds-close-btn" id="domain-setup-close-btn" title="Close">✕</button>
</div>
<div class="creds-body" id="domain-setup-body"></div>
</div>
</div>
<!-- SSL Email Modal -->
<div class="modal-overlay" id="ssl-email-modal" role="dialog" aria-modal="true" aria-labelledby="ssl-email-title">
<div class="creds-dialog domain-narrow-dialog">
<div class="creds-header">
<span class="creds-title" id="ssl-email-title">📧 SSL Certificate Email</span>
<button class="creds-close-btn" id="ssl-email-close-btn" title="Close">✕</button>
</div>
<div class="creds-body">
<p class="support-desc">Let's Encrypt needs an email address for SSL certificate notifications.</p>
<div class="domain-field-group">
<label class="domain-field-label" for="ssl-email-input">Email:</label>
<input class="domain-field-input" type="email" id="ssl-email-input" placeholder="you@example.com" />
</div>
<div class="domain-field-actions">
<button class="btn btn-close-modal" id="ssl-email-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="ssl-email-save-btn">Save</button>
</div>
</div>
</div>
</div>
<!-- Feature Confirm Modal -->
<div class="modal-overlay" id="feature-confirm-modal" role="dialog" aria-modal="true" aria-labelledby="feature-confirm-title">
<div class="creds-dialog domain-narrow-dialog">
<div class="creds-header">
<span class="creds-title" id="feature-confirm-title">Confirm Action</span>
<button class="creds-close-btn" id="feature-confirm-close-btn" title="Close">✕</button>
</div>
<div class="creds-body">
<p class="support-desc" id="feature-confirm-message"></p>
<div class="domain-field-actions">
<button class="btn btn-close-modal" id="feature-confirm-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="feature-confirm-ok-btn">Continue</button>
</div>
</div>
</div>
</div>
<!-- Port Requirements Modal -->
<div class="modal-overlay" id="port-requirements-modal" role="dialog" aria-modal="true" aria-labelledby="port-req-title">
<div class="creds-dialog">
<div class="creds-header">
<span class="creds-title" id="port-req-title">🔌 Router / Firewall Port Requirements</span>
<button class="creds-close-btn" id="port-req-close-btn" title="Close">✕</button>
</div>
<div class="creds-body" id="port-req-body"></div>
</div>
</div>
<!-- Rebuild Modal -->
<div class="modal-overlay" id="rebuild-modal" role="dialog" aria-modal="true" aria-labelledby="rebuild-modal-title">
<div class="modal-dialog">
<div class="upd-header">
<span class="widget-chip chip-sovran upd-chip"><svg class="upd-chip-svg"><use href="#g-server"/></svg></span>
<span class="upd-title-wrap">
<span class="upd-title" id="rebuild-modal-title">Sovran_SystemsOS Rebuild</span>
<span class="upd-sub">
<span class="ver-chip">Sovran_SystemsOS v{{ sovran_version }}</span>
<span class="upd-pill" id="rebuild-pill"></span>
<span class="modal-spinner" id="rebuild-spinner"></span>
</span>
</span>
<button class="creds-close-btn" id="rebuild-close-hdr" title="Close">✕</button>
</div>
<div class="modal-body-scroll">
<div class="update-status-msg" id="rebuild-status">Rebuilding…</div>
<div class="modal-log" id="rebuild-log" aria-live="polite" style="display:none"></div>
</div>
<div class="modal-footer">
<button class="btn btn-save" id="rebuild-save-report" style="display:none">Save Error Report</button>
<button class="btn btn-reboot" id="rebuild-reboot-btn" style="display:none">Restart Entire System</button>
<span class="modal-footer-spacer"></span>
<button class="btn btn-close-modal" id="rebuild-close-btn" disabled>Close</button>
</div>
</div>
</div>
<!-- Upgrade Modal (Node → Server+Desktop) -->
<div class="modal-overlay" id="upgrade-modal" role="dialog" aria-modal="true" aria-labelledby="upgrade-modal-title">
<div class="creds-dialog upgrade-dialog">
<div class="creds-header">
<span class="creds-title" id="upgrade-modal-title">🚀 Upgrade to Server + Desktop</span>
<button class="creds-close-btn" id="upgrade-close-btn" title="Close">✕</button>
</div>
<div class="creds-body">
<p class="support-desc">
Upgrading to the full <strong>Server + Desktop</strong> experience will unlock all services —
encrypted messaging, password management, cloud storage, website hosting, and more.
</p>
<div class="upgrade-info-box">
<p class="upgrade-info-title">Simple, guided setup:</p>
<ul class="upgrade-info-list">
<li>Sovran_SystemsOS walks you through getting your domain from <a href="https://njal.la" target="_blank" rel="noopener noreferrer">Njal.la</a> and connecting your services</li>
<li>To make your services available outside your home, complete one router task: forward ports <strong>80 and 443</strong> to this computer</li>
</ul>
</div>
<p class="support-desc">
⚠️ <strong>Heads-up:</strong> your domain points at your home internet connection,
so anyone can look up your home IP address. Domain privacy does not hide it.
</p>
<p class="support-desc">
The Hub guides you through every step.
</p>
<p class="support-desc upgrade-rebuild-note">
The system will rebuild after upgrading. This may take several minutes.
</p>
<div class="domain-field-actions">
<button class="btn btn-close-modal" id="upgrade-cancel-btn">Cancel</button>
<button class="btn btn-primary" id="upgrade-confirm-btn">Yes, Upgrade</button>
</div>
</div>
</div>
</div>
<!-- Security Reset overlay -->
<div class="security-reset-overlay" id="security-reset-overlay">
<!-- Phase 1: wiping in progress -->
<div class="reboot-card" id="security-reset-phase1">
<div class="security-reset-overlay-icon">🛡</div>
<h2 class="reboot-title">Security Reset In Progress</h2>
<p class="reboot-message">
⚠️ Wiping all data and credentials.<br />
<strong>Do not power off your computer.</strong><br />
This may take several minutes.
</p>
<div class="reboot-dots">
<span class="reboot-dot"></span>
<span class="reboot-dot"></span>
<span class="reboot-dot"></span>
</div>
<p class="reboot-submessage" id="security-reset-overlay-step">Erasing data and resetting credentials…</p>
</div>
<!-- Phase 2: password display -->
<div class="reboot-card" id="security-reset-phase2" style="display:none;">
<div class="security-reset-overlay-icon">🔑</div>
<h2 class="reboot-title">Security Reset Complete</h2>
<p class="security-reset-password-label">Your new login password is:</p>
<div class="security-reset-password-box" id="security-reset-new-password">&nbsp;</div>
<p class="security-reset-password-warning">
✍️ <strong>Write this down now.</strong><br />
You will need it to log in to your computer<br />and the Sovran Hub at <em>sovransystemsos.local:8937</em>.
</p>
<button class="security-reset-reboot-btn" id="security-reset-reboot-btn" disabled>
I have written down my new password — Restart Entire System
</button>
</div>
</div>
<!-- Reboot overlay -->
<div class="reboot-overlay" id="reboot-overlay">
<!-- Normal restarting card -->
<div class="reboot-card" id="reboot-main-card">
<div class="reboot-icon" aria-hidden="true"></div>
<h2 class="reboot-title">Restarting Entire System</h2>
<p class="reboot-message">
The entire computer is restarting, including the desktop and all hosted services.<br />
This page will reconnect automatically when Sovran_SystemsOS is back online.
</p>
<div class="reboot-dots" aria-hidden="true">
<span class="reboot-dot"></span>
<span class="reboot-dot"></span>
<span class="reboot-dot"></span>
</div>
<p class="reboot-submessage" id="reboot-submessage" aria-live="polite">Sending restart request…</p>
</div>
<!-- Error card (shown if restart request fails definitively) -->
<div class="reboot-card" id="reboot-error-card" style="display:none">
<div class="reboot-icon" aria-hidden="true"></div>
<h2 class="reboot-title">Restart could not be started</h2>
<p class="reboot-message">
The computer did not begin restarting. No services were intentionally stopped. Please try again.
</p>
<div class="reboot-error-actions">
<button class="btn btn-close-modal" id="reboot-error-close-btn">Close</button>
<button class="btn btn-restart-amber" id="reboot-error-retry-btn">Try Again</button>
</div>
</div>
</div>
<!-- Restart Confirm Dialog -->
<div class="modal-overlay" id="restart-confirm-modal" role="dialog" aria-modal="true" aria-labelledby="restart-confirm-title">
<div class="creds-dialog domain-narrow-dialog">
<div class="creds-header">
<span class="creds-title" id="restart-confirm-title">Restart the entire computer?</span>
</div>
<div class="creds-body">
<div id="restart-conflict-box" class="restart-conflict-box" style="display:none">
<p class="restart-conflict-title">The system cannot restart right now.</p>
<p class="restart-conflict-desc">A system update, rebuild, backup, restore, or security operation is currently running. Wait for it to finish, then try again.</p>
</div>
<p class="support-desc"><strong>This will reboot the physical machine running Sovran_SystemsOS — not just the Hub.</strong></p>
<p class="support-desc">The desktop and all hosted services will stop temporarily and restart with the computer. Anyone currently using these services will be disconnected.</p>
<p class="support-desc">The system usually returns within 1–3 minutes. This page will reconnect automatically.</p>
<div class="domain-field-actions">
<button class="btn btn-close-modal" id="restart-confirm-cancel-btn">Cancel</button>
<button class="btn btn-restart-amber" id="restart-confirm-ok-btn">Restart Entire System</button>
</div>
</div>
</div>
</div>
<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>
<script src="/static/js/update.js?v={{ asset_version }}"></script>
<script src="/static/js/rebuild.js?v={{ asset_version }}"></script>
<script src="/static/js/features.js?v={{ asset_version }}"></script>
<script src="/static/js/security.js?v={{ asset_version }}"></script>
<script src="/static/js/dashboard.js?v={{ asset_version }}"></script>
<script src="/static/js/events.js?v={{ asset_version }}"></script>
</body>
</html>
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Hub — Login</title>
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
</head>
<body>
<div class="login-wrapper">
<div class="login-card">
<div class="login-header">
<img src="/static/sovran-hub-icon.svg" alt="The Hub" class="login-logo" />
<div class="login-title">The <em>Hub</em></div>
<div class="login-sub">Sovran_SystemsOS</div>
</div>
<form class="login-form" id="login-form" onsubmit="return false;">
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
autocomplete="current-password"
autofocus
placeholder="Enter your Hub password"
/>
</div>
<div class="login-error" id="login-error">Incorrect password. Please try again.</div>
<button type="submit" class="btn btn-login" id="btn-login">Sign In</button>
</form>
</div>
</div>
<script>
(function () {
var form = document.getElementById('login-form');
var input = document.getElementById('password');
var errEl = document.getElementById('login-error');
var btnEl = document.getElementById('btn-login');
form.addEventListener('submit', function () {
var password = input.value;
if (!password) return;
btnEl.disabled = true;
btnEl.textContent = 'Signing in…';
errEl.classList.remove('visible');
fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: password }),
credentials: 'same-origin',
})
.then(function (res) {
if (res.ok) {
window.location.replace('/');
} else {
return res.json().then(function (data) {
errEl.textContent = (data && data.detail) ? data.detail : 'Incorrect password. Please try again.';
errEl.classList.add('visible');
input.value = '';
input.focus();
btnEl.disabled = false;
btnEl.textContent = 'Sign In';
});
}
})
.catch(function () {
errEl.textContent = 'Network error. Please try again.';
errEl.classList.add('visible');
btnEl.disabled = false;
btnEl.textContent = 'Sign In';
});
});
})();
</script>
</body>
</html>
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sovran_SystemsOS — First-Boot Setup</title>
<link rel="stylesheet" href="/static/css/base.css" />
<link rel="stylesheet" href="/static/css/buttons.css" />
<link rel="stylesheet" href="/static/css/header.css" />
<link rel="stylesheet" href="/static/css/layout.css" />
<link rel="stylesheet" href="/static/css/tiles.css" />
<link rel="stylesheet" href="/static/css/modals.css" />
<link rel="stylesheet" href="/static/css/features.css" />
<link rel="stylesheet" href="/static/css/onboarding.css" />
<link rel="stylesheet" href="/static/css/support.css" />
<link rel="stylesheet" href="/static/css/domain-setup.css" />
</head>
<body class="onboarding-body">
<!-- Onboarding wizard container -->
<div class="onboarding-shell">
<!-- Progress bar -->
<div class="onboarding-progress-bar" id="onboarding-progress-bar">
<div class="onboarding-progress-fill" id="onboarding-progress-fill"></div>
</div>
<!-- Step indicators -->
<div class="onboarding-steps-nav" id="onboarding-steps-nav">
<span class="onboarding-step-dot" data-step="1">1</span>
<span class="onboarding-step-connector"></span>
<span class="onboarding-step-dot" data-step="2">2</span>
<span class="onboarding-step-connector"></span>
<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>
</div>
<!-- Step panels -->
<div class="onboarding-panel-wrap">
<!-- ── Migration Password Gate (pre-step) ── -->
<div class="onboarding-panel" id="step-migration" style="display:none">
<div class="onboarding-hero">
<div class="onboarding-logo">🔐</div>
<h1 class="onboarding-title">Your system has been migrated to Sovran_SystemsOS</h1>
<p class="onboarding-subtitle">Important password update required</p>
</div>
<div class="onboarding-card">
<p class="onboarding-body-text" style="text-align:center; margin-bottom:4px;">
Your new login password is:
</p>
<div id="migration-password-value" style="font-family:monospace; font-size:1.35rem; font-weight:700; color:var(--text-primary); background:rgba(62, 207, 142, 0.10); border:1.5px solid rgba(62, 207, 142, 0.35); border-radius:8px; padding:14px 24px; letter-spacing:0.04em; text-align:center; word-break:break-all; margin-bottom:8px;">
&nbsp;
</div>
<div style="padding:10px 14px; background-color:rgba(233, 182, 74, 0.1); border:1px solid rgba(233, 182, 74, 0.35); border-radius:8px; font-size:0.92rem; color:var(--yellow); line-height:1.55;">
⚠ Write this password down! You will need it to log in next time. This is also your Sovran Hub login password.
</div>
<div id="migration-password-status" class="onboarding-save-status" style="margin-top:8px;"></div>
</div>
<div class="onboarding-footer">
<div></div>
<button class="btn btn-primary" id="migration-password-continue">
I've written it down — Continue →
</button>
</div>
</div>
<!-- ── Step 1: Welcome ── -->
<div class="onboarding-panel" id="step-1">
<div class="onboarding-hero">
<div class="onboarding-logo">
<img src="/static/logo-light.svg" alt="Sovran Systems" class="onboarding-logo-img" />
</div>
<h1 class="onboarding-title">Welcome to Sovran_SystemsOS!</h1>
<p class="onboarding-subtitle">Be Digitally Sovereign</p>
</div>
<div class="onboarding-card">
<p class="onboarding-body-text">
Your system is installed and ready to configure. This wizard will guide
you through the final setup steps so everything works perfectly.
</p>
<div class="onboarding-role-row" id="onboarding-role-row">
<span class="onboarding-role-label">Your Role:</span>
<span class="onboarding-role-badge" id="onboarding-role-badge">Loading…</span>
</div>
<p class="onboarding-body-text onboarding-body-text--dim">
This setup only takes a few minutes. You can always revisit these
settings from the main Hub dashboard.
</p>
</div>
<div class="onboarding-footer">
<div></div>
<button class="btn btn-primary onboarding-btn-next" id="step-1-next">
Let's Go →
</button>
</div>
</div>
<!-- ── Step 2: Timezone &amp; Locale ── -->
<div class="onboarding-panel" id="step-2" style="display:none">
<div class="onboarding-step-header">
<span class="onboarding-step-icon">🌍</span>
<h2 class="onboarding-step-title">Timezone &amp; Locale</h2>
<p class="onboarding-step-desc">
Select your timezone and preferred language so your system clock, logs,
and services display the correct time and format.
</p>
</div>
<div class="onboarding-card" id="step-2-body">
<p class="onboarding-loading">Loading timezone data…</p>
</div>
<div id="step-2-status" class="onboarding-save-status"></div>
<div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="1">← Back</button>
<button class="btn btn-primary onboarding-btn-next" id="step-2-next">
Save &amp; Continue →
</button>
</div>
</div>
<!-- ── Step 3: Domain Configuration ── -->
<div class="onboarding-panel" id="step-3" style="display:none">
<div class="onboarding-step-header">
<span class="onboarding-step-icon">🌐</span>
<h2 class="onboarding-step-title">Connect Your Services</h2>
<p class="onboarding-step-desc">
Sovran_SystemsOS walks you through getting your domain from <strong><a href="https://njal.la" target="_blank" style="color: var(--accent-color);">Njal.la</a></strong> and connecting your services.
To make your services available outside your home, you’ll complete one router task: forward ports <strong>80</strong> and <strong>443</strong> to this computer.
</p>
</div>
<div class="onboarding-card" id="step-3-body">
<p class="onboarding-loading">Loading service information…</p>
</div>
<div id="step-3-status" class="onboarding-save-status"></div>
<div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button>
<button class="btn btn-primary onboarding-btn-next" id="step-3-next">
Save &amp; Continue →
</button>
</div>
</div>
<!-- ── Step 4: Complete ── -->
<div class="onboarding-panel" id="step-4" style="display:none">
<div class="onboarding-hero">
<div class="onboarding-logo">✅</div>
<h1 class="onboarding-title">Your Sovran_SystemsOS is Ready!</h1>
<p class="onboarding-subtitle">Setup complete</p>
</div>
<div class="onboarding-card">
<p class="onboarding-body-text">
All configuration steps are done. Head to the main Hub dashboard to
monitor your services, manage credentials, and make changes at any time.
</p>
<ul class="onboarding-checklist" id="onboarding-checklist">
<li>✅ Timezone &amp; locale configured</li>
<li>✅ Domain configuration saved</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">
Go to Dashboard →
</button>
</div>
</div>
</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>
+88
View File
@@ -0,0 +1,88 @@
"""Update-state helpers for the Sovran Hub.
The full-system updater stages a NixOS generation with ``nixos-rebuild boot``.
That generation is not active until the machine reboots — and the same is true
for updates started from a terminal or an SSH support session, which never go
near the Hub's status files.
The ONLY reliable indicator that a reboot is pending is NixOS itself: the
system profile (``/nix/var/nix/profiles/system``), which ``nixos-rebuild``
points at the newest generation on every ``boot`` AND every ``switch``, versus
``/run/current-system``, the generation actually running since the last boot.
When the two differ, a staged generation has not been booted yet.
Earlier revisions reconstructed this from a marker file and log tails written
by the Hub's own updater. Any system updated by other means — or whose
``REBOOT_REQUIRED`` status was written by an updater older than the marker
feature — left the Hub showing "Restart required" forever: the recorded
generation could never equal the (since advanced) running one, so the marker
could never be cleared.
This module has no FastAPI or systemd dependencies so the policy can be tested
without importing the Hub server.
"""
from __future__ import annotations
import os
# The NixOS system profile. ``nixos-rebuild boot`` and ``nixos-rebuild
# switch`` both add a generation here; ``boot`` additionally makes it the
# bootloader default. The path is a symlink chain (``system`` ->
# ``system-N-link`` -> ``/nix/store/...-nixos-system-...``).
BOOT_PROFILE_PATH = "/nix/var/nix/profiles/system"
CURRENT_SYSTEM_PATH = "/run/current-system"
def reboot_is_pending(
boot_profile_path: str = BOOT_PROFILE_PATH,
current_system_path: str = CURRENT_SYSTEM_PATH,
) -> bool:
"""Return whether a staged NixOS generation has not been booted yet.
This is deliberately independent of how the update was started — Hub
"Update System", terminal ``nixos-rebuild boot``, or a support session all
move the system profile the same way:
* after ``nixos-rebuild boot``: profile -> new, current -> old → pending
* after rebooting: both -> new → cleared
* after ``nixos-rebuild switch``: both move together → no reboot
ever needed (switch activates immediately)
* after a rollback: both point at the rollback target → cleared
Unreadable or missing paths are treated as "not pending": the Hub must
never demand a reboot it cannot substantiate.
"""
try:
boot_default = os.path.realpath(boot_profile_path)
current = os.path.realpath(current_system_path)
except OSError:
return False
if not os.path.exists(boot_default) or not os.path.exists(current):
return False
return boot_default != current
def effective_update_status(
status: str,
boot_profile_path: str = BOOT_PROFILE_PATH,
current_system_path: str = CURRENT_SYSTEM_PATH,
) -> str:
"""Map a persisted Hub status to the one that reflects live NixOS state.
Only ``REBOOT_REQUIRED`` is re-validated: it means "the update staged a
generation the machine has not booted into", a claim that must stay true
no matter which tool performed the last update. When the boot default IS
the running system the claim is stale — the staged generation booted, was
superseded by a newer update, or the marker was written by an updater that
could never clear it — so the effective status is ``IDLE``.
All other statuses (``RUNNING``, ``FAILED``, ``SUCCESS``, ``IDLE``) pass
through unchanged; RUNNING staleness is handled separately against the
systemd unit itself.
"""
if status == "REBOOT_REQUIRED" and not reboot_is_pending(
boot_profile_path, current_system_path
):
return "IDLE"
return status
+16
View File
@@ -0,0 +1,16 @@
{
"caddy.service": "2.11.7",
"tor.service": "0.4.9.14",
"gnome-remote-desktop.service": "50.2",
"bitcoind.service": "31.1",
"electrs.service": "0.12.0",
"lnd.service": "0.21.4-beta",
"rtl.service": "0.15.13",
"btcpayserver.service": "2.4.5",
"albyhub.service": "2.0.0",
"mempool.service": "3.3.1",
"matrix-synapse.service": "1.162.0",
"livekit.service": "1.13.8",
"vaultwarden.service": "1.37.4",
"haven-relay.service": "1.2.2"
}
-69
View File
@@ -1,69 +0,0 @@
/* ── Tile (locked dimensions via GTK min-width/height only) ── */
.sovran-tile {
border-radius: 18px;
padding: 0px;
min-width: 180px;
min-height: 210px;
transition: box-shadow 200ms ease-in-out;
}
.sovran-tile:hover {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
/* ── Tile text ─────────────────────────────────────────────── */
.tile-name {
font-weight: bold;
}
.tile-status {
}
/* ── Section headers ───────────────────────────────────────── */
.section-header {
font-weight: bold;
}
/* ── Status colors ─────────────────────────────────────────── */
.success { color: #2ec27e; }
.warning { color: #e5a50a; }
.error { color: #e01b24; }
.disabled-label { color: #888888; font-style: italic; }
/* ── Header / role ─────────────────────────────────────────── */
.hub-title {
font-weight: bold;
}
.role-badge {
padding: 2px 8px;
border-radius: 4px;
}
/* ── Update indicator ──────────────────────────────────────── */
.update-badge {
color: #2ec27e;
font-weight: bold;
}
.update-available {
background: #2ec27e;
color: white;
}
.update-available:hover {
background: #26a269;
}
/* ── IP bar ────────────────────────────────────────────────── */
.ip-bar {
padding: 10px 20px;
border-radius: 10px;
background: alpha(@card_bg_color, 0.5);
}
.ip-value {
font-family: monospace;
font-weight: bold;
color: @accent_color;
}
/* ── Grid container ────────────────────────────────────────── */
.tiles-container {
margin-left: auto;
margin-right: auto;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

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

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

@@ -0,0 +1,300 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 3440 1440"
width="3440"
height="1440"
version="1.1"
id="svg21"
sodipodi:docname="sovran-wallpaper-12-ultrawide-3440x1440.svg"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
xml:space="preserve"
inkscape:export-filename="sovran-wallpaper-12-ultrawide-3440x1440.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview21"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.657762"
inkscape:cx="1721.7474"
inkscape:cy="718.34493"
inkscape:window-width="3440"
inkscape:window-height="1363"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg21" /><defs
id="defs14"><linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1"><stop
offset="0%"
stop-color="#040706"
id="stop1" /><stop
offset="50%"
stop-color="#06100c"
id="stop2" /><stop
offset="100%"
stop-color="#050706"
id="stop3" /></linearGradient><radialGradient
id="softGlow"
cx="0"
cy="0"
r="210"
fx="0"
fy="0"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(210)"><stop
offset="0%"
stop-color="#28d978"
stop-opacity="0.04"
id="stop4" /><stop
offset="100%"
stop-color="#28d978"
stop-opacity="0"
id="stop5" /></radialGradient><linearGradient
id="tileBg"
x1="0"
y1="0"
x2="0"
y2="340"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(210)"><stop
offset="0%"
stop-color="#153126"
id="stop6" /><stop
offset="55%"
stop-color="#0F241B"
id="stop7" /><stop
offset="100%"
stop-color="#091C14"
id="stop8" /></linearGradient><linearGradient
id="outerArc"
x1="75.634857"
y1="47.268153"
x2="326.94922"
y2="298.58251"
gradientTransform="matrix(0.95194204,0,0,1.0504841,210,0)"
gradientUnits="userSpaceOnUse"><stop
offset="0%"
stop-color="#42F39A"
id="stop9" /><stop
offset="45%"
stop-color="#28D978"
id="stop10" /><stop
offset="100%"
stop-color="#1AA45D"
id="stop11" /></linearGradient><linearGradient
id="innerArc"
x1="130.64136"
y1="106.15404"
x2="258.09194"
y2="251.81184"
gradientTransform="matrix(0.95325178,0,0,1.0490408,210,0)"
gradientUnits="userSpaceOnUse"><stop
offset="0%"
stop-color="#27C86F"
id="stop12" /><stop
offset="100%"
stop-color="#157E49"
id="stop13" /></linearGradient><filter
id="tileShadow"
x="-0.12705882"
y="-0.12705882"
width="1.2541176"
height="1.2952941"><feOffset
dy="14"
id="feOffset13" /><feGaussianBlur
stdDeviation="18"
result="blur"
id="feGaussianBlur13" /><feColorMatrix
type="matrix"
values=" 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .24 0"
id="feColorMatrix13" /><feMerge
id="feMerge14"><feMergeNode
in="blur"
id="feMergeNode13" /><feMergeNode
in="SourceGraphic"
id="feMergeNode14" /></feMerge></filter><linearGradient
id="bg-7"
x1="0"
y1="0"
x2="0"
y2="256"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.79478947,0,0,0.82005964,452.62858,4.2254746)"><stop
offset="0%"
stop-color="#153126"
id="stop1-0" /><stop
offset="55%"
stop-color="#0F241B"
id="stop2-9" /><stop
offset="100%"
stop-color="#091C14"
id="stop3-3" /></linearGradient><linearGradient
id="outerArc-6"
x1="70"
y1="40"
x2="190"
y2="210"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.79478947,0,0,0.82005964,452.62858,4.2254746)"><stop
offset="0%"
stop-color="#42F39A"
id="stop4-0" /><stop
offset="45%"
stop-color="#28D978"
id="stop5-6" /><stop
offset="100%"
stop-color="#1AA45D"
id="stop6-2" /></linearGradient><linearGradient
id="innerArc-6"
x1="90"
y1="60"
x2="180"
y2="190"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.79478947,0,0,0.82005964,452.62858,4.2254746)"><stop
offset="0%"
stop-color="#27C86F"
id="stop7-1" /><stop
offset="100%"
stop-color="#157E49"
id="stop8-8" /></linearGradient><filter
id="innerShade"
x="-0.049180328"
y="-0.049180328"
width="1.0983607"
height="1.1065574"><feOffset
dx="0"
dy="2"
id="feOffset8" /><feGaussianBlur
stdDeviation="5"
result="blur"
id="feGaussianBlur8" /><feComposite
in="blur"
in2="SourceAlpha"
operator="arithmetic"
k2="-1"
k3="1"
id="feComposite8" /><feColorMatrix
type="matrix"
values=" 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .18 0"
id="feColorMatrix8" /></filter></defs><rect
width="3440"
height="1440"
fill="url(#bg)"
id="rect14" /><!-- centered for ultrawide balance --><g
id="g3"
transform="translate(18.243681,41.048282)"><g
id="g2"
transform="translate(-161.15251,-21.284294)"><g
transform="translate(1330,720)"
id="g20"><circle
cx="0"
cy="0"
r="310"
fill="none"
stroke="rgba(242,255,247,0.045)"
stroke-width="1"
id="circle15" /><circle
cx="0"
cy="0"
r="390"
fill="none"
stroke="rgba(66,243,154,0.055)"
stroke-width="2"
stroke-dasharray="3, 22"
id="circle16" /></g><g
transform="translate(1565,702)"
id="g21"><text
x="0"
y="0"
fill="#c3cbc6"
font-family="Inter, ui-sans-serif, system-ui, '-apple-system', BlinkMacSystemFont, 'Segoe UI', sans-serif"
font-size="42px"
font-weight="500"
letter-spacing="8"
id="text20">PRIVACY. SOVEREIGNTY. BITCOIN.</text><rect
x="0"
y="56"
width="240"
height="2"
rx="1"
fill="#42f39a"
id="rect21" /></g><g
id="g1"
transform="matrix(1.2581949,0,0,1.2194235,1167.4138,564.08337)"><rect
width="256"
height="256"
rx="48"
ry="48"
fill="url(#bg)"
id="rect8"
style="fill:url(#bg-7)"
x="0"
y="0" /><rect
x="1.5"
y="1.5"
width="253"
height="253"
rx="46.5"
ry="46.5"
fill="none"
stroke="rgba(255,255,255,0.08)"
id="rect9" /><rect
x="6"
y="6"
width="244"
height="244"
rx="42"
ry="42"
fill="none"
filter="url(#innerShade)"
id="rect10" /><path
d="M 128,32 A 96,96 0 1 1 58,196"
fill="none"
stroke="url(#outerArc)"
stroke-width="12"
stroke-linecap="round"
id="path10"
style="stroke:url(#outerArc-6)" /><path
d="M 128,56 A 72,72 0 1 1 76,178"
fill="none"
stroke="url(#innerArc)"
stroke-width="10"
stroke-linecap="round"
id="path11"
style="stroke:url(#innerArc-6)" /><circle
cx="128"
cy="128"
r="8"
fill="#f2fff7"
id="circle11" /><circle
cx="128"
cy="128"
r="18"
fill="none"
stroke="#7bffc0"
stroke-opacity="0.14"
stroke-width="4"
id="circle12" /></g><rect
x="0"
y="56"
width="560"
height="2"
rx="1"
fill="rgba(242,255,247,0.08)"
id="rect20"
style="fill:#b3b3b3"
transform="translate(1565,702)" /></g></g></svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

+94 -43
View File
@@ -3,7 +3,6 @@
{
imports = [
./modules/modules.nix
./iso/branding.nix
];
# ── Boot ────────────────────────────────────────────────────
@@ -11,6 +10,8 @@
boot.loader.efi.canTouchEfiVariables = true;
boot.loader.efi.efiSysMountPoint = "/boot/efi";
boot.kernelPackages = pkgs.linuxPackages_latest;
boot.kernelParams = [ "quiet" "loglevel=3" "rd.systemd.show_status=false" "udev.log_level=3" ];
boot.blacklistedKernelModules = [ "rxrpc" ];
# ── Filesystems ─────────────────────────────────────────────
fileSystems."/run/media/Second_Drive" = {
@@ -25,26 +26,50 @@
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
download-buffer-size = 524288000;
# Network resilience for cache.nixos.org (Fastly) flakiness.
connect-timeout = 10; # fail-fast on dead TCP connects (default: 0 = unlimited)
stalled-download-timeout = 90; # default 300s; retry sooner on stalled transfers
download-attempts = 7; # default 5
http-connections = 25; # cap concurrency (helps MTU/middlebox paths)
fallback = true; # build locally if a substitute can't be fetched
};
# ── Networking ──────────────────────────────────────────────
networking.hostName = "nixos";
networking.networkmanager.enable = true;
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [ 80 443 8448 3051 ];
networking.firewall.allowedUDPPorts = [ 80 443 8448 3051 ];
networking.firewall.allowedUDPPortRanges = [
{ from = 49152; to = 65535; }
];
networking.firewall.allowedUDPPorts = [ 5353 ];
# ── Avahi (mDNS) ───────────────────────────────────────────
services.avahi = {
enable = true;
hostName = "sovransystemsos";
nssmdns4 = true;
publish = { enable = true; addresses = true; };
};
# ── Locale / Time ──────────────────────────────────────────
time.timeZone = "America/Los_Angeles";
i18n.defaultLocale = "en_US.UTF-8";
time.timeZone = null;
i18n.defaultLocale = lib.mkDefault "en_US.UTF-8";
i18n.supportedLocales = [
"en_US.UTF-8/UTF-8"
"en_GB.UTF-8/UTF-8"
"es_ES.UTF-8/UTF-8"
"fr_FR.UTF-8/UTF-8"
"de_DE.UTF-8/UTF-8"
"pt_BR.UTF-8/UTF-8"
"ja_JP.UTF-8/UTF-8"
"zh_CN.UTF-8/UTF-8"
"ko_KR.UTF-8/UTF-8"
"ru_RU.UTF-8/UTF-8"
"ar_SA.UTF-8/UTF-8"
"hi_IN/UTF-8"
];
# ── Desktop ────────────────────────────────────────────────
services.displayManager.gdm.enable = true;
services.displayManager.gdm.autoSuspend = false;
services.displayManager.gdm.wayland = true;
services.desktopManager.gnome.enable = true;
services.printing.enable = true;
systemd.enableEmergencyMode = false;
@@ -67,16 +92,25 @@
extraGroups = [ "networkmanager" ];
};
services.displayManager.autoLogin.enable = true;
services.displayManager.autoLogin.user = "free";
services.displayManager.autoLogin.enable = false;
# ── Flatpak ────────────────────────────────────────────────
services.flatpak.enable = true;
systemd.services.flatpak-repo = {
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" "nss-lookup.target" ];
wants = [ "network-online.target" "nss-lookup.target" ];
path = [ pkgs.flatpak ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
Restart = "on-failure";
RestartSec = "15s";
};
unitConfig = {
StartLimitIntervalSec = 120;
StartLimitBurst = 5;
};
script = ''
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
'';
@@ -84,9 +118,9 @@
# ── Packages ───────────────────────────────────────────────
nixpkgs.config.allowUnfree = true;
nixpkgs.config.permittedInsecurePackages = [ "jitsi-meet-1.0.8043" ];
environment.systemPackages = with pkgs; [
nftables
git wget fish htop btop
gnomeExtensions.transparent-top-bar-adjustable-transparency
gnomeExtensions.dash-to-dock
@@ -99,12 +133,13 @@
ranger fastfetch gedit openssl pwgen
aspell aspellDicts.en lm_sensors
hunspell hunspellDicts.en_US
synadm brave dua bitwarden-desktop
synadm brave-origin dua
gparted pv unzip parted screen zenity
libargon2 gnome-terminal libreoffice-fresh
dig firefox element-desktop wp-cli axel
lk-jwt-service livekit-libwebrtc livekit-cli livekit
matrix-synapse age
libargon2 gnome-terminal libreoffice-stable
dig firefox wp-cli axel
lk-jwt-service livekit-libwebrtc livekit
matrix-synapse age onlyoffice-desktopeditors
tor-browser
];
# ── Shell ──────────────────────────────────────────────────
@@ -118,6 +153,14 @@
programs.fish = { enable = true; promptInit = "fastfetch"; };
# ── PostgreSQL base ────────────────────────────────────────
# Shared cluster for Nextcloud (nextclouddb) + Matrix Synapse.
# Sized for the README's Server + Desktop recommendation (32 GB RAM,
# 500 GB NVMe OS + 2 TB NVMe timechain). Postgres shares the box with
# Bitcoin Core, Electrs, LND, MariaDB, PHP-FPM and GNOME, so
# shared_buffers stays below the 25%-of-RAM dedicated-server rule.
# Fixes Nextcloud 35 Database checks (pg.cache_hit_ratio,
# pg.dead_tuples). Override in custom.nix for other hosts, e.g.:
# services.postgresql.settings.shared_buffers = lib.mkForce "512MB";
services.postgresql = {
enable = true;
authentication = lib.mkForce ''
@@ -125,6 +168,34 @@
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
'';
settings = {
# Memory — fixes low buffer cache hit ratio (stock default is
# 128MB shared_buffers). effective_cache_size is only a planner
# hint, not an allocation, so it can be generous.
# NOTE: changing shared_buffers requires a Postgres restart.
shared_buffers = "2GB";
effective_cache_size = "12GB";
maintenance_work_mem = "512MB";
work_mem = "32MB";
wal_buffers = "64MB";
# Checkpoints — spread write bursts out on NVMe. Reload-only.
min_wal_size = "1GB";
max_wal_size = "4GB";
checkpoint_completion_target = 0.9;
# Autovacuum — the stock 60s naptime can't keep up with
# Nextcloud's and Synapse's write-heavy tables (filecache,
# activity, jobs, state). Reload-only.
autovacuum_naptime = "30s";
autovacuum_vacuum_scale_factor = 0.05;
autovacuum_analyze_scale_factor = 0.025;
autovacuum_max_workers = 4;
# NVMe planner assumptions (README: NVMe OS + data disks).
random_page_cost = "1.1";
effective_io_concurrency = 200;
};
};
# ── Backups ────────────────────────────────────────────────
@@ -146,35 +217,15 @@ backup /etc/nix-bitcoin-secrets/ localhost/
};
# ── Cron ───────────────────────────────────────────────────
services.cron = {
enable = true;
systemCronJobs = [
"*/15 * * * * root /run/current-system/sw/bin/bash /var/lib/njalla/njalla.sh"
"*/15 * * * * root /run/current-system/sw/bin/bash /var/lib/external_ip/external_ip.sh"
];
};
# 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.
# ── Tor ────────────────────────────────────────────────────
services.tor = { enable = true; client.enable = true; torsocks.enable = true; };
# ── SSH ────────────────────────────────────────────────────
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "yes";
};
};
# ── Fail2Ban ───────────────────────────────────────────────
services.fail2ban = {
enable = true;
ignoreIP = [ "127.0.0.0/8" "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16" "8.8.8.8" ];
};
# ── Garbage Collection ─────────────────────────────────────
nix.gc = { automatic = true; dates = "weekly"; options = "--delete-older-than 7d"; };
system.stateVersion = "22.05";
}
}
+30 -111
View File
@@ -5,123 +5,42 @@
# #
# Sovran_SystemsOS — custom.nix #
# #
# This is YOUR configuration file. Edit it to customize #
# which services and features run on your machine. #
# Services, features, and roles are managed by the #
# Sovran Hub. Any changes you make through the Hub #
# will appear in a Hub Managed section added #
# automatically below. #
# #
# If you want to add your own NixOS modules or #
# configuration, place them here — outside of the #
# Hub Managed section. #
# #
# After making changes, rebuild with: #
# #
# nixos-rebuild switch #
# nixos-rebuild switch #
# #
###########################################################
# ─── Add your custom NixOS configuration below ───────────
# ═══════════════════════════════════════════════════════════
# STEP 1: CHOOSE YOUR ROLE
# ═══════════════════════════════════════════════════════════
#
# Your initial role was selected during installation.
# To CHANGE your role, uncomment exactly ONE of the lines below.
#
# Server+Desktop: Full server + desktop environment
# Desktop Only: Desktop environment, no server services
# Node (Bitcoin Only): Bitcoin ecosystem
#
# ───────────────────────────────────────────────────────────
# ─── Mullvad VPN (Desktop Only) ───────────────────────────
# Installed on the Desktop Role as the stock NixOS service; account, relay
# and settings belong to the app. To take it off a Desktop machine:
# services.mullvad-vpn.enable = lib.mkForce false;
# sovran_systemsOS.roles.server_plus_desktop = true;
# sovran_systemsOS.roles.desktop = true;
# sovran_systemsOS.roles.node = true;
# ─── Custom Caddy virtual hosts ──────────────────────────
# Uncomment and edit below to add your own Caddy sites:
#
# sovran_systemsOS.caddy.extraVirtualHosts = ''
# mysite.example.com {
# encode gzip zstd
# root * /var/lib/www/mysite
# php_fastcgi unix//run/phpfpm/mypool.sock
# file_server browse
# }
#
# anotherdomain.com {
# reverse_proxy localhost:9090
# }
# '';
# ═══════════════════════════════════════════════════════════
# STEP 2: SERVICES (default: ON)
# ═══════════════════════════════════════════════════════════
#
# These are all ON by default in the Server+Desktop role.
# Set any to "false" to disable it.
#
# ┌─────────────────────┬────────────────────────────────┐
# │ Service │ What it does │
# ├─────────────────────┼────────────────────────────────┤
# │ synapse │ Matrix Synapse homeserver │
# │ bitcoin │ Bitcoin ecosystem (bitcoind, │
# │ │ electrs, lnd, rtl, btcpay) │
# │ vaultwarden │ Vaultwarden password manager │
# │ wordpress │ WordPress website │
# │ nextcloud │ Nextcloud file hosting │
# └─────────────────────┴────────────────────────────────┘
#
# Example — disable WordPress and Nextcloud:
#
# sovran_systemsOS.services.wordpress = false;
# sovran_systemsOS.services.nextcloud = false;
#
# ───────────────────────────────────────────────────────────
# sovran_systemsOS.services.wordpress = false;
# ═══════════════════════════════════════════════════════════
# STEP 3: FEATURES (default: OFF)
# ═══════════════════════════════════════════════════════════
#
# These are OFF by default. Set to "true" to enable.
#
# ┌─────────────────────┬────────────────────────────────┐
# │ Feature │ What it does │
# ├─────────────────────┼────────────────────────────────┤
# │ haven │ Haven NOSTR relay & Blossom │
# │ bip110 │ BIP-110 Bitcoin Better Money │
# │ mempool │ Mempool.space block explorer │
# │ element-calling │ LiveKit server for Matrix │
# │ rdp │ GNOME Remote Desktop (RDP) │
# │ bitcoin-core │ Bitcoin Core GUI desktop app │
# └─────────────────────┴─────���──────────────────────────┘
#
# Example — enable element video calling:
#
# sovran_systemsOS.features.element-calling = true;
#
# ───────────────────────────────────────────────────────────
# sovran_systemsOS.features.element-calling = true;
# ═══════════════════════════════════════════════════════════
# STEP 4: WEB EXPOSURE (default: ON)
# ═══════════════════════════════════════════════════════════
#
# Controls whether Caddy serves this application to the web.
# (Does not stop the application itself from running).
#
# ┌─────────────────────┬────────────────────────────────┐
# │ Option │ Default │
# ├─────────────────────┼────────────────────────────────┤
# │ btcpayserver │ true (false in Node role) │
# └─────────────────────┴────────────────────────────────┘
#
# Example — hide BTCPay from the web:
#
# sovran_systemsOS.web.btcpayserver = false;
#
# ───────────────────────────────────────────────────────────
# sovran_systemsOS.web.btcpayserver = false;
# ═══════════════════════════════════════════════════════════
# STEP 5: NOSTR PUBLIC KEY (required for Haven)
# ═══════════════════════════════════════════════════════════
#
# If you enabled Haven above, paste your npub here.
# Haven will NOT start without a valid npub.
#
# Example:
#
# sovran_systemsOS.nostr_npub = "npub1abc123...";
#
# ───────────────────────────────────────────────────────────
# sovran_systemsOS.nostr_npub = "";
}
}
@@ -1,70 +0,0 @@
#!/usr/bin/env bash
cd /home/free/Downloads
#### SCRIPT 1 ####
/run/current-system/sw/bin/wget "https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/raw/branch/main/file_fixes_and_new_services/sovran-pro-flake-update.sh"
/run/current-system/sw/bin/bash /home/free/Downloads/sovran-pro-flake-update.sh
rm -rf /home/free/Downloads/sovran-pro-flake-update.sh
#### SCRIPT 2 ####
/run/current-system/sw/bin/wget "https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/raw/branch/main/file_fixes_and_new_services/add-custom-nix.sh"
/run/current-system/sw/bin/bash /home/free/Downloads/add-custom-nix.sh
rm -rf /home/free/Downloads/add-custom-nix.sh
#### SCRIPT 3 ####
/run/current-system/sw/bin/wget "https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/raw/branch/main/file_fixes_and_new_services/sovran-pro-flake-update2.sh"
/run/current-system/sw/bin/bash /home/free/Downloads/sovran-pro-flake-update2.sh
rm -rf /home/free/Downloads/sovran-pro-flake-update2.sh
#### SCRIPT 4 ####
/run/current-system/sw/bin/wget "https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/raw/branch/main/file_fixes_and_new_services/nextcloud_maintenance_window_fix.sh"
/run/current-system/sw/bin/bash /home/free/Downloads/nextcloud_maintenance_window_fix.sh
rm -rf /home/free/Downloads/nextcloud_maintenance_window_fix.sh
#### SCRIPT 5 ####
/run/current-system/sw/bin/wget "https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/raw/branch/main/file_fixes_and_new_services/add_external_backup_app.sh"
/run/current-system/sw/bin/bash /home/free/Downloads/add_external_backup_app.sh
rm -rf /home/free/Downloads/add_external_backup_app.sh
#### SCRIPT 6 ####
/run/current-system/sw/bin/wget "https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/raw/branch/main/file_fixes_and_new_services/update-agenix.sh"
/run/current-system/sw/bin/bash /home/free/Downloads/update-agenix.sh
rm -rf /home/free/Downloads/update-agenix.sh
#### SCRIPT 7 ####
/run/current-system/sw/bin/wget "https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/raw/branch/main/file_fixes_and_new_services/element-calling_haven"
/run/current-system/sw/bin/bash /home/free/Downloads/element-calling_haven.sh
rm -rf /home/free/Downloads/element-calling_haven.sh
#### REMOVAL OF MAIN SCRIPT ####
rm -rf /home/free/Downloads/Sovran_SystemsOS_File_Fixes_And_New_Services.sh
@@ -1,81 +0,0 @@
#!/usr/bin/env bash
function log_console () {
echo "`date` :: $1" >> /var/lib/beacons/awesome.log
echo $1
}
#### CHECK TO SEE IF IT HAS BEEN RUN BEFORE ####
FILE=/var/lib/beacons/file_fixes_and_new_services/add-custom-nix/completed
if [ -e $FILE ]; then
/run/current-system/sw/bin/echo "File Found :), No Need to Run ... Exiting"
exit 1
fi
#### CREATE INITIAL TAG ####
/run/current-system/sw/bin/mkdir -p /var/lib/beacons/file_fixes_and_new_services/add-custom-nix ; touch /var/lib/beacons/file_fixes_and_new_services/add-custom-nix/started
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Initial Tag"
exit 1
fi
#### MAIN SCRIPT ####
touch /etc/nixos/custom.nix
/run/current-system/sw/bin/cat > /etc/nixos/custom.nix <<- "EOF"
{config, pkgs, lib, ...}:
# Add custom NixOS modules here.
let
personalization = import ./personalization.nix;
in
{
}
EOF
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Run add-custom-nix"
exit 1
fi
#### CREATE COMPELETE TAG ####
/run/current-system/sw/bin/touch /var/lib/beacons/file_fixes_and_new_services/add-custom-nix/completed
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Completed Tag"
exit 1
fi
exit 0
@@ -1,66 +0,0 @@
#!/usr/bin/env bash
function log_console () {
echo "`date` :: $1" >> /var/lib/beacons/awesome.log
echo $1
}
#### CHECK TO SEE IF IT HAS BEEN RUN BEFORE ####
FILE=/var/lib/beacons/file_fixes_and_new_services/add_external_backup_app/completed
if [ -e $FILE ]; then
/run/current-system/sw/bin/echo "File Found :), No Need to Run ... Exiting"
exit 1
fi
#### CREATE INITIAL TAG ####
/run/current-system/sw/bin/mkdir -p /var/lib/beacons/file_fixes_and_new_services/add_external_backup_app ; touch /var/lib/beacons/file_fixes_and_new_services/add_external_backup_app/started
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Initial Tag"
exit 1
fi
#### MAIN SCRIPT ####
cd /home/free/Downloads
/run/current-system/sw/bin/wget "https://git.sovransystems.com/Sovran_Systems/Software/raw/branch/main/Sovran_SystemsOS_External_Backup/sovran_systemsOS_external_backup_local_installer/sovran_systemsOS_external_backup_install.sh"
/run/current-system/sw/bin/bash "sovran_systemsOS_external_backup_install.sh"
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Run add_external_backup_app"
exit 1
fi
#### CREATE COMPELETE TAG ####
/run/current-system/sw/bin/touch /var/lib/beacons/file_fixes_and_new_services/add_external_backup_app/completed
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Completed Tag"
exit 1
fi
exit 0
@@ -1,63 +0,0 @@
#!/usr/bin/env bash
function log_console () {
echo "`date` :: $1" >> /var/lib/beacons/awesome.log
echo $1
}
#### CHECK TO SEE IF IT HAS BEEN RUN BEFORE ####
FILE=/var/lib/beacons/file_fixes_and_new_services/element-calling_haven/completed
if [ -e $FILE ]; then
/run/current-system/sw/bin/echo "File Found :), No Need to Run ... Exiting"
exit 1
fi
#### CREATE INITIAL TAG ####
/run/current-system/sw/bin/mkdir -p /var/lib/beacons/file_fixes_and_new_services/element-calling_haven ; touch /var/lib/beacons/file_fixes_and_new_services/element-calling_haven/started
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Initial Tag"
exit 1
fi
#### MAIN SCRIPT ####
touch /var/lib/domains/haven
touch /var/lib/domains/element-calling
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Run element-calling_haven"
exit 1
fi
#### CREATE COMPELETE TAG ####
/run/current-system/sw/bin/touch /var/lib/beacons/file_fixes_and_new_services/element-calling_haven/completed
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Completed Tag"
exit 1
fi
exit 0
@@ -1,62 +0,0 @@
#!/usr/bin/env bash
function log_console () {
echo "`date` :: $1" >> /var/lib/beacons/awesome.log
echo $1
}
#### CHECK TO SEE IF IT HAS BEEN RUN BEFORE ####
FILE=/var/lib/beacons/file_fixes_and_new_services/nextcloud_maintenance_window_fix/completed
if [ -e $FILE ]; then
/run/current-system/sw/bin/echo "File Found :), No Need to Run ... Exiting"
exit 1
fi
#### CREATE INITIAL TAG ####
/run/current-system/sw/bin/mkdir -p /var/lib/beacons/file_fixes_and_new_services/nextcloud_maintenance_window_fix ; touch /var/lib/beacons/file_fixes_and_new_services/nextcloud_maintenance_window_fix/started
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Initial Tag"
exit 1
fi
#### MAIN SCRIPT ####
/run/wrappers/bin/sudo -u caddy /run/current-system/sw/bin/php /var/lib/www/nextcloud/occ config:system:set maintenance_window_start --type=integer --value=1
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Run add-custom-nix"
exit 1
fi
#### CREATE COMPELETE TAG ####
/run/current-system/sw/bin/touch /var/lib/beacons/file_fixes_and_new_services/nextcloud_maintenance_window_fix/completed
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Completed Tag"
exit 1
fi
exit 0
@@ -1,96 +0,0 @@
#!/usr/bin/env bash
function log_console () {
echo "`date` :: $1" >> /var/lib/beacons/awesome.log
echo $1
}
#### CHECK TO SEE IF IT HAS BEEN RUN BEFORE ####
FILE=/var/lib/beacons/file_fixes_and_new_services/sovran-pro-flake-update/completed
if [ -e $FILE ]; then
/run/current-system/sw/bin/echo "File Found :), No Need to Run ... Exiting"
exit 1
fi
#### CREATE INITIAL TAG ####
/run/current-system/sw/bin/mkdir -p /var/lib/beacons/file_fixes_and_new_services/sovran-pro-flake-update ; touch /var/lib/beacons/file_fixes_and_new_services/sovran-pro-flake-update/started
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Initial Tag"
exit 1
fi
#### MAIN SCRIPT ####
/run/current-system/sw/bin/rm /etc/nixos/flake.nix
/run/current-system/sw/bin/cat > /etc/nixos/flake.nix <<- "EOF"
{
description = "Sovran_SystemsOS for the Sovran Pro from Sovran Systems";
inputs = {
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS";
};
outputs = { self, Sovran_Systems, ... }@inputs: {
nixosConfigurations."nixos" = Sovran_Systems.inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./hardware-configuration.nix
Sovran_Systems.nixosModules.Sovran_SystemsOS
];
};
};
}
EOF
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Run sovran-pro-flake-update"
exit 1
fi
#### CREATE COMPELETE TAG ####
/run/current-system/sw/bin/touch /var/lib/beacons/file_fixes_and_new_services/sovran-pro-flake-update/completed
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Completed Tag"
exit 1
fi
exit 0
@@ -1,98 +0,0 @@
#!/usr/bin/env bash
function log_console () {
echo "`date` :: $1" >> /var/lib/beacons/awesome.log
echo $1
}
#### CHECK TO SEE IF IT HAS BEEN RUN BEFORE ####
FILE=/var/lib/beacons/file_fixes_and_new_services/sovran-pro-flake-update2/completed
if [ -e $FILE ]; then
/run/current-system/sw/bin/echo "File Found :), No Need to Run ... Exiting"
exit 1
fi
#### CREATE INITIAL TAG ####
/run/current-system/sw/bin/mkdir -p /var/lib/beacons/file_fixes_and_new_services/sovran-pro-flake-update2 ; touch /var/lib/beacons/file_fixes_and_new_services/sovran-pro-flake-update2/started
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Initial Tag"
exit 1
fi
#### MAIN SCRIPT ####
/run/current-system/sw/bin/rm /etc/nixos/flake.nix
/run/current-system/sw/bin/cat > /etc/nixos/flake.nix <<- "EOF"
{
description = "Sovran_SystemsOS for the Sovran Pro from Sovran Systems";
inputs = {
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS";
};
outputs = { self, Sovran_Systems, ... }@inputs: {
nixosConfigurations."nixos" = Sovran_Systems.inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./custom.nix
./hardware-configuration.nix
Sovran_Systems.nixosModules.Sovran_SystemsOS
];
};
};
}
EOF
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Run sovran-pro-flake-update2"
exit 1
fi
#### CREATE COMPELETE TAG ####
/run/current-system/sw/bin/touch /var/lib/beacons/file_fixes_and_new_services/sovran-pro-flake-update2/completed
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Completed Tag"
exit 1
fi
exit 0
@@ -1,83 +0,0 @@
#!/usr/bin/env bash
#### CHECK TO SEE IF IT HAS BEEN RUN BEFORE ####
FILE=/var/lib/beacons/file_fixes_and_new_services/update-agenix/completed
if [ -e $FILE ]; then
/run/current-system/sw/bin/echo "File Found :), No Need to Run ... Exiting"
exit 1
fi
#### CREATE INITIAL TAG ####
/run/current-system/sw/bin/mkdir -p /var/lib/beacons/file_fixes_and_new_services/update-agenix ; touch /var/lib/beacons/file_fixes_and_new_services/update-agenix/started
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Initial Tag"
exit 1
fi
#### MAIN SCRIPT ####
/run/current-system/sw/bin/rm -rf /var/lib/agenix-secrets/nextclouddb.age
/run/current-system/sw/bin/rm -rf /var/lib/agenix-secrets/wordpressdb.age
/run/current-system/sw/bin/rm -rf /var/lib/agenix-secrets/turn.age
/run/current-system/sw/bin/rm -rf /var/lib/agenix-secrets/matrixdb.age
/run/current-system/sw/bin/rm -rf /var/lib/agenix-secrets/matrix_reg_secret.age
pushd /var/lib/agenix-secrets/
/run/current-system/sw/bin/echo -n $(/run/current-system/sw/bin/cat /var/lib/secrets/wordpressdb) | EDITOR='/run/current-system/sw/bin/cp /dev/stdin' /run/current-system/sw/bin/nix run github:ryantm/agenix -- -e wordpressdb.age -i /root/.ssh/agenix/agenix-secret-keys
/run/current-system/sw/bin/echo -n $(/run/current-system/sw/bin/cat /var/lib/secrets/nextclouddb) | EDITOR='/run/current-system/sw/bin/cp /dev/stdin' /run/current-system/sw/bin/nix run github:ryantm/agenix -- -e nextclouddb.age -i /root/.ssh/agenix/agenix-secret-keys
/run/current-system/sw/bin/echo -n $(/run/current-system/sw/bin/cat /var/lib/secrets/matrixdb) | EDITOR='/run/current-system/sw/bin/cp /dev/stdin' /run/current-system/sw/bin/nix run github:ryantm/agenix -- -e matrixdb.age -i /root/.ssh/agenix/agenix-secret-keys
/run/current-system/sw/bin/echo -n $(/run/current-system/sw/bin/cat /var/lib/secrets/turn) | EDITOR='/run/current-system/sw/bin/cp /dev/stdin' /run/current-system/sw/bin/nix run github:ryantm/agenix -- -e turn.age -i /root/.ssh/agenix/agenix-secret-keys
/run/current-system/sw/bin/echo -n $(/run/current-system/sw/bin/cat /var/lib/secrets/matrix_reg_secret) | EDITOR='/run/current-system/sw/bin/cp /dev/stdin' /run/current-system/sw/bin/nix run github:ryantm/agenix -- -e matrix_reg_secret.age -i /root/.ssh/agenix/agenix-secret-keys
popd
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Run update-agenix"
exit 1
fi
#### CREATE COMPELETE TAG ####
/run/current-system/sw/bin/touch /var/lib/beacons/file_fixes_and_new_services/update-agenix/completed
if [[ $? != 0 ]]; then
/run/current-system/sw/bin/echo "Could Not Create Completed Tag"
exit 1
fi
exit 0
Generated
+70 -216
View File
@@ -1,34 +1,15 @@
{
"nodes": {
"bip110": {
"btc-clients": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1773169138,
"narHash": "sha256-6X41z8o2z8KjF4gMzLTPD41WjvCDGXTc0muPGmwcOMk=",
"owner": "emmanuelrosa",
"repo": "bitcoin-knots-bip-110-nix",
"rev": "b9d018b71e20ce8c1567cbc2401b6edc2c1c7793",
"type": "github"
},
"original": {
"owner": "emmanuelrosa",
"repo": "bitcoin-knots-bip-110-nix",
"type": "github"
}
},
"btc-clients": {
"inputs": {
"nixpkgs": "nixpkgs_2",
"oldNixpkgs": "oldNixpkgs"
},
"locked": {
"lastModified": 1774797058,
"narHash": "sha256-URUOiKNjG3s7vDkTj554+3yzQ0qqNQoQwHdc7vs63X0=",
"lastModified": 1792157233,
"narHash": "sha256-XQnyEc+Pj0Tg21ckbPMri0fiK/exCzhBw6wNgd1INqw=",
"owner": "emmanuelrosa",
"repo": "btc-clients-nix",
"rev": "a10dae067da04b7b170eed73efc665d27fc0e0c5",
"rev": "990d29e23437f30b8573f640d3e323167de1dc7b",
"type": "github"
},
"original": {
@@ -37,32 +18,6 @@
"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": [
@@ -71,11 +26,11 @@
]
},
"locked": {
"lastModified": 1769996383,
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
"lastModified": 1790898470,
"narHash": "sha256-zTwuwezD0w3hpga6a6P8emidzAZFyWqlNeRBlpWOOl8=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
"rev": "024633cd702b10285db5cb19b40ad48d2399ba60",
"type": "github"
},
"original": {
@@ -84,149 +39,44 @@
"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_3",
"nixpkgs-25_05": "nixpkgs-25_05",
"nixpkgs-unstable": "nixpkgs-unstable"
},
"locked": {
"lastModified": 1767721199,
"narHash": "sha256-UzRxDiJlopBGPTjyhCdMP+QdTwXK+l+y45urXCyH69A=",
"owner": "fort-nix",
"repo": "nix-bitcoin",
"rev": "5b532698ce9e8bd79b07d77ab4fc60e1a8408f73",
"type": "github"
},
"original": {
"owner": "fort-nix",
"ref": "release",
"repo": "nix-bitcoin",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1772380631,
"narHash": "sha256-FhW0uxeXjefINP0vUD4yRBB52Us7fXZPk9RiPAopfiY=",
"owner": "NixOS",
"lastModified": 1792155732,
"narHash": "sha256-3Dk56anLQ1V1VklQFKFD6ul3cKQk40sThurHlPHIhOo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "6d3b61b190a899042ce82a5355111976ba76d698",
"rev": "7694c4f6b8537d50062ff36c21ef36de3c9322d7",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "master",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-25_05": {
"locked": {
"lastModified": 1767051569,
"narHash": "sha256-0MnuWoN+n1UYaGBIpqpPs9I9ZHW4kynits4mrnh1Pk4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "40ee5e1944bebdd128f9fbada44faefddfde29bd",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1751274312,
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
"lastModified": 1792030654,
"narHash": "sha256-OaQwAtPMTjQiY2sT/2/62UbSPhzP/0aDnWzMm/NCiUw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
"rev": "26f4107e47e3ad0ffd0db220469ba83b6f6d96e8",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1772380631,
"narHash": "sha256-FhW0uxeXjefINP0vUD4yRBB52Us7fXZPk9RiPAopfiY=",
"lastModified": 1792094642,
"narHash": "sha256-zp8ToDMnqZcu4o4IH9pdwXFXGp7yLm/M0dEnNWnyd7I=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6d3b61b190a899042ce82a5355111976ba76d698",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1767480499,
"narHash": "sha256-8IQQUorUGiSmFaPnLSo2+T+rjHtiNWc+OAzeHck7N48=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "30a3c519afcf3f99e2c6df3b359aec5692054d92",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"rev": "fe13242281a67641c0102cdb1087f0ebe586c7df",
"type": "github"
},
"original": {
@@ -236,13 +86,13 @@
"type": "github"
}
},
"nixpkgs_5": {
"nixpkgs_3": {
"locked": {
"lastModified": 1770380644,
"narHash": "sha256-P7dWMHRUWG5m4G+06jDyThXO7kwSk46C1kgjEWcybkE=",
"lastModified": 1791487292,
"narHash": "sha256-tqM7xDEsaeK1p6VvRygeZ9LkQRpUUkbRT02BHv6hnTM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ae67888ff7ef9dff69b3cf0cc0fbfbcd3a722abe",
"rev": "8edc0c72e3a38faf5434e40d1f19431125fe4b30",
"type": "github"
},
"original": {
@@ -252,78 +102,82 @@
"type": "github"
}
},
"nixvim": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs_5",
"systems": "systems_2"
},
"nixpkgs_4": {
"locked": {
"lastModified": 1774802402,
"narHash": "sha256-L1UJ/zxKTyyaGGmytH6OYlgQ0HGSMhvPkvU+iz4Mkb8=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "cbd8536a05d1aae2593cb5c9ace1010c8c5845cb",
"lastModified": 1791956154,
"narHash": "sha256-jBYGPi4udQ9uo4KQp9m2J0546nJVQitG15gvZSaVVAs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "55bfb4d7437bd4e09ab7cba58a43bd68eb310044",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixvim",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"oldNixpkgs": {
"nixvim": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs_3",
"systems": "systems"
},
"locked": {
"lastModified": 1727619874,
"narHash": "sha256-a4Jcd+vjQAzF675/7B1LN3U2ay22jfDAVA8pOml5J/0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "6710d0dd013f55809648dfb1265b8f85447d30a6",
"lastModified": 1792203519,
"narHash": "sha256-yJ072hxngo/srNdLTp4BlFeo0hedwnKicI3TGlSXLFE=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "453dc9c5037e6d1bec81d2a51ddacd58a2f44e9c",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "6710d0dd013f55809648dfb1265b8f85447d30a6",
"repo": "nixpkgs",
"owner": "nix-community",
"repo": "nixvim",
"type": "github"
}
},
"root": {
"inputs": {
"bip110": "bip110",
"btc-clients": "btc-clients",
"nix-bitcoin": "nix-bitcoin",
"nixpkgs": "nixpkgs_4",
"nixpkgs": "nixpkgs_2",
"nixpkgs-stable": "nixpkgs-stable",
"nixvim": "nixvim"
"nixvim": "nixvim",
"sovran-bitcoin": "sovran-bitcoin"
}
},
"sovran-bitcoin": {
"inputs": {
"nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1792361662,
"narHash": "sha256-EySy/HbufH9trjH0kEuZ2nVVk0+Sn5FoxJI7YtHrspo=",
"owner": "naturallaw777",
"repo": "Sovran_Bitcoin",
"rev": "5c13bf5e01beccc4a225a36cde833f00b2595718",
"type": "github"
},
"original": {
"owner": "naturallaw777",
"repo": "Sovran_Bitcoin",
"type": "github"
}
},
"systems": {
"flake": false,
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"lastModified": 1791244623,
"narHash": "sha256-uuanbkFk8a313Zfrxdcz8Us/yQtaxNnKJtX8v0ZPpd8=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"rev": "d36eeed142f88b5a636f2e3856a53c61654cf29b",
"type": "github"
},
"original": {
"owner": "nix-systems",
"ref": "future-26.11",
"repo": "default",
"type": "github"
}
Executable → Regular
+16 -27
View File
@@ -3,14 +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-24.11";
bip110.url = "github:emmanuelrosa/bitcoin-knots-bip-110-nix";
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05";
sovran-bitcoin.url = "github:naturallaw777/Sovran_Bitcoin";
};
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, bip110, ... }:
outputs = { self, nixpkgs, nixvim, btc-clients, nixpkgs-stable, sovran-bitcoin, ... }:
let
overlay-stable = final: prev: {
@@ -23,29 +22,19 @@
{
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.hostPlatform = "x86_64-linux"; }
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; }
self.nixosModules.Sovran_SystemsOS
/etc/nixos/role-state.nix
/etc/nixos/custom.nix
./hardware-configuration.nix
./role-state.nix
./custom.nix
];
};
nixosConfigurations.sovran-iso-desktop = nixpkgs.lib.nixosSystem {
nixosConfigurations.sovran_systemsos-iso = nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.hostPlatform = "x86_64-linux"; }
({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; })
./iso/desktop.nix
nix-bitcoin.nixosModules.default
nixvim.nixosModules.nixvim
];
};
nixosConfigurations.sovran-iso-server = nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.hostPlatform = "x86_64-linux"; }
({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; })
./iso/server.nix
nix-bitcoin.nixosModules.default
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable ]; }
./iso/common.nix
sovran-bitcoin.nixosModules.default
nixvim.nixosModules.nixvim
];
};
@@ -56,16 +45,16 @@
nixpkgs.overlays = [ overlay-stable ];
})
./configuration.nix
nix-bitcoin.nixosModules.default
sovran-bitcoin.nixosModules.default
./modules/sovran-bitcoin-integration.nix
nixvim.nixosModules.nixvim
];
config = {
environment.systemPackages = with pkgs; [
btc-clients.packages.${pkgs.system}.bisq
btc-clients.packages.${pkgs.system}.bisq2
btc-clients.packages.${pkgs.system}.sparrow
btc-clients.packages.${pkgs.stdenv.hostPlatform.system}.bisq
btc-clients.packages.${pkgs.stdenv.hostPlatform.system}.bisq2
btc-clients.packages.${pkgs.stdenv.hostPlatform.system}.sparrow
];
sovran_systemsOS.packages.bip110 = bip110.packages.${pkgs.system}.bitcoind-knots-bip-110;
};
};
};
+52
View File
@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#153126"/>
<stop offset="55%" stop-color="#0F241B"/>
<stop offset="100%" stop-color="#091C14"/>
</linearGradient>
<linearGradient id="outerArc" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#42F39A"/>
<stop offset="45%" stop-color="#28D978"/>
<stop offset="100%" stop-color="#1AA45D"/>
</linearGradient>
<linearGradient id="innerArc" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#27C86F"/>
<stop offset="100%" stop-color="#157E49"/>
</linearGradient>
<filter id="innerShade" x="-10%" y="-10%" width="120%" height="120%">
<feOffset dx="0" dy="2"/>
<feGaussianBlur stdDeviation="5" result="blur"/>
<feComposite in="blur" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 .18 0"/>
</filter>
</defs>
<rect width="256" height="256" rx="48" ry="48" fill="url(#bg)"/>
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5"
fill="none" stroke="rgba(255,255,255,0.08)"/>
<rect x="6" y="6" width="244" height="244" rx="42" ry="42"
fill="none" filter="url(#innerShade)"/>
<path d="M128 32 A96 96 0 1 1 58 196"
fill="none"
stroke="url(#outerArc)"
stroke-width="12"
stroke-linecap="round"/>
<path d="M128 56 A72 72 0 1 1 76 178"
fill="none"
stroke="url(#innerArc)"
stroke-width="10"
stroke-linecap="round"/>
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 31 KiB

-12
View File
@@ -1,12 +0,0 @@
{ config, pkgs, lib, ... }:
let
theme = pkgs.callPackage ./plymouth-theme.nix {};
in
{
boot.plymouth.enable = true;
boot.plymouth.theme = "sovran";
boot.plymouth.themePackages = [ theme ];
boot.kernelParams = [ "quiet" "splash" ];
boot.initrd.systemd.enable = true;
}
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff. Show more