931 Commits

Author SHA1 Message Date
Sovran Systems 9e09f9eb40 Merge pull request #296 from naturallaw777/copilot/fix-switch-inhibitors-check
Fix switchInhibitors fallback detection race in sovran-hub update/rebuild scripts
2026-05-09 10:07:41 -05:00
copilot-swe-agent[bot] 08bfa73e74 fix: detect switchInhibitors from captured nixos-rebuild output
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/637e0a15-b70f-44b0-abe8-8ba3dd25a359

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
2026-04-03 01:29:21 +00:00
copilot-swe-agent[bot] 12eb68abdf Initial plan 2026-04-03 01:26:13 +00:00
naturallaw777 0670f1248a fix for feature manager 2026-04-02 20:15:57 -05:00
naturallaw777 69c01d605f fix for feature manager 2026-04-02 20:03:13 -05:00
naturallaw777 1090aa056b fix for feature manager 2026-04-02 20:00:27 -05:00
naturallaw777 2378a278f2 reverted to old file 2026-04-02 19:49:51 -05:00
Sovran_Systems 280e7a4132 Merge pull request #8 from naturallaw777/copilot/fix-feature-manager-bugs
[WIP] Fix Feature Manager bugs in Sovran Hub
2026-04-02 18:59:55 -05:00
copilot-swe-agent[bot] 6a0a4e0489 Initial plan 2026-04-02 23:50:44 +00:00
112 changed files with 17909 additions and 3742 deletions
+8 -1
View File
@@ -1,4 +1,11 @@
custom.nix
role-state.nix
*.iso
*.zip
*.pma
__pycache__/
*.pyc
*.pyo
iso/secrets/enroll-token
iso/secrets/provisioner-url
result
-8
View File
@@ -1,8 +0,0 @@
custom.nix
role-state.nix
*.iso
*.zip
*.pma
__pycache__/
*.pyc
*.pyo
+1
View File
@@ -0,0 +1 @@
### Testing Branch
+244 -1
View File
@@ -1 +1,244 @@
<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>
<svg width="256" height="256" version="1.1" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="256" height="256" image-rendering="optimizeQuality" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABmJLR0QA/wD/AP+gvaeTAAAgAElE
QVR4nO2dd3gU1frHv+9sTy9AKh2RIqh0kHJFpVoQO1IURa6CCFak6CrFdr1KU1HUa8NKEREQRQUR
ASlSlR5KEkpIQtr2Ob8/Aj9Isrszuzu7s7s5n+fJo+zOnHmTmfOdU94CcDgcDofD4XA4HA6Hw+Fw
OJyohtQ2gBM4zAytNTWtPkiozxjqChDqMiCVIKYyJtQBiakA6QGYiGAEAMaQAJDmfAsuIpSc/9wK
wALABrCzBJxlEM4SEwtE4AwJKICDHTOeO3mCzHCq9CtzFIILQITAvoLGdjqrqSiiLQmsFYDGYKwR
iBqBIRuANsQmOUE4AcZyQJQDRocZw98Cox2GohOHyAwxxPZw/IALQBjCzK31lpTidgKJnRmoDYAr
AbQCEKOyaXKpAKM9ANtBwC4RtNlkSN9Ko7c61DaMUxUuAGEAm52SYIOxEyN0B3DN+R+TymYpjYMI
O0XgdzBa79SzXxJG5xWobVRthwuACrCvoKnIz7iKNHQ9MdwEoCsAQW27VGAvMXwH4CeDIWMtHyGE
Hi4AIaL4rQbJBtF1M0Q2gAg3MCBZbZvCjEIAq0FshdVh+C55Qk6x2gbVBrgABBE2PzPGZsN1IAxj
wC0A9GrbFCHYGPAjMfraCMtSGldYorZB0QoXAIVh5tZ6S2rxLQLYEAb0Ayq33Th+YyXQChHiQpM+
cxmfJigLFwCFsM7Nag6RjWSE+wHUU9ueaISAIgb6WhCdcwyPndqttj3RABeAAGCzmxmsKL8dJIwC
WE/wv2eoYABbC9B7xrPJ35B5j11tgyIV/sD6AZudkmAjw/0M9CSAbLXtqeWcAvCO3SHMTnz8RKHa
xkQaXAB8wDInrTEjzXhieABArNr2cKpQBtBCIrxuHJu7X21jIgUuADKwzclsIQLPA7gDgEZtezhe
cYHwBYFe5EIgDRcAL1jeSG8EreZZgI1E6H3tOYEhErAIgjDJOObEQbWNCVe4ALjBMjejIUSaAsII
ADq17eEEhAOgD5nApseMyTuutjHhBheAS2CvpcVajZqnADwDvn8fbdgZ4R2TaJ3KHYsuwgUAAGMg
65ysYSD2MoAMte3hBJU8IvaCoSB/AQ9Z5gKAijmZ1xDwJoAOatvCCSVsM2PCYzHjcjeqbYma1FoB
YPObJFoc1heJYSxqZyQeB2Bg9KndSeNrqw9BrRQA69ysGxljb4M78XAqyWdgj8Y8mr9IbUNCTa0S
gLJ5ddM1Lv2rIDZMbVs44QcDlkMr/Dvm4RO5atsSKmqNAFTMzbpTYOwdHofPkaCQMRodMy73G7UN
CQVRLwDslTrx1hjDfwD2kNq2cCIIRp8YNfZHaMyZMrVNCSZRLQAVs7M7EYmfAWimti2cCISQwxiG
xjya97vapgSLqBQAZoZgTc2aDLDnwF14OYHhALHnjWPyXyYCU9sYpYk6AagM1TV+xIBBatvCiSq+
t2m0w5IeOVaktiFKElUCYHsru63oEhcDaKq2LZyo5KAAYbDh0RO71DZEKaLGAaZiduY9okvcAN75
OcGjmQhxk2Vu1gi1DVGKiB8BMDMEW2rmSwx4Wm1bOLUJesk4NndypK8LRLQAsNnNDFaq+BDAPWrb
Eu6cLQdmrBFRZgNeGSggOVKKjIUxBFpkcNAwevyERW1b/CViBaBkblaqjrElAHqobUu4s3Q3g/kH
EcXnH9N6cYTXbyb0aBKxtz+c2Ohk4s3x406eUdsQf4jIJ8A6u35TRq4VAJqrbUs4Y3EAz60S8fWO
mqNUIuCBzgIm9iZoo2YlSDUOkQsDjePz9qltiK9EnACUz81sJ4j4AYQ6atsSzhwrAh76WsQ/p71P
Ua+7jDB3sAATz3sUKKddAvWNG5P7l9qG+EJECUD5vPSOgiisApCiti3hzM48YOSXLhSUyzu+bSbw
v7s1SOHrAgHBgGIQDYgZm/uH2rbIJWIEwDI3oydAy8EQr7Yt4czvRxhGfSWiwscCWk1Tgc+GapDO
/7qBUk4Mg4zj8n5S2xA5RMTszzonvT8YreKd3zt/5TI89LXvnR8ADp0Fhi904Zw1one1woFYRlhu
m5t5i9qGyCHsBcA6J70/g7AUgEltW8KZAwXA8M9FlAdQJGv/GWDsYgaHi4tAgBhEhq+s8zL6qW2I
FGEtABVzMq9hEL4GL6vtlTI78PA3LpRYA2/rt8MMk1dwAVAAPRNpsWVO9r/UNsQbYSsAFbOzuhBh
JXgJLkmeXCbiYIH0cckxQPO60sd9tYNh0U4uAgpgAsTllnmZ3dU2xBNhKQD22VlXErHv+ZxfmqW7
GVb9I91Z4w3Ax/do8PlQDZqkSq/9vrCa4WQJFwEFiIWI5eVzM9upbYg7wk4ArHOzmrvAfgLf6pOk
2AK8uFo6tb1WAObfIaBNBpAaCyy4S0CcxKSqxMrw3A9cABQiUWBYaZ2XHXaJacJKAErmZqUyxpZz
Jx95vP6riMIK6eOevpbQrdHFt36TFOCVG6Vv/ep9DCtljC44sqjHRHFV6ex0GZOw0BE2AsA+bGTU
gS0DcJnatkQCJ84BX2yX7py9mxFGdal5mwe2ItzWVvr2v/ozg7PW189RjKZaCIvZh43CpuxcWAgA
YyBrmf19MHRT25ZIYdY6EQ6JjhmnB2YMEEAepvxTrifJqMAjhQzf7uajAMUgdLeV2T9m5vDoe2Fh
hG1u5ssAhqhtR6RQUA4s3SXdKR/rSchI8Px9cgwwoaf0I/DBZi4ASsKAO6ypWdPVtgMIAwGomJ15
D0/m4Rtf/sUk3/7JMcDQ9tK39552hOxE78fsOcmw7QQXAWVhz1pmZw1X2wpVM+aez+H3npo2RCIi
YxjSzvtWXpeGJCvCTycAD3UV8Nwq74qybA9Du+yICR2JDIi9ZZuVts3w2Kndqpmg1oWL32qQbHA5
/wTP4ac6pTag6ywXyry4EdeLI2x6zPN6AsdvDlid+k7JE3KK1bi4KlMAZoZgcDk/Ae/8YUG8Aejf
0nvPPl3G8LdEbgGOX1xm0Km3KKjKRSuLdmCgGtfmuKdfC+lX+4YjXACCATHcZE3NVGUdLOQDuvNJ
PX4HENE5aDYdQ8RHzXWoTzCeXwWyOYEr/+OC1en5+FuuIMwadPGdYXEAh88yxBsIqbFALA/ZCgQn
I6F7zNgTm0J50ZAuArJ5deOsovAZIrzzA5XRd3K88MKZ9+8ScN1lle8AgxZok0n485hnUfv7VNXv
Sm0MAxdcXDxMiQEapRBapxOuymToUJ/QMJkvGshES0z8jL1S52p6pqA0VBcN6RTAKmrnIUo8/d4c
JCDBGNkP9x9Hq/77qkzvv8/RIoBdogH14qjK36CwAth2guGTLSKeWMbQa56I69524ZWfRew9Fdmj
pRDR1BqjfyOUFwyZAFTMybgNINX3PZWiZxPC1yMEyT30cGZ7tb39xhLhVzYnaox6GiZ779iHzgJv
b2AY8J6IQR+K+GantA9DLeeBijmZd4XqYiERgPI3GmYIoKjb77+8LvD1CA3qJ6ltiX/knqvaebNk
iFlBNQFIjZU/Cvorl+HJZSL+Nc+Fhdt45iFPEPBW2Zv10kJxrZAIgEbrnMuA5FBcK9RkJACfDBFQ
Ly7ypgMF5YDrkrdxSoz072CxV+20SX6EteSeAyatENH3XYb1fGfBHSkajXZWKC4UdAGwzs26kYEN
DvZ11KRRCmHuYIJGdcdq33AxVHHsEUi6M9pcVUVCr/H/+ofPMgxbKGLSChEWPxKZRjl3hSKxaFAf
WTa/SSJj7J1gXiNc6NSAMOaayFIAgwYQLunPLlF6BKAVqopEoPN5xoCF2xhu+cCFAzLSmtUmRIa3
it5oFNQJZlCfWKvd+hqArGBeI5wYcw1FVF79RFPVDn+mXHoEoNdUPcfmxW/AF/afAW77n4gNOXxK
cAmZBo09qFGDQROAijmZ1wB4MFjthyMGLTC2e+SsBbSqtsyULyMHYPX8AWdliIZcSqwMIxaKPAvR
JRDh4Yq52Z2D1X5QBICZIRCjNxBBlYeUYnBbATER4ubUttq+/z+nvd8uIqButRzNueeUtckhAuOW
MPxykIvAeQRi4izGgtOXguIJaE3NHAGwjsFoO9yJ0QG9mhFW/i3/AW6VTljxoPrrBzvzvU/oU2Iq
RzmX4s112F8cLoYxixi+HqFB63Tl249AOlvnZtwL5H+qdMOKP3XslTrxAGYo3W4kcWkCTjmEwzCp
xArsOen9mBb1alq6ebwGK0cJMPcV0LUhVVlUDIQKB/DIIhElvFTZeehl9lqa4jUyFBcAm0k/GUCG
0u1GEm0i8K219hCr4hPgjsvr1fxMIKBlGuG+joTPhwlY/6gGj/ciJClQyO1oEcOUlVwAzpNlNWqe
UbpRRQXAMjejISM8pmSbkchldX17DYbDI75URuLPDjIyAmUmAON6CPhtrAbje1LAEYLL9jD8zp2F
LvBkxeysbCUbVHYNgNFUAGGT8lgtYvWVP3ILdfoiFw4XwwurK9OCeWNibwEJMu/E6TKGtYe8tycQ
0LWhfEvjDcD4ngJubs0wbomI3RLTC2+YfxCx6iFNxDlaBQETkTgZwMNKNaiYAFhn12/K4IqaYJ9A
qRNLKLfLe3P58n7TaQjbc0XsOen9rMvqMtzfUV6H/XgLJHP/t8mQTiHujiaphCUjNXh2eWUgkD8c
KABW72foLyNpSfRDD1jfSnvN+Mipw0q0ppimMojPIwri/JXCl6Gvr491zybSx3y2VYQoo7+V2oCP
t0gfeHNrGYZ5QCcAr94k4JYr/O/Ab2/gIYTn0TFRM1mpxhQRAOvcrOYgdo8SbUULugB85KUY0FL6
th0sAFbI2Iqc/weTXGnXCMBAiZyBUggEvH6zgM4N/Dt/Zx5kVUCuFTCMsL6ZebkSTSkiAIyxF6Fy
ivFwQ6+RP9z1dWDcJgNoJqN64n/Xeg+5zT0HLNgo/Wa9oTkhPSHw4bdWAF6/RVPDl0Auq/fxxcDz
aJgWU5VoKGABsMxJawzgdgVsiSp82Q/3p2vdeaX0WYfPMizwkmHuhdWiLEeeBzsrN/fOTpRXsMQd
aw5wAfh/GO62vpUmYzLonYAFgEHzOIAgDng57rj7apIs8Q1U1hA8Uliz46w9xGS9UTs1qEweqiTD
2vt33p6T0r4KtQgNnNpHA20kIAEofqtBMgH3BWoEx3cSjIS720nfPqsTGLdEhN1V9fPujQmDZCzK
PdNb+b23RikkawpTHasTyClS3JyIhREbde6/2RKJ3LwT0N01uFyPAIgLpA2O/zzSjRBvkD5uVz4w
48eqr06NAPznZgEDvCzu9WtBaB+kcmBSCUg94W40U4uJ1enFUYE04LcAsNnNDAAbE8jFOYGREgOM
6iKvI320pTJb76VoBWDWIHJbFShOD5j7Bs/zxt88iiUWZe2IdIhhHDO39tvf0u87bEX57ajlPv/h
wOiuAhqnyBOBF1Yz/FRtIU2nIcy9VcDIagt9T/UWgprcxF8X4RKbsnZEAZmWlKLb/D3Zf4knesjv
czmKYdAC0/qTrKKdThEYs0jEusNVRUAjAM/dIODlgQJ0GkKvpoTh7YPrdeetEKk3qqck4wBE/ife
8UsArHOzmgPo4e9FOcrSvTFhqES58AvYnMDor0W3W2p3X034eAjh9ZuDXwX4lJ+1b5JN3B3YDdda
Z2X7VXDHvxEAY6MQHmHsnPNMvkHAZTJX1i2OShH4fHtNEejakFBH8ajzmuT7KQCJCoQZRyHEBHGk
Pyf6LADM3FrPAB70E2YYtcD8OzWyIwCdIvDs9yJe/lmERGCh4jhcDFuP+3fRFC4AnrifzW/vcyyO
zwJgSS2+BYCb1BActWmSAswbLEDrw119ZwPDk9+xoKT28sS2XEKpH4t5eg3QzMdcC7WINIsj70Zf
T/J9CsDYvT6fwwkZPZoQpvbx7bYu2inixgWhy8v/zQ7/3PmuygqsEEm0IwBD/DhHPmx2SgIR+vp6
EU5oGdGBMMzHVfyDBcDgD10+JTP1h7wSYOku/67RqQHPCOINxmgAm1fXJ8c8n/6iVpgGgWf8iQhe
6Cfgtra+dZhSG/DIYhFTVzFUBKlU18yfRL+rCfVvoawtUUiMxaW7yZcTfHtCiN3p0/GckHGqlFXZ
WxcIePVGwq1tfMxPyIBPtojoO9+FzceUHQ0s2cWwfK9/bXZuALRO5/N/KQSCT31UtgCcr1F2g88W
cYKOUwTGLmEY9aVYpVSXRgBeu0nAwFa+d5zjxcCQT0X851cWcP0/oDLl+HOr/G/ovo58+C8HBvRn
85vIKPReiey/qlHnuAVAgDleOcHgP7+K+PMYwx9HGR5dIlbJ71fp7+9fOi6nCMxdL+KmBYEl9Txc
CAz/3OXXyj9QGZLcl+cDlIvBarPI3g2QL6siG+CXOZygsisfeG/jxWH16n0MTy8Xq8TNawXgzVsE
PCQzcKg6/5xmGPSBCy//XDOsWIqdecCdH7lwttyvS0OvAWYO0ChWcKRWIFB/2YfKOYh9BQ0I1/tv
EScYuETgme/FGkkyFu9keGypWCUdGBEw6XoBz/Xxz83XKVb6DNy4wIWdefLOWb2P4e5PXCjws/MD
lTb7kzugViOiLzPL69uyDrKcyuwCIKDEAxzlWbKbYa+H9ODL9zI89HVNB5+RnQiv3lgZ9OMP+88A
g//nwuZjno9xuBjeXMfw72/EgHYThneorDjE8RFCnYq66bLyLskSACLWLzCLOErjEIE31npfVPvl
IMOIha4akXd3XElYeC8hxY88/ykxwFu3CejkIbvv4bMMgz5keHOdvLTknri1DeGFIOYjiHYEJsia
BsgTAMifU3BCw/I9TFZp7rwSuPX179iAsOR+34bXPZsQVo4S0Ody92/lRTtF3PS+dNESKe7vGJqI
xKiGQdZLW1IASuZmpTKGqwO3iKMkH8ko5nFhB8BT2rCGyYTF92nQvbH3nmbSAdP6C/joHgFp8TWP
LawAHv5GxBPLmOxyaO7QCMBzfQQ831fgi36B00nOdqCkAOhF1k3OcZzQcbgQ+CtXWgDGdBfQTiKn
X4IR+OgewWMZsaapwOL7BAxr7z7pyJoDDP3eFbHyn8De+unxwOdDNRjZifd8hdDYnJauUgfJKdFw
jQLGcBRk2W5ph5r0BMLD3eR1Jo0APN9XQKNUhmmrL/oRDG5LmNFfgMlNkGmxBZi6SsR3ewL3Fuzb
onJhMpE7mSsKY7gGwCpvx0gKAOMCEHb8clD6mPE9CEYfK/CM6EBokiJgzGIRIzsJGN/TvYCsOcAw
8XsRZ8p8a786KTGViUdvbs3f+sGBJPuu10eEmVvrrVTkZxkHTjAos0NykS0rEbhdRuUgd/RoQvhj
nMZt0k67C3jpJxH/28ICSiJCBAy6gvDcDYJfFYc5MmHowsyt9WTe43FlxqsAWOoUtycGnoMljNhz
UrqU96AryKekINVx1/kPFwKPLg58hb9JSmWkYo8m/K0fAkwVdc9eCeBPTwd4FQCBiZ0YT/0XVuw/
I90Bb7lC2TXbRTtFPLcqsBX+GB3wUFcBY7oTdHxJOWRoXEJn+CsAjKitz6VrOUElp9D79xkJQPO6
ylyrzA5MXiHi293+PwREwOA2Aib2JviWqoKjBIzQ1tv33peJmPeTOaGnsMJ7Z2ydpsyI7Xgx8OCX
Luw7438brdIIL/QldGzg3iabExAZ3O4ycBSjjbcvPQ7G2FfQAGiluDmcgCixev++mQJv/y3HGQZ9
6H/nN+mA8T0FfPuA4LHzrz/CMOA9F/rMd2H9ET7MDCJtvAUGeRwB2PIym0EDvkYbZkj51/tbcusC
S3YxPP2d/2m7+rUgmPt6Lit2pJBh2mqGnw9e/EWGLRRxaxvC8324L0AQiLWl1G8MHD/k7kuPAiBq
qC3xBYCwQ8pFVgjAgf6TrQzPr/IviCc1FpjeX0B/D4k7yuzA7HUiPtxcM8MQY5UhzBtyRLxxC6Fr
Q77wrCQucrUB4FYAPA4NiFjLoFnE8RupyjgVdv9E+50NDFNX+tf5B7Qk/DRa47Hz//APw3Vvi3h3
o/f0YidLGIZ+JuLNdaxGjgOO/wiMeZzKe1sEbBwEWzgBUieWAC8jM39y+/93LcPs33zvcYlG4OWB
gtvy4gBwpqwyD6AvcQIuEXhznYiNRwmzbyXUi+OjgcAhj33Z844s83wSRz0aJnvvTP+c9u0V/sFm
/zr/1VmE5Q9qPHb+FX8z9HnX5XeQ0MajDDe97znhCccHCI08feXFJYN5PImjHs3reH8jHiuSPwr4
dKuIaT/61vk1AjC+J+GbEQLqJ7k/5tnvRTyySERRhU9N1+BUKcPdn1YmO+UEhG8jAGaGFkBW0Mzh
+M0VGSRZHmuZDMedVf8wPLfKN5/+eAPw3p0CxvcUoPHy6njqWgEd68tv1xslVobhnwUeblzLaXB+
W78Gbm+jNTWtPuSFCnNCjElXWSPPG9/srFofoDp7TzE8/q1vC34NkwmL7tOgdzPpOXlKDPD5MA1G
dFBm/u4QgXFLGNYe4iLgJzprfqbbp8a9jjPykPGNEw5c39y7M31+CfDxFvdD+8IKYNRXviXr7NqQ
sOwBwScXY61QGfQzc4D/CUgvxeGqTDK67QQXAb8g1tDdx+6nAKTh5b/DmBtbuc/OcynzfmduvQbH
LBJl5RK8QJ/LCR8N8d9BZ0g7wgd3EeIUKCljcQCjvhZxsjTwtmobDHAr324FQABSg2sOJxAyEyoT
dHqj2OK+FNewDuR1/n4pN7cmzBssveYgRY8mhC+Ga/zKQlyds+XAE8vEgPIR1EbIQ592PwJg4KUY
whxPOfwuZeluhmXVUnYNaEl45UbppJv3tiO8OUiZ4TsAXJEOfDVcg9TYwNv6/QjDp1u5p5BPCO77
tFsB8KQWnPChV1PClZnSnXPichF/n6oqAre3JTzf13Pa7XvbEab3Vz4zb7M6wMf3aDxmKfaFl9Yw
nPBhKlPbIZAPIwAuAGHPhVJfUlQ4gAe+FJFfUvXzER0I0/pRjU5+Q3PCC/2Cl5O/dTow/w4hoIxF
QOXv9aZEYRTORTz1afe3gRgvAxYBdG5QWUFHirwS4J5PxRo1+oa2F6oM86/OIsy+NfDOKUW3RoQJ
vQK/yJLdDMeLFTCoFuCjAPAw4Ejh+T6CrEw7OYUMIz53ocRadTpwc2vC/DsIbTIq6wOEKjnHw90I
XQKM+nOJ4GsBMvGU29ODHwAU2LThhIIkEzB3sEbWYt2ek8B9X7AaItC7GeHbkRokhDAWXyDgxX6B
rzMs3sUCqkFYi3C78uJpHKbAMg0nVHRuADzXR15P2naC4Z5PWQ0/fTVKcTWvC9wUYE2AM2XAjjyu
ANIw+QLA+Agg4hjWnmS73u45ydyuCajBv7sGrjwbjypgSJRDILd92v02IHEBiETMfQXcfbW8DvXP
aYY7P3Kp7lXXMo2QLVnC0jvbZdRJrO0w36YA7tWCE94QATP6CxjgIUa/OocLgbs+9s01OBhcKyPA
yBuHCrgASEM+rQFwIhSNAMwaRBjYSl6nOlrEcMdHLuQUqteJrs4KTAByz4G7BvuJBwFgAdSA4aiN
TkOYPUjAbW3lday8EuDuT9QbCWQGOAWwOit/ON5gNnefeloE5AIQQZRYWQ13X40AvHaTgDtkFgk9
WQoM/UydhUFfqxi7w+JDeHNthAD5AkDEBSCSmL0euOsThs3HqoqAQMCrNwoYLnN34Eghw/CFIs5Z
QzueVuLtLTfCsbbCPIzqPf3Z3KoFJ/w4WsTw8Z8iSqyVKbVXVUudRQS80FfAsPbyRGDvKYbxS0Pr
XFMYYO5AAIjR8UUA75D8EQD4CCBieGMtg91V+f92FzB2sYilu2uKwIv95E8HfjnIMOu30HWoE8WB
XcuohWJhy1GMDwLAoIAmc4LN8WJg+d6qnccpVibMWLSzpgi8PFDAjTJ3B+b8JtaYUgSLvacCOz/R
xDu/FIxgcfe5p12As8E0hqMM720U4XQTC+MSgae+E/HNzpoLg28MEmQF4YisMpeAvzUC5cIY8Ofx
wISmMY9dlYQY3CaL95QQhAtAmGNxAEt3e/5eZMDT34n45WDVzqUTgHdu95zT/1IOFwL/2xxcBdh7
igW8/XhZXT4CkMJTn/aQEETgAhDmrPy7ZlRfdfQaoEW9mp0jyQS8c7tG1vbbnPUM5UFcEfpie+DT
jGY8gZ0kzBcBICb6UWGOE0qqL/S548EuAjIS3H/XOh0Y31N676zECny1IzhrAQXlqDFN8YfOPIm9
NOTDFED0cDAnPCi3V9bO84ZRCzzY2fvQ+IHOkJXr/7MgJd2Y9qMYsANPejxwOZ8CSCL6tAbAGBeA
AAnm+vm6wxe3/jwxqA0hSaKUuE5DmNZfOuf3wQJg/xkfDJTBsj0M38oYxUhxbTPpGgkcQGDuF/Y9
5QQ8FlRragE2Z/Ceyg050sfcIzMsuHMDeUPonw8oJ2lbjjM8s1yZUcVgGTkROQDTUI67z90KgPHs
qeMAeHhFAHirzRcom4967zzZiUDbDPkdY2h76bWAbQrF3K87zDDi88CH/kBlLoGODbgAyMBhqpuX
5+4Lt+vAZIbTMhcnwDzXFed4p9QWnEnAOSuTLP/dr6Vvab17X1YZPnwhpNakAwyXPBkJRiBFYjoh
hcPF8PaGSg9Dl0JLCnJjHDg4RnfC7aTR80YQYzkANQqKOVGOSwROlxGCsRKw7zQk/fS7ui0D6ZlY
PTBvcPCiaX49BMz8SVR0HaFZHeD2tsq1F+Uc8fSFZwEgygnqSlYUc7qs8o0XDPad9t6uQECH+kG5
tE+UWBlW7QM++lPEnpPKtz/lBg10AdYsrEX4IQBeTuJ4Z1d+8JQzp8j7yKJBMiHRGPqhcYmVYf8Z
YNdJYO1BERuPBi9Jx4CWhH81DU7bUQljOZ6+8igAjLE9BD7H8gdfk1TanQzHitx/VycOiLmkWMep
Uu9tV9/XL7MDf8m0p9TqfdIisso1iBIrnf8vkF8C7D8TuDuvXBokAy8P5A8P8RsAABVFSURBVM+l
LwgC7fH0nUcBEEjYxXiiNb9Yf8S3v9uBAqDnPPcb+5/eK6B744sP/Jky7203Sq767x25lXkClEWd
58KoBebeGtoCJtGAyFw7PX3nceXHUJB7EEAYZI6PLI4WMezKV669rGr58k6VeX/7pcVX/Xduifvj
Ig2dALx1u4C2mWpbEmEQSo1jT+V4+tqjAJAZIsA8Dh047vlmh3JtEQGZCVU7vNQIoE5s1ePzoqCE
tk4A5gwW0DvA9OG1EobdRJ6HbBJ7P7RLaXuimXNWho+2KDfcNmqr7sdbHJCMzKte3FMqYjDcSTIB
nw7VoF8L3vn9gpjH4T/gfRcAxLCT8b+7bN7eULkwphTVt7nkeBdWPyeSl3Fa1CO8czuhEU/44Tck
kpesERICIArCJmK8/LIcduYBCzYq29sM1e6OXYZvQZDcD0KKVgAe7EJ4vJcAPd/rDwgXsY3evvcq
ACZd2jarPb8CQIyiVkUZpTZg/FKX2/RcgWCt5i9v1EoPx2zVzok1BMcjMVh0a0SYfD2hdTofeipA
eczZ/L+8HeB1DYBGb3UA2KKoSVGGQwTGLBJxuFD5tsvsqOI3HyOjYmORpWpnb5Ds4cAw48pMwsdD
BCwcKvDOrxQMG8nsPahPRlIo+h1gPZWyKZq4kIZ73eHgvGEZqxxdXIjr1wqVgTne1hnyq237ycn9
pxY6AbjhcsKQdlTF14GjGL9LHSApAMTwO18IrMk5K/DQVy5sCnLmhOPFqJLYo34SYc9Jz4JzrLjq
v69IJ+gEBD27r1wMWqB7Y8J1lxH6tSCk8Mll0CAlBMDmpD/0OiaCVxKuwj2fiAHns5dDTiFDm0ti
+xsmw2twTXUnpHgD0LEBYUOOOusA6fGVcfvts4FODSodeZSoBciRxGUwGDdJHSR5KxIfP1FomZO5
DUAHRcyKEro1JlyVFfzrJFaLw2+TQVjxt+fOfLSIobACVd6swzpcTA/mFIEKBbP8mnSVW48mHaFO
LENaHCE1FmiUAjRJJcS7rUrPCTqETTT6sKQbmFwtXgkuAFWYcj0BKgRLtc3w/j1jwK8HGQZfUhq8
fwtC/5A40vC5YtjAaJWcw2QN65kIWY1xgk+H+iQ5hP5Rwfx9nMhEhLhSznGyBMCUkbcJvFpQWGDQ
Ap0lSnv9coDhnIIeiZwIg6Eg5mz+NjmHypoC0J1wWebgJwB3BWQYRxEGtiKsPeT5LW91At/sEPFA
Z3nrtkUV0lGDBMb35yMFolWVwXzSyF+PJfY9GHEBCAP6Xg5MXek9NuDjLcCIjpW+A1I8+Z2INRLT
hh5NCJ8M4QIQCTDGVsg9VvbWnlG0fQuADyzDgEQj4ebW3jvj0SJWo0S4O/48xiQ7PwAMkVlngKM6
VhOs38s9WLYA0LjCEkb40T+bOEpzX0fpijiz1omo8JJ/3yUCL6yWHimmxROub+6jgRxVINAKGlco
Ow2MT849JLIvfTeJEwxapxOuv8y7AuSVVIqAJz7awrBbRsbesd0JOg0fAUQCIsGnPuqTABgtjmUA
LD5ZxAkaj/ciaCTu4PubGHa6qQlzpJDhv79Kv/2zE4G7+fA/UqgwWZyyh/+AryOAZwpKCSRrf5ET
fFqmkeRKv1MExix2VQkgqgxiYiiT4RE46XoBOu4EHil8R0+d8imPp8+3VoS40NdzOMFjQk9Cw2Tv
b+jjxcC4pSIcLgbGgInfi14Dii7QrwVhQEv+9o8UBMLnPp/j6wkmfeYyAEGo9cLxB5MOeGmg9ILg
rwcZnl7OMHMNw2IZuwOpscC0fvzVH0Gc1OsyZG//XcDnO0yjtzqI8Imv53GCR7dGhPs7Sb+pl+xi
eG+j9LxfpyHMGyygbpwS1nFCAtEH5xP4+IR/Ei9q5iOS8kzVAiZfp1za7MnXE7pIuBtzwgpGRB/6
c6JfAmAcd/wQQOv8OZcTHDTnc+e3Sgus447qIuC+jrzzRxQMPxvHnDjoz6kBTPLYu/6fywkGsXrg
/bsEZCT4d/6DXQRMvp53/kiDCVjg77l+C4BRn/E1gBP+ns8JDhkJwNL7BbT0YSRABIzvKZzPccCJ
MHJNBcmL/T3ZbwE4v+Awz9/zOcEjLZ7wxTAB7bKlO7ROAP5zE2F8T975IxHGMIvMe/zO8RTQPo/V
qX8HQFkgbXCCQ6IR+OxeAX0u99yxsxOBRfdpcFtbvt0XkRBKbS79e4E0EdCdT56QU8xA7wfSBid4
mHTA/NsFTO9PNWoG9m9JWDFKw6vtRjDEMD95Qk6x9JFe2gjUCMsb6Y2gFQ7Al9wCnJBzvBiY8K2I
/WcYJvYWMKQdH/JHOA6IaGZ6LC+gxPSKPAWWOZmfARiiRFuc4OFwMVQ4CIlGtS3hBA772PRo/ohA
W1Fk8keiYAa8lyDiqI9Owzt/lOAiEmYo0ZBi40DHn7M3CGlXdVWqPQ6H4x7x9F+/6TqMU6Rcn2Lz
9lMlSVPSr75mjUYqQJ3D4fiNyyWi8NDBF5VqT9GVoPffX5dzXGzZUMk2ORzORRpo9h4eObJXU6Xa
U3Tl3pKz/q4/zqZvtDNeD4rDURq9YEVayobblWxT0fH62GmTNnWM37BeyTY5HE4lneI2/PrwtGe3
K9mm4hP2VqacW1N1BXxHgMNRkBRNgesy01HF63IoLgBDzOaCDrF/cO9ADkdBOsZveGu42Xxa6XaD
smRvNRQ80thwkMcIcDgK0Nh4sNRqODs+GG0HRQDMZrPYJmbrBJJXnozD4XiAIOLKmC1jzGZzUDpT
UB3Cn35q4e4/y65pHcxrcDjRTKf433a88urQq4LVflC9dlon7umboilwBfMaHE60kqorcDbR7u0f
zGsEVQBGTJmR2y1h3fRgXoPDiVa6xq1/ZfTMmfnBvEZIYkLHPbnk+K7ydtmhuBaHEw1cGbc1583X
BjcO9nVC4rjfIm7HwFhNKU8jrhAxRgHxMRd/BD/volLtAJAsTMKRT4K2RGwRu3NgKK4Vstv22qQ3
Xl5RNPiZUF0vWkmME/DF9CwY9Bdv3eiXTuLAcd/SwtVJ0mDhi5nQai+2M9ychxOn5flwEQHXto/B
dR1j0bKRHolxGjhdDGeKXNi814Jl68qQk+9znQoOgP7JS994euZjj4fiWiEL3Xtq5oSJ7eI37Q/V
9aKV4QMSq3R+fxl5U1KVzu8LSfEazH4iDVNG1kHXNiYkxWtABOi0hMy6WgzqFY93J6VjaP/EgO2s
bXSI++PvUHV+IIQCAABXxuztkabP8zuDaW2nW1sTBvWKD7id3h1i0LdLrF/nxhgFvP5YPbRu4j3g
S6shjLwpEcMHcBGQS5ou39E86dC1obxmSAVguNl8ukvs2ge0xEMFfKVrGxOmjqwT8Fy7V7sYPDM8
1e92hg9IQONMnfSB5xk2IBFNsuQfX1vRkhNdY38bPWry5FOhvG7Is3eMnznx024Jv6wO9XUjFa2W
8OAtSZg2um5AQ3+DjvDI7cl47oE60Pk59E9L0WLwtVVHIIwB7y4pxh3P5uLB6fnYvs9a5XuNAIy+
Ndlvu2sLPRJ/Xv7YS0/5Vd8vEFRJ30PGE/3bxPyVq8a1I4n2LYx4Z2I6hvRN8HuFngjo1NqE+c+m
4/be8QGNIK5tHwOtpmoD3/1Whi9+LMHZcy4cznPg+XcLUFpe1Wu1fQsjkhM0/l84ymlt2nFSNOTe
osa1VREAs9kstjZs6pihz7Wpcf1wx6gnzH82Ha+Nq4cmPgy3qxMfK2DB5Ay8PKYuGqQHPgzv1S6m
xmcr/6ga81VmEfHrtooqnwkC0OMqU8DXj0bS9Hn2dqadnYLl6y+Fagn8Rs+cmd8tZs1gk1DB/QOq
odcRLquvr/H5vqN2n7b7YgyC2/n67kM2n7fodFpCs2o2uUTgUG7NdvYcrqnrLRvxLFHVMQoW1j1x
zZ0jZ0w5rpYNqmbwHPvS1BXXJq56lYhrgDcYA5avL8P4/55Ccan/LwrGgMW/lOKJWadRWuFbO/Xr
aVE932t+gRNOZ817d+JMzUXeRhl8IfBSiBiuTfzhzbEvTvlWTTtUr+bz1MwJE6c8ndLz99J/8ZTi
bjhw3I53lxRj6z9W6YO9sPeIDfOXFGPXQf9mXfXdTCEKit3v5hQU1fy8fprqj1pY0S3+1/Wh3O/3
RFjcFW3Mke5Xi7EHt5d3DLrvcyTAGPD7TguWry/D5j0WMD8HSC6RYe32CixfX4atfwcmIAmxNQeL
Nrt7w+yOmp/HGAVoteR2xFDbuDJ22zGtKaeX2nYAKk8BLmA2m8UuFbvbXmb6p0htW8KB0goRU985
g027/e/8AFBQ7MIL7xUE3PkBwGRwIwBuOjoA2D24eZgMPGCgqWn/uQ5xO9qotehXnbAQAAC48y1z
WbuYzZ3TuadgWGJ044Pg7k0PAHYPb3l3IlKbqKc76eiU8GenoWZzidq2XCCs7si/p089cE3izwMS
tOfCQh05fuJh1CLU4gFAkrZIvC5xzQ0PmSeFVTxMWAkAAIx9cfKaPokr7+Lhw+GFu+G+J49Cvc79
5xXW2qnrMUI565mwcuhD0yatVduW6oSdAADAmOnPfNMn+YcHTYKFi0CYYLXV7LyeOrqnzy222nc7
TUIF65P63f0TZjz7udq2uCMsBQAAxk174oO+ScsfMZBFbVM4AMrcaLHehxGAzcHgqGU7AHqy4bqk
5Y8/9uIzH6ltiyfCVgAA4LEZT75zffKKqTx6UH3yztT0+EtJdO/fX8fN58dO1q7kIDpyoG/y8slP
zHjqTbVt8UZYCwAAPDnj8el9k7+dqiceNqAmx0/VFOHMulq3wUXZ9Wq6lxytRdmB9IIV/ZKXTX58
xuMz1bZFirAXAKBSBAakfDeWxw2oR5lFRH61ko8GHbl18XUX/7//WO3Y3TUKFtY38bsnI6HzAxEi
AADw2PQn5vVJ/HZ4nFDCRUAlNuysuR7Tr2tclX9rtYR/ta+abYgxYN1f0b+WEyuUsT6Jyx56fOaT
r6tti1wiRgCAymQi/ZOW38X9BNThl60VNT4bfG08eneIAVFl+PHTQ1NQL7nqGsDuwzacLozudZwk
bZHYJ3n57RNmPr1AbVt8ISxiAXzhkRnPfv3uVHb615J/rc63Z9WMmeUEjb1HbFi3vQI9r76YF0Aj
AFNG1sHTwxh0WqqxJsAYsGBpcYgtDS1punxH97g1A8dOn/yj2rb4SkSNAC7w0LRJa3vp17RqZtwX
3U9WGDLv6yK3b3O9rmbnByrDmHcdit4F3MbGg6W9U36+YuzMyOv8QIQKAACMfm3qoV6JGxpeFbcl
R21bahNnil14fNZpHMmTXtX//vcyzPqyMARWqUOb2G0neiT+1iDc3Ht9IaITtS3+9Vdbvz6t5mQL
5/octzeqr7Y9oaBxhg52J0N+gfP/f9bvqEBJuW/LIk2z9LDYqrazblsFKqzSa6ylFSJW/F6Oc+Ui
EuM0qJN08TGyOxg277Vi9ldFWPRzaUDRjOEKEcM1Cb+uj43dd+UEszmiVzejJjzjtSmzZv5S1Gei
RYyJmt8pUtBpCcnxApwuoKjUFZWd/gJGwcKuTfzhzXBI5qEEUdVZ5k2Z1u/30t5L8+3ZPAEdR3HS
9Hn2nvG/3PXI9ElL1bZFKaJKAABg/qRJGXtsnf/cVXFVltq2cKKHVqYdp9qbdnZUM4FnMIjYRUBP
jJ45Mz8lfnuDXgmrf+AxBJxA0ZIT1yatXl43YVtmtHV+IApHAJcya/Irw/4o67nglD2T+wtwfCZd
n2fvHLv2gfEzJ36qti3BIqoFAAAWmM0pByqa/by5tMeVatvCiRyuivvzcKukfd1CXasv1ES9AFzg
1WffeG1d6Q1PlLvia83vzPGdBG2JeE3CmlefnjH+WbVtCQW1qjO8Pcl8xX7HFd/+Vdaxidq2cMKP
NrHbTrSK29H332bzXrVtCRW1SgAu8PqkN8x/lPWcfNZRJ+JiITjKk6w56+qasPY/T82cMFFtW0JN
rRQAAPjYbK63v6LptxtKu3dh0bcZwpEBEUP72I1/N9PtvG70zJn5atujBrVWAC4we+rrI/8qvWrW
EVuzOOmjOdFCE+OBkrYxW8c+NuOZT9S2RU0iOhZACVauW739ln5NXmmlP5pQ5ErtaBHd1MDiRA3J
2gJXz8Q1C2Jj/+kxcdqUHWrboza1fgRwKQvN5jr7rdkfbyrt3s8qmvjfJorQC1Z0jN28sWHisUHR
vrXnC/whd8O8qS91zLE0/N9fFZ1bORlfJ4xktOTE1bGbdjcyHr3vkWnPblXbnnCDC4AX3ps8vct+
e7MPtpd1bOmKvORJtRotOXFF7PbDzXT7Hhwzc/IvatsTrnABkMHcSTNuOORo8e6usqsacSEIb7Tk
RNuY7Yea6PeN4h1fGi4APjBv8vTu+Y6Gr20vb9+5Qozlf7swwiRYWJvYLTub6XL+PWrGlI1q2xMp
8IfYDz6aPjnraFn9OTvLO9x81lm31u+kqEmStkhsE7vtt7ra3PsfnT71iNr2RBpcAALgq0fMcUcS
U186ZGt+70Hr5cmM8T9nKCBiaGb8p6iJ4cCnyYazE0ebzTXzlXNkwZ9YhZj73IzrTtszpu4tb9ud
jwqCQ5K2SGxt2rU5Q3tsEp/fKwMXAIWZbzbHnHMkTc21NRr6T0WrbDvj2ckCQS9Y0cK093i28din
dbRFL95vNlvVtima4AIQRBaYzSnFjqQJufbsIX+XX9HExkxqmxQR6MiO5qZ/TmUZjn6bgjxzbfXT
DwVcAELER9MnZ+VXpD170pE9MMfSrME5VyJ3Ob6ERM05sYlx/9E0Xf73SZQ/k3f60MAFQAV+MZu1
O+zGu4vFOkPy7VmdD1ubp9Q2j0OBXKivP1aRrT++M8Vw6pOrNWXvXms28ySOIYYLQBjw3owZaaVW
w20ljqQbCxxp7Y5Ym9aLNj8DvWBFtv54SYYuf2+S7uxP8fbS/41+beohte2q7UTVQxYtfGg2G0ts
sfeUs/j+hc6kNoXOutn5jqzYSAlQMgoWlqHLLU/VnTmerCneHas/932CYPmSL+CFHxHxQHEAs9ks
1LXrelUgpm+5K75duRjfoMSZWLfYlRxf6KyjC/UUQktOpGgLHEnawtIETcmZWKH0WIy2bGuSULEq
T2P/zWw28xLuEQAXgCjgK7NZf86hbVdBpvZ2UahvF/UZdhjS7aIh1S4akmzMFOdgWp2LaTROptcB
gJUZtIxpCACIXMxINicA6MhuF8gl6sjpMJClzKCxFenIVqiH7aResOfrBfF4DLNsTdQ5t91pNtvV
/L05HA6Hw+FwOBwOh8PhcDgcDocjwf8BnHZ+mE3sgIkAAAAASUVORK5CYII=
"/>
</svg>

Before

Width:  |  Height:  |  Size: 362 B

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

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,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
"/>
</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

+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 -1
View File
@@ -1 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect x="6" y="12" width="52" height="34" rx="4" fill="#3584E4" stroke="#fff" stroke-width="2"/><rect x="12" y="18" width="40" height="22" rx="2" fill="#1A1A2E"/><rect x="26" y="46" width="12" height="4" rx="1" fill="#aaa"/><rect x="20" y="50" width="24" height="3" rx="1.5" fill="#888"/></svg>
<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>

Before

Width:  |  Height:  |  Size: 354 B

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
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
25fimfNdFFFf3gfxCf/Z
"/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+321
View File
@@ -0,0 +1,321 @@
#!/usr/bin/env bash
# ── Sovran Hub External Backup Script ────────────────────────────
# Backs up Sovran_SystemsOS data to an external USB hard drive.
# Designed for the Hub web UI (no GUI dependencies).
#
# Your Sovran Pro already backs up your data automatically to its
# internal second drive (BTCEcoandBackup at /run/media/Second_Drive).
# This script creates an additional copy on an external USB drive —
# storing your data in a third location for maximum protection.
#
# Usage:
# BACKUP_TARGET=/run/media/<user>/<drive> bash sovran-hub-backup.sh
# (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"
MIN_FREE_GB=10
HUB_CONFIG_JSON="/var/lib/sovran-hub/config.json"
ROLE_STATE_NIX="/etc/nixos/role-state.nix"
# ── Internal drive labels/paths to NEVER use as backup targets ───
INTERNAL_LABELS=("BTCEcoandBackup" "sovran_systemsos")
INTERNAL_MOUNTS=("/run/media/Second_Drive" "/boot/efi" "/")
# ── Logging helpers ──────────────────────────────────────────────
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" | tee -a "$BACKUP_LOG"
}
set_status() {
echo "$1" > "$BACKUP_STATUS"
}
fail() {
log "ERROR: $*"
set_status "FAILED"
exit 1
}
# ── Check whether a mount point is an internal drive ────────────
is_internal() {
local mnt="$1"
# Reject known internal mount points and their subdirectories
for internal in "${INTERNAL_MOUNTS[@]}"; do
if [[ "$mnt" == "$internal" || "$mnt" == "${internal}/"* ]]; then
return 0
fi
done
return 1
}
# ── Use lsblk to find the first genuine external USB drive ───────
find_external_drive() {
local target=""
# lsblk JSON output: NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE
if command -v lsblk &>/dev/null; then
while IFS=$'\t' read -r dev_type hotplug removable label mountpoint; do
# Must be a partition or disk, and be removable/hotplug
[[ "$dev_type" == "part" || "$dev_type" == "disk" ]] || continue
[[ "$hotplug" == "1" || "$removable" == "1" ]] || continue
[[ -n "$mountpoint" ]] || continue
# Filter out internal labels
local skip=0
for lbl in "${INTERNAL_LABELS[@]}"; do
[[ "$label" == "$lbl" ]] && skip=1 && break
done
[[ "$skip" -eq 1 ]] && continue
# Filter out internal mount points
is_internal "$mountpoint" && continue
if mountpoint -q "$mountpoint" 2>/dev/null; then
target="$mountpoint"
break
fi
done < <(lsblk -J -o NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE 2>/dev/null | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
def flatten(devs):
for d in devs:
yield d
for c in d.get('children', []):
yield from flatten([c])
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)
fi
# Fallback: walk /run/media/ if lsblk produced nothing
if [[ -z "$target" && -d "$MEDIA_ROOT" ]]; then
while IFS= read -r -d '' mnt; do
is_internal "$mnt" && continue
# Check label via lsblk on the device backing this mount
local dev
dev=$(findmnt -n -o SOURCE "$mnt" 2>/dev/null || true)
if [[ -n "$dev" ]]; then
local lbl
lbl=$(lsblk -n -o LABEL "$dev" 2>/dev/null || true)
local skip=0
for internal_lbl in "${INTERNAL_LABELS[@]}"; do
[[ "$lbl" == "$internal_lbl" ]] && skip=1 && break
done
[[ "$skip" -eq 1 ]] && continue
fi
if mountpoint -q "$mnt" 2>/dev/null; then
target="$mnt"
break
fi
done < <(find "$MEDIA_ROOT" -mindepth 2 -maxdepth 2 -type d -print0 2>/dev/null)
fi
echo "$target"
}
# ── Detect the configured system role ───────────────────────────
#
# Priority:
# 1. Hub config JSON (/var/lib/sovran-hub/config.json) — "role" key
# 2. role-state.nix (/etc/nixos/role-state.nix) — grep for true flag
# 3. Default: server_plus_desktop
detect_role() {
local role="server_plus_desktop"
# 1. Try the Hub config JSON
if [[ -f "$HUB_CONFIG_JSON" ]] && command -v python3 &>/dev/null; then
local r
r=$(python3 -c \
"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
# 2. Fall back to parsing role-state.nix
if [[ -f "$ROLE_STATE_NIX" ]]; then
if grep -q 'roles\.desktop = lib\.mkDefault true' "$ROLE_STATE_NIX" 2>/dev/null; then
role="desktop"
elif grep -q 'roles\.node = lib\.mkDefault true' "$ROLE_STATE_NIX" 2>/dev/null; then
role="node"
fi
fi
echo "$role"
}
# ── Initialise log file ──────────────────────────────────────────
: > "$BACKUP_LOG"
set_status "RUNNING"
log "=== Sovran_SystemsOS External Hub Backup ==="
log "Starting backup process…"
# ── 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"
# ── Detect target drive ──────────────────────────────────────────
if [[ -n "${BACKUP_TARGET:-}" ]]; then
TARGET="$BACKUP_TARGET"
# Safety: never allow internal drives even if explicitly passed
if is_internal "$TARGET"; then
fail "Target '$TARGET' is an internal system drive and cannot be used for external backup."
fi
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 exFAT-formatted USB drive (≥500 GB) and try again."
fi
log "Detected external drive: $TARGET"
fi
# ── Verify mount point ───────────────────────────────────────────
[[ -d "$TARGET" ]] || fail "Target path '$TARGET' does not exist."
mountpoint -q "$TARGET" || fail "Target path '$TARGET' is not a mount point."
# ── Check free disk space (require ≥ 10 GB) ──────────────────────
FREE_KB=$(df -k --output=avail "$TARGET" | tail -1)
FREE_GB=$(( FREE_KB / 1024 / 1024 ))
log "Free space on drive: ${FREE_GB} GB"
(( FREE_GB >= MIN_FREE_GB )) || \
fail "Not enough free space on drive (${FREE_GB} GB available, ${MIN_FREE_GB} GB required)."
# ── Create timestamped backup directory ─────────────────────────
TIMESTAMP="$(date '+%Y%m%d_%H%M%S')"
BACKUP_DIR="${TARGET}/Sovran_SystemsOS_Backup/${TIMESTAMP}"
mkdir -p "$BACKUP_DIR"
log "Backup destination: $BACKUP_DIR"
# ── Stage 1/4: NixOS configuration ──────────────────────────────
log ""
log "── Stage 1/4: NixOS configuration (/etc/nixos) ──────────────"
if [[ -d /etc/nixos ]]; then
rsync -a --info=progress2 /etc/nixos/ "$BACKUP_DIR/nixos/" 2>&1 | tee -a "$BACKUP_LOG" || \
fail "Stage 1 failed while copying /etc/nixos"
log "Stage 1 complete."
else
log "WARNING: /etc/nixos not found — skipping."
fi
# ── Stage 2/4: Secrets ──────────────────────────────────────────
log ""
log "── Stage 2/4: Secrets ───────────────────────────────────────"
mkdir -p "$BACKUP_DIR/secrets"
if [[ "$ROLE" == "desktop" ]]; then
log "Skipping /etc/nix-bitcoin-secrets — not applicable for Desktop Only role."
else
if [[ -e /etc/nix-bitcoin-secrets ]]; then
rsync -a --info=progress2 /etc/nix-bitcoin-secrets "$BACKUP_DIR/secrets/" 2>&1 | tee -a "$BACKUP_LOG" || \
log "WARNING: Could not copy /etc/nix-bitcoin-secrets — continuing."
else
log " (not found: /etc/nix-bitcoin-secrets — skipping)"
fi
fi
log "Stage 2 complete."
# ── Stage 3/4: Home directory ────────────────────────────────────
log ""
log "── Stage 3/4: Home directory (/home) ───────────────────────"
if [[ -d /home ]]; then
rsync -a --info=progress2 \
--exclude='.cache/' \
--exclude='.local/share/Trash/' \
--exclude='*/Trash/' \
/home/ "$BACKUP_DIR/home/" 2>&1 | tee -a "$BACKUP_LOG" || \
fail "Stage 3 failed while copying /home"
log "Stage 3 complete."
else
log "WARNING: /home not found — skipping."
fi
# ── Stage 4/4: System data ───────────────────────────────────────
log ""
log "── Stage 4/4: System data (/var/lib) ────────────────────────"
if [[ "$ROLE" == "desktop" ]]; then
if [[ -d /var/lib ]]; then
rsync -a --info=progress2 \
--filter='- /lnd/***' \
--exclude='logs/' \
--exclude='log/' \
--exclude='*/logs/' \
--exclude='*/log/' \
/var/lib/ "$BACKUP_DIR/var-lib/" 2>&1 | tee -a "$BACKUP_LOG" || \
fail "Stage 4 failed while copying /var/lib for Desktop Only role"
log "Stage 4 complete (Desktop Only role excludes /var/lib/lnd)."
else
log "WARNING: /var/lib not found — skipping."
fi
elif [[ -d /var/lib ]]; then
rsync -a --info=progress2 \
--exclude='logs/' \
--exclude='log/' \
--exclude='*/logs/' \
--exclude='*/log/' \
/var/lib/ "$BACKUP_DIR/var-lib/" 2>&1 | tee -a "$BACKUP_LOG" || \
fail "Stage 4 failed while copying /var/lib"
log "Stage 4 complete."
else
log "WARNING: /var/lib not found — skipping."
fi
# ── Generate manifest ────────────────────────────────────────────
log ""
log "Generating BACKUP_MANIFEST.txt …"
{
echo "Sovran_SystemsOS Backup Manifest"
echo "Generated: $(date)"
echo "Hostname: $(hostname)"
echo "Role: $ROLE_LABEL"
echo "Target: $TARGET"
echo ""
echo "Contents:"
find "$BACKUP_DIR" -mindepth 1 -maxdepth 2 | sort
} > "$BACKUP_DIR/BACKUP_MANIFEST.txt"
log "Manifest written to $BACKUP_DIR/BACKUP_MANIFEST.txt"
# ── Done ─────────────────────────────────────────────────────────
log ""
log "All Finished! Your data is now backed up to a third location."
log "Please eject the drive safely before removing it from your Sovran Pro."
set_status "SUCCESS"
File diff suppressed because it is too large Load Diff
-378
View File
@@ -1,378 +0,0 @@
/* Sovran_SystemsOS Hub — Vanilla JS Frontend
v8 — Status-only dashboard + Tech Support + Feature Manager */
"use strict";
var POLL_INTERVAL_SERVICES = 5000;
var POLL_INTERVAL_UPDATES = 1800000;
var UPDATE_POLL_INTERVAL = 2000;
var REBOOT_CHECK_INTERVAL = 5000;
var SUPPORT_TIMER_INTERVAL = 1000;
var CATEGORY_ORDER = [
"infrastructure",
"bitcoin-base",
"bitcoin-apps",
"communication",
"apps",
"nostr",
"support",
"feature-manager",
];
var FEATURE_SUBCATEGORY_LABELS = {
"infrastructure": "🔧 Infrastructure",
"bitcoin": "₿ Bitcoin",
"communication": "💬 Communication",
"nostr": "📡 Nostr",
};
var FEATURE_SUBCATEGORY_ORDER = ["infrastructure", "bitcoin", "communication", "nostr"];
var FEATURE_UNIT_MAP = {
"rdp": "gnome-remote-desktop.service",
"haven": "haven-relay.service",
"element-calling": "livekit.service",
"mempool": "mempool-frontend.service",
};
var STATUS_LOADING_STATES = new Set([
"reloading", "activating", "deactivating", "maintenance",
]);
// ── State ─────────────────────────────────────────────────────────
var _servicesCache = [];
var _categoryLabels = {};
var _updateLog = "";
var _updatePollTimer = null;
var _updateLogOffset = 0;
var _serverWasDown = false;
var _updateFinished = false;
var _supportTimerInt = null;
var _supportEnabledAt = null;
var _cachedExternalIp = null;
// Feature Manager state
var _featuresData = null;
var _rebuildLog = "";
var _rebuildLogOffset = 0;
var _rebuildPollTimer = null;
var _rebuildFinished = false;
var _rebuildServerDown = false;
var _pendingToggle = null;
// ── DOM refs ──────────────────────────────────────────────────────
var $tilesArea = document.getElementById("tiles-area");
var $updateBtn = document.getElementById("btn-update");
var $updateBadge = document.getElementById("update-badge");
var $refreshBtn = document.getElementById("btn-refresh");
var $internalIp = document.getElementById("ip-internal");
var $externalIp = document.getElementById("ip-external");
var $modal = document.getElementById("update-modal");
var $modalSpinner = document.getElementById("modal-spinner");
var $modalStatus = document.getElementById("modal-status");
var $modalLog = document.getElementById("modal-log");
var $btnReboot = document.getElementById("btn-reboot");
var $btnSave = document.getElementById("btn-save-report");
var $btnCloseModal = document.getElementById("btn-close-modal");
var $rebootOverlay = document.getElementById("reboot-overlay");
var $credsModal = document.getElementById("creds-modal");
var $credsTitle = document.getElementById("creds-modal-title");
var $credsBody = document.getElementById("creds-body");
var $credsCloseBtn = document.getElementById("creds-close-btn");
var $supportModal = document.getElementById("support-modal");
var $supportBody = document.getElementById("support-body");
var $supportCloseBtn = document.getElementById("support-close-btn");
// Feature Manager — rebuild modal
var $rebuildModal = document.getElementById("rebuild-modal");
var $rebuildSpinner = document.getElementById("rebuild-spinner");
var $rebuildStatus = document.getElementById("rebuild-status");
var $rebuildLogEl = document.getElementById("rebuild-log");
var $rebuildReboot = document.getElementById("rebuild-reboot-btn");
var $rebuildSave = document.getElementById("rebuild-save-report");
var $rebuildClose = document.getElementById("rebuild-close-btn");
// Feature Manager — domain setup modal
var $domainSetupModal = document.getElementById("domain-setup-modal");
var $domainSetupTitle = document.getElementById("domain-setup-title");
var $domainSetupBody = document.getElementById("domain-setup-body");
var $domainSetupClose = document.getElementById("domain-setup-close-btn");
// Feature Manager — SSL email modal
var $sslEmailModal = document.getElementById("ssl-email-modal");
var $sslEmailInput = document.getElementById("ssl-email-input");
var $sslEmailSave = document.getElementById("ssl-email-save-btn");
var $sslEmailCancel = document.getElementById("ssl-email-cancel-btn");
var $sslEmailClose = document.getElementById("ssl-email-close-btn");
// Feature Manager — confirm modal
var $featureConfirmModal = document.getElementById("feature-confirm-modal");
var $featureConfirmMsg = document.getElementById("feature-confirm-message");
var $featureConfirmOk = document.getElementById("feature-confirm-ok-btn");
var $featureConfirmCancel = document.getElementById("feature-confirm-cancel-btn");
var $featureConfirmClose = document.getElementById("feature-confirm-close-btn");
// ── Helpers ───────────────────────────────────────────────────────
function tileId(svc) { return svc.unit + "::" + svc.name; }
function statusClass(status) {
if (!status) return "unknown";
if (status === "active") return "active";
if (status === "inactive") return "inactive";
if (status === "failed") return "failed";
if (status === "disabled") return "disabled";
if (STATUS_LOADING_STATES.has(status)) return "loading";
return "unknown";
}
function statusText(status, enabled) {
if (!enabled) return "disabled";
if (!status || status === "unknown") return "unknown";
return status;
}
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>');
}
function formatDuration(seconds) {
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
var 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 ────────────────────────────────────────────────
async function apiFetch(path, options) {
var res = await fetch(path, options || {});
if (!res.ok) throw new Error(res.status + " " + res.statusText);
return res.json();
}
// ── Render: initial build ─────────────────────────────────────────
function buildTiles(services, categoryLabels) {
_servicesCache = services;
var grouped = {};
for (var i = 0; i < services.length; i++) {
var cat = services[i].category || "other";
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push(services[i]);
}
$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>';
}
}
function buildTile(svc) {
var isSupport = svc.type === "support";
var sc = statusClass(svc.status);
var st = statusText(svc.status, svc.enabled);
var dis = !svc.enabled;
var hasCreds = svc.has_credentials && 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 (dis) tile.title = svc.name + " is not enabled in custom.nix";
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 to manage</span></div>';
tile.style.cursor = "pointer";
tile.addEventListener("click", function() { openSupportModal(); });
return tile;
}
var infoBtn = hasCreds ? '<button class="tile-info-btn" data-unit="' + escHtml(svc.unit) + '" title="Connection info">i</button>' : "";
tile.innerHTML = infoBtn + '<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">' + escHtml(st) + '</span></div>';
var infoBtnEl = tile.querySelector(".tile-info-btn");
if (infoBtnEl) {
infoBtnEl.addEventListener("click", function(e) {
e.stopPropagation();
openCredsModal(svc.unit, svc.name);
});
}
return tile;
}
// ── Render: live update ───────────────────────────────────────────
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;
var sc = statusClass(svc.status);
var st = statusText(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;
}
}
// ── 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); }
} 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 ──────────────────────────────────────────────────
async function checkUpdates() {
try {
var data = await apiFetch("/api/updates/check");
var hasUpdates = !!data.available;
if ($updateBadge) $updateBadge.classList.toggle("visible", hasUpdates);
if ($updateBtn) $updateBtn.classList.toggle("has-updates", hasUpdates);
} catch (_) {}
}
// ── Credentials info modal ───────────────────────────────────────
async function openCredsModal(unit, name) {
if (!$credsModal) return;
if ($credsTitle) $credsTitle.textContent = name + " — Connection Info";
if ($credsBody) $credsBody.innerHTML = '<p class="creds-loading">Loading…</p>';
$credsModal.classList.add("open");
try {
var data = await apiFetch("/api/credentials/" + encodeURIComponent(unit));
if (!data.credentials || data.credentials.length === 0) {
$credsBody.innerHTML = '<p class="creds-empty">No connection info available yet.</p>';
return;
}
var html = "";
for (var i = 0; i < data.credentials.length; i++) {
var cred = data.credentials[i];
var id = "cred-" + Math.random().toString(36).substring(2, 8);
var displayValue = linkify(cred.value);
var qrBlock = "";
if (cred.qrcode) {
qrBlock = '<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + cred.qrcode + '" alt="QR Code for ' + escHtml(cred.label) + '"><div class="creds-qr-hint">Scan with Zeus app on your phone</div></div>';
}
html += '<div class="creds-row"><div class="creds-label">' + escHtml(cred.label) + '</div>' + qrBlock + '<div class="creds-value-wrap"><div class="creds-value" id="' + id + '">' + displayValue + '</div><button class="creds-copy-btn" data-target="' + id + '">Copy</button></div></div>';
}
$credsBody.innerHTML = html;
$credsBody.querySelectorAll(".creds-copy-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
var target = document.getElementById(btn.dataset.target);
if (target) {
navigator.clipboard.writeText(target.textContent).then(function() {
btn.textContent = "Copied!";
btn.classList.add("copied");
setTimeout(function() { btn.textContent = "Copy"; btn.classList.remove("copied"); }, 1500);
}).catch(function() {});
}
});
});
} catch (err) {
$credsBody.innerHTML = '<p class="creds-empty">Could not load credentials.</p>';
}
}
function closeCredsModal() { if ($credsModal) $credsModal.classList.remove("open"); }
// ── Tech Support modal ────────────────────────────────────────────
async function openSupportModal() {
if (!$supportModal) return;
$supportModal.classList.add("open");
$supportBody.innerHTML = '<p class="creds-loading">Checking support status…</p>';
try {
var status = await apiFetch("/api/support/status");
if (status.active) { _supportEnabledAt = status.enabled_at; renderSupportActive(); }
else { renderSupportInactive(); }
} catch (err) {
$supportBody.innerHTML = '<p class="creds-empty">Could not check support status.</p>';
}
}
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 give Sovran Systems secure SSH access to your machine so we can diagnose and fix issues for you.</p><div class="support-info-box"><div class="support-info-row"><span class="support-info-label">Your External IP</span><span class="support-info-value" id="support-ext-ip">' + escHtml(ip) + '</span></div><p class="support-info-hint">Give this IP to your Sovran Systems technician when asked.</p></div><div class="support-steps"><p class="support-steps-title">What happens when you click Enable:</p><ol><li>A Sovran Systems SSH key is added to this machine</li><li>You give us your External IP shown above</li><li>We connect and help you remotely</li><li>When done, you click <strong>End Support Session</strong> to remove the key</li></ol></div><button class="btn support-btn-enable" id="btn-support-enable">Enable Support Access</button><p class="support-fine-print">You can end the session at any time. The access key will be completely removed.</p></div>';
document.getElementById("btn-support-enable").addEventListener("click", enableSupport);
}
function renderSupportActive() {
var ip = _cachedExternalIp || "loading…";
$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-desc">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 External IP</span><span class="support-info-value">' + escHtml(ip) + '</span></div><div class="support-info-row"><span class="support-info-label">Session Duration</span><span class="support-info-value" id="support-timer">—</span></div></div><p class="support-active-note">When your support session is complete, click the button below to <strong>immediately remove</strong> the access key.</p><button class="btn support-btn-disable" id="btn-support-disable">End Support Session</button></div>';
document.getElementById("btn-support-disable").addEventListener("click", disableSupport);
startSupportTimer();
}
function renderSupportRemoved(verified) {
stopSupportTimer();
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 your machine to be sure.";
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><button class="btn support-btn-done" id="btn-support-done">Done</button></div>';
document.getElementById("btn-support-done").addEventListener("click", closeSupportModal);
}
async function enableSupport() {
var btn = document.getElementById("btn-support-enable");
if (btn) { btn.disabled = true; btn.textContent = "Enabling…"; }
try {
await apiFetch("/api/support/enable", { method: "POST" });
var status = await apiFetch("/api/support/status");
@@ -0,0 +1,141 @@
/* Sovran_SystemsOS Hub — Web UI Stylesheet
Dark theme — near-black with green accents matching the Sovran Hub icon
v8 — Black-forward, green used for accents/borders/highlights only */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg-color: #080a09;
--surface-color: rgba(14, 16, 15, 0.7);
--card-color: rgba(20, 22, 21, 0.6);
--border-color: rgba(255, 255, 255, 0.06);
--text-primary: #ecf3ef;
--text-secondary: #8aaa9a;
--text-dim: #4a6658;
--accent-color: #5EAD8A;
--green: #6DBF8B;
--yellow: #e5a50a;
--red: #e01b24;
--grey: #5E7A6A;
--radius-card: 18px;
--radius-btn: 8px;
--shadow-card: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-hover: 0 8px 32px rgba(0, 0, 0, 0.5);
}
html, body {
height: 100%;
}
body {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background:
radial-gradient(ellipse at top, rgba(94, 173, 138, 0.04) 0%, transparent 50%),
var(--bg-color);
color: var(--text-primary);
line-height: 1.5;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── Login page ──────────────────────────────────────────────────── */
.login-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 24px;
}
.login-card {
background-color: rgba(14, 16, 15, 0.75);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
padding: 48px 40px;
width: 100%;
max-width: 400px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-logo {
height: 64px;
margin-bottom: 16px;
}
.login-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
}
.login-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group label {
display: block;
font-size: 0.82rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 6px;
}
.form-group input {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--border-color);
border-radius: var(--radius-btn);
background-color: var(--card-color);
color: var(--text-primary);
font-size: 0.92rem;
}
.form-group input:focus {
outline: none;
border-color: var(--accent-color);
}
.btn-login {
width: 100%;
padding: 12px;
border-radius: var(--radius-btn);
background-color: var(--accent-color);
color: #0A1A10;
font-size: 0.95rem;
font-weight: 700;
margin-top: 8px;
}
.btn-login:hover {
opacity: 0.88;
}
.login-error {
background-color: rgba(224, 27, 36, 0.12);
border: 1px solid var(--red);
color: #f87171;
padding: 10px 14px;
border-radius: 8px;
font-size: 0.85rem;
display: none;
}
.login-error.visible {
display: block;
}
@@ -0,0 +1,86 @@
/* ── Buttons ────────────────────────────────────────────────────── */
button {
font-family: inherit;
cursor: pointer;
border: none;
outline: none;
transition: opacity 0.15s, box-shadow 0.15s, background-color 0.15s;
}
button:disabled {
opacity: 0.45;
cursor: default;
}
.btn {
padding: 7px 16px;
border-radius: var(--radius-btn);
font-size: 0.88rem;
font-weight: 600;
}
.btn-primary {
background-color: var(--accent-color);
color: #0A1A10;
}
.btn-primary:hover:not(:disabled) {
opacity: 0.88;
}
/* Update System button: uses accent green by default */
.btn-update {
background-color: #4A9474;
color: #E0F2EA;
position: relative;
display: flex;
align-items: center;
gap: 8px;
}
.btn-update:hover:not(:disabled) {
opacity: 0.88;
}
/* Update System button: brighter green when updates are available */
.btn-update.has-updates {
background-color: #5EAD8A;
color: #0A1A10;
}
.btn-update.has-updates:hover:not(:disabled) {
background-color: #78C8A2;
}
.update-badge {
display: none;
width: 10px;
height: 10px;
background-color: var(--yellow);
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); }
}
.btn-icon {
background: none;
color: var(--text-secondary);
padding: 6px;
border-radius: 50%;
font-size: 1.1rem;
line-height: 1;
}
.btn-icon:hover:not(:disabled) {
background-color: var(--border-color);
color: var(--text-primary);
}
@@ -0,0 +1,173 @@
/* ── 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: #0c0f0e;
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: #0c0f0e;
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;
}
@@ -0,0 +1,143 @@
/* ── Feature Manager styles ──────────────────────────────────────── */
.feature-manager-section {
margin-bottom: 32px;
}
.feature-subcategory {
margin-bottom: 16px;
}
.feature-subcategory-header {
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-dim);
margin-bottom: 8px;
padding-left: 4px;
}
.feature-cards-wrap {
display: flex;
flex-direction: column;
gap: 10px;
}
.feature-card {
background-color: var(--card-color);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 14px 16px;
}
.feature-card-top {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 8px;
}
.feature-card-info {
flex: 1;
min-width: 0;
}
.feature-card-name {
font-size: 0.9rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
.feature-card-desc {
font-size: 0.78rem;
color: var(--text-secondary);
line-height: 1.5;
}
.feature-card-status {
font-size: 0.72rem;
color: var(--text-dim);
margin-top: 6px;
}
.feature-toggle {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
flex-shrink: 0;
cursor: pointer;
}
.feature-toggle-input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
.feature-toggle-slider {
position: absolute;
inset: 0;
background-color: var(--border-color);
border-radius: 24px;
transition: background-color 0.2s;
}
.feature-toggle-slider::before {
content: "";
position: absolute;
width: 18px;
height: 18px;
left: 3px;
top: 3px;
background-color: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
.feature-toggle.active .feature-toggle-slider {
background-color: var(--green);
}
.feature-toggle.active .feature-toggle-slider::before {
transform: translateX(20px);
}
.feature-domain-badge {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
font-size: 0.78rem;
}
.feature-domain-icon {
flex-shrink: 0;
}
.feature-domain-label {
color: var(--text-secondary);
}
.feature-domain-label--checking {
color: var(--text-dim);
font-style: italic;
}
.feature-domain-label--ok {
color: var(--green);
font-weight: 600;
}
.feature-domain-label--warn {
color: var(--yellow);
font-weight: 600;
}
.feature-domain-label--error {
color: var(--red);
font-weight: 600;
}
@@ -0,0 +1,93 @@
/* ── Header bar ─────────────────────────────────────────────────── */
.header-bar {
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
background-color: rgba(10, 12, 11, 0.82);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
padding: 8px 24px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 16px;
position: sticky;
top: 0;
z-index: 100;
}
.header-logo {
height: 80px;
width: auto;
display: block;
flex-shrink: 0;
}
.header-bar .title {
font-size: 1.15rem;
font-weight: 700;
color: var(--text-primary);
}
.header-buttons {
display: flex;
align-items: center;
gap: 10px;
}
.role-badge {
background-color: var(--accent-color);
color: #0A1A10;
font-size: 0.72rem;
font-weight: 700;
padding: 3px 10px;
border-radius: 20px;
letter-spacing: 0.03em;
}
/* ── IP bar ─────────────────────────────────────────────────────── */
.ip-bar {
background-color: rgba(10, 12, 11, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding: 8px 24px;
display: flex;
align-items: center;
justify-content: center;
gap: 32px;
font-size: 0.82rem;
color: var(--text-secondary);
}
.ip-bar .ip-label {
color: var(--text-dim);
margin-right: 6px;
}
.ip-bar .ip-value {
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
color: var(--accent-color);
font-weight: 600;
}
.ip-separator {
color: var(--border-color);
}
.btn-logout {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.18);
color: var(--text-secondary);
font-size: 0.78rem;
font-weight: 600;
padding: 4px 12px;
border-radius: var(--radius-btn);
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.btn-logout:hover {
border-color: var(--accent-color);
color: var(--accent-color);
}
@@ -0,0 +1,175 @@
/* ── Main content ───────────────────────────────────────────────── */
.main-content {
display: flex;
align-items: flex-start;
flex: 1;
overflow: hidden;
max-width: 1400px;
width: 100%;
margin-left: auto;
margin-right: auto;
}
/* ── Sidebar ────────────────────────────────────────────────────── */
.sidebar {
width: 270px;
flex-shrink: 0;
height: 100%;
overflow-y: auto;
border-right: 1px solid rgba(255, 255, 255, 0.06);
background-color: rgba(12, 14, 13, 0.65);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
padding: 20px 14px;
display: flex;
flex-direction: column;
gap: 0;
}
/* ── Sidebar: Tech Support button ───────────────────────────────── */
.sidebar-support-btn {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
background-color: var(--card-color);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 12px 14px;
color: var(--text-primary);
cursor: pointer;
transition: border-style 0.15s, border-color 0.15s, background-color 0.15s;
text-align: left;
}
.sidebar-support-btn:hover {
border-color: var(--accent-color);
border-style: solid;
background-color: #162320;
}
.sidebar-support-btn + .sidebar-support-btn {
margin-top: 8px;
}
.sidebar-support-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.sidebar-support-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.sidebar-support-title {
font-size: 0.88rem;
font-weight: 700;
color: var(--text-primary);
}
.sidebar-support-hint {
font-size: 0.72rem;
color: var(--accent-color);
font-weight: 600;
}
.sidebar-divider {
border: none;
border-top: 1px solid var(--border-color);
margin: 16px 0;
}
/* ── Upgrade modal ──────────────────────────────────────────────── */
.upgrade-dialog {
max-width: 480px;
}
.upgrade-info-box {
background-color: var(--card-color);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 14px 18px;
margin-bottom: 14px;
}
.upgrade-info-title {
font-size: 0.88rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.upgrade-info-list {
padding-left: 20px;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.7;
margin: 0;
}
.upgrade-info-list a {
color: var(--accent-color);
}
.upgrade-rebuild-note {
font-style: italic;
color: var(--text-dim);
font-size: 0.82rem;
}
/* ── Tiles area ─────────────────────────────────────────────────── */
#tiles-area {
flex: 1;
height: 100%;
overflow-y: auto;
padding: 24px 20px 48px;
min-width: 0;
}
/* ── Category sections ──────────────────────────────────────────── */
.category-section {
margin-bottom: 32px;
}
.section-header {
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-secondary);
margin-bottom: 4px;
padding-left: 4px;
}
.section-divider {
border: none;
border-top: 1px solid var(--border-color);
margin-bottom: 16px;
}
.tiles-grid {
display: flex;
flex-wrap: wrap;
gap: 14px;
}
/* ── Empty state ────────────────────────────────────────────────── */
.empty-state {
text-align: center;
padding: 64px 24px;
color: var(--text-dim);
}
.empty-state p {
font-size: 1rem;
margin-bottom: 8px;
}
@@ -0,0 +1,438 @@
/* ── Update modal ────────────────────────────────────────────────── */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
z-index: 200;
align-items: center;
justify-content: center;
}
.modal-overlay.open {
display: flex;
}
.modal-dialog {
background-color: rgba(14, 16, 15, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
width: 90vw;
max-width: 900px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
}
.modal-header {
display: flex;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
gap: 12px;
}
.modal-title {
font-size: 1rem;
font-weight: 700;
flex: 1;
}
.modal-status {
font-size: 0.85rem;
color: var(--text-secondary);
}
.modal-spinner {
width: 18px;
height: 18px;
border: 2.5px solid var(--border-color);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: spin 0.75s linear infinite;
display: none;
}
.modal-spinner.spinning {
display: block;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.modal-log {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-size: 0.78rem;
line-height: 1.6;
color: var(--text-primary);
background-color: #0c0f0e;
white-space: pre-wrap;
word-break: break-all;
min-height: 200px;
}
.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 12px 20px;
border-top: 1px solid var(--border-color);
}
/* Reboot = GREEN */
.modal-footer .btn-reboot,
button.btn-reboot {
background-color: #6DBF8B;
color: #fff;
}
.modal-footer .btn-reboot:hover:not(:disabled),
button.btn-reboot:hover:not(:disabled) {
background-color: #529E7E;
}
.btn-save {
background-color: var(--yellow);
color: #0A1A10;
}
.btn-save:hover:not(:disabled) {
background-color: #c98d08;
}
.btn-close-modal {
background-color: var(--border-color);
color: var(--text-primary);
}
.btn-close-modal:hover:not(:disabled) {
background-color: #1c2a24;
}
/* ── Credentials info modal ──────────────────────────────────────── */
.creds-dialog {
background-color: rgba(14, 16, 15, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
width: 90vw;
max-width: 700px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
animation: creds-fade-in 0.2s ease-out;
}
@keyframes creds-fade-in {
from { opacity: 0; transform: scale(0.95) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.creds-header {
display: flex;
align-items: center;
padding: 20px 28px;
border-bottom: 1px solid var(--border-color);
}
.creds-title {
font-size: 1.15rem;
font-weight: 700;
flex: 1;
display: flex;
align-items: center;
gap: 10px;
}
.creds-title-icon {
width: 28px;
height: 28px;
vertical-align: middle;
border-radius: 6px;
flex-shrink: 0;
}
.creds-close-btn {
background: none;
color: var(--text-secondary);
font-size: 1.3rem;
padding: 4px 8px;
border-radius: 6px;
cursor: pointer;
border: none;
}
.creds-close-btn:hover {
background-color: var(--border-color);
color: var(--text-primary);
}
.creds-body {
padding: 24px 28px;
overflow-y: auto;
}
.creds-loading {
color: var(--text-dim);
text-align: center;
padding: 24px 0;
}
.creds-row {
margin-bottom: 20px;
}
.creds-row:last-child {
margin-bottom: 0;
}
.creds-label {
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-dim);
margin-bottom: 6px;
}
.creds-value-wrap {
display: flex;
align-items: flex-start;
gap: 10px;
}
.creds-value {
flex: 1;
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-size: 0.92rem;
color: var(--text-primary);
background-color: #0c0f0e;
padding: 12px 16px;
border-radius: 8px;
word-break: break-all;
white-space: pre-wrap;
line-height: 1.6;
border: 1px solid var(--border-color);
}
.creds-copy-btn {
background-color: var(--border-color);
color: var(--text-primary);
font-size: 0.78rem;
font-weight: 600;
padding: 8px 14px;
border-radius: 6px;
cursor: pointer;
border: none;
white-space: nowrap;
flex-shrink: 0;
align-self: flex-start;
margin-top: 10px;
}
.creds-copy-btn:hover {
background-color: #1c2a24;
}
.creds-copy-btn.copied {
background-color: var(--green);
color: #fff;
}
.creds-empty {
color: var(--text-dim);
text-align: center;
padding: 24px 0;
font-size: 0.88rem;
}
/* ── Credential links ────────────────────────────────────────────── */
.creds-link {
color: #b8f0c0;
text-decoration: none;
word-break: break-all;
}
.creds-link:hover {
text-decoration: underline;
color: #defce6;
}
/* ── Matrix action buttons ───────────────────────────────────────── */
.matrix-actions-divider {
border: none;
border-top: 1px solid var(--border-color);
margin: 18px 0 14px;
}
.matrix-actions-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.matrix-action-btn {
background-color: var(--accent-color);
color: #0A1A10;
font-size: 0.88rem;
font-weight: 700;
padding: 10px 18px;
border-radius: 8px;
border: none;
cursor: pointer;
flex: 1;
min-width: 140px;
}
.matrix-action-btn:hover {
background-color: #7CC4A0;
}
.matrix-form-group {
margin-bottom: 14px;
}
.matrix-form-label {
display: block;
font-size: 0.82rem;
color: var(--text-secondary);
margin-bottom: 6px;
font-weight: 600;
}
.matrix-form-input {
width: 100%;
background-color: #0c0f0e;
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 10px 12px;
font-size: 0.9rem;
box-sizing: border-box;
}
.matrix-form-input:focus {
outline: none;
border-color: var(--accent-color);
}
.matrix-form-checkbox-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.matrix-form-checkbox-row input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--accent-color);
}
.matrix-form-actions {
display: flex;
gap: 10px;
margin-top: 18px;
}
.matrix-form-submit {
background-color: var(--accent-color);
color: #0A1A10;
font-size: 0.88rem;
font-weight: 700;
padding: 10px 20px;
border-radius: 8px;
border: none;
cursor: pointer;
flex: 1;
}
.matrix-form-submit:hover:not(:disabled) {
background-color: #7CC4A0;
}
.matrix-form-submit:disabled {
opacity: 0.6;
cursor: default;
}
.matrix-form-back {
background-color: var(--border-color);
color: var(--text-primary);
font-size: 0.88rem;
font-weight: 600;
padding: 10px 20px;
border-radius: 8px;
border: none;
cursor: pointer;
}
.matrix-form-back:hover {
background-color: #1c2a24;
}
.matrix-form-result {
margin-top: 14px;
padding: 12px 16px;
border-radius: 8px;
font-size: 0.88rem;
line-height: 1.5;
display: none;
}
.matrix-form-result.success {
background-color: rgba(109, 191, 139, 0.12);
border: 1px solid var(--green);
color: var(--green);
display: block;
}
.matrix-form-result.error {
background-color: rgba(239, 68, 68, 0.12);
border: 1px solid #ef4444;
color: #f87171;
display: block;
}
/* ── QR code in credentials modal ────────────────────────────────── */
.creds-qr-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
margin-bottom: 10px;
}
.creds-qr-img {
width: 240px;
height: 240px;
border-radius: 12px;
border: 4px solid #fff;
background-color: #fff;
image-rendering: pixelated;
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
}
.creds-qr-hint {
margin-top: 10px;
font-size: 0.82rem;
color: var(--text-secondary);
font-style: italic;
}
@@ -0,0 +1,879 @@
/* ── 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(94, 173, 138, 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: #0A1A10;
}
.onboarding-step-dot.completed {
background-color: var(--green);
border-color: var(--green);
color: #0A1A10;
}
.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(14, 16, 15, 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(94, 173, 138, 0.10);
padding: 3px 10px;
border-radius: 20px;
border: 1px solid rgba(94, 173, 138, 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(229, 165, 10, 0.1);
border: 1px solid rgba(229, 165, 10, 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(94, 173, 138, 0.08);
border: 1px solid rgba(94, 173, 138, 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(94, 173, 138, 0.08);
border: 1px solid rgba(94, 173, 138, 0.20);
color: var(--accent-color);
cursor: pointer;
white-space: nowrap;
transition: background-color 0.15s;
}
.onboarding-cred-reveal-btn:hover {
background-color: rgba(94, 173, 138, 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(94, 173, 138, 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(229, 165, 10, 0.1);
border: 1px solid rgba(229, 165, 10, 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(94, 173, 138, 0.10);
border: 1px solid rgba(94, 173, 138, 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(14, 16, 15, 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 ─────────────────────────────────────────────────── */
@media (max-width: 768px) {
body {
overflow: auto;
}
.main-content {
flex-direction: column;
overflow: visible;
}
.sidebar {
width: 100%;
height: auto;
border-right: none;
border-bottom: 1px solid var(--border-color);
padding: 14px 12px;
}
#tiles-area {
height: auto;
overflow-y: visible;
padding: 16px 12px 40px;
}
}
@media (max-width: 600px) {
.header-bar {
padding: 10px 14px;
gap: 10px;
}
.header-bar .title {
font-size: 0.95rem;
}
.ip-bar {
gap: 16px;
flex-wrap: wrap;
padding: 8px 14px;
}
.tiles-grid {
justify-content: center;
}
.service-tile {
width: 140px;
min-height: 130px;
}
.reboot-card {
padding: 36px 28px;
margin: 0 16px;
}
.creds-dialog {
margin: 0 12px;
}
.creds-qr-img {
width: 200px;
height: 200px;
}
}
@@ -0,0 +1,457 @@
/* ── 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 #c94040;
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: #c94040;
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: #a83030;
}
.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: #6DBF8B; }
.security-status-error { color: #e05252; }
/* ── 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, #6DBF8B);
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(109, 191, 139, 0.15);
color: #6DBF8B;
}
.security-verify-fail {
background-color: rgba(224, 82, 82, 0.15);
color: #e05252;
}
.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-color: rgba(6, 8, 7, 0.94);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 1000;
align-items: center;
justify-content: center;
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: monospace;
font-size: 1.35rem;
font-weight: 700;
color: var(--text-primary);
background: rgba(109, 191, 139, 0.10);
border: 1.5px solid rgba(109, 191, 139, 0.35);
border-radius: 8px;
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-color: #6DBF8B;
color: #0a0c0b;
border: none;
border-radius: 7px;
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: #5aab78;
}
.security-reset-reboot-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ── First-login security banner ─────────────────────────────────── */
.security-first-login-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 18px;
background-color: rgba(94, 173, 138, 0.08);
border-bottom: 2px solid rgba(94, 173, 138, 0.25);
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-color);
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
color: var(--text-secondary);
padding: 2px 7px;
flex-shrink: 0;
line-height: 1.4;
transition: background-color 0.15s;
}
.security-banner-dismiss:hover {
background-color: rgba(0,0,0,0.08);
}
/* ── 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 #c97a00;
border-radius: 6px;
color: var(--text-primary);
}
.security-inline-icon {
font-size: 1rem;
color: #e69000;
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: #e69000;
text-decoration: none;
border: 1px solid #c97a00;
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: #c97a00;
background-color: rgba(180, 100, 0, 0.10);
border-left: 2px solid #c97a00;
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(94, 173, 138, 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: #0A1A10;
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: #0A1A10;
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: #1c2a24;
}
.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(109, 191, 139, 0.06);
border-color: rgba(109, 191, 139, 0.3);
}
.support-wallet-unlocked {
background-color: rgba(229, 165, 10, 0.06);
border-color: rgba(229, 165, 10, 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: #0A1A10;
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: #529E7E;
}
/* ── 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: #0c0f0e;
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,410 @@
/* ── Service tile card (status-only) ─────────────────────────────── */
.service-tile {
width: 160px;
min-height: 130px;
background-color: var(--card-color);
border: 1px solid var(--border-color);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-radius: var(--radius-card);
box-shadow: var(--shadow-card);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px 12px 18px;
gap: 0;
transition: box-shadow 0.2s, border-color 0.2s;
position: relative;
cursor: pointer;
}
.service-tile:hover {
box-shadow: var(--shadow-hover);
border-color: var(--accent-color);
}
.service-tile.disabled {
opacity: 0.45;
}
.tile-icon {
width: 48px;
height: 48px;
object-fit: contain;
margin-bottom: 10px;
}
.tile-icon-fallback {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--border-color);
border-radius: 12px;
color: var(--text-dim);
font-size: 1.5rem;
margin-bottom: 10px;
}
.tile-name {
font-size: 0.88rem;
font-weight: 600;
text-align: center;
color: var(--text-primary);
line-height: 1.3;
max-width: 140px;
word-break: break-word;
hyphens: auto;
min-height: 1.3em;
display: flex;
align-items: center;
justify-content: center;
}
.tile-status {
font-size: 0.75rem;
margin-top: 8px;
display: flex;
align-items: center;
gap: 5px;
color: var(--text-secondary);
}
.tile-version {
font-size: 0.7rem;
color: var(--text-dim);
margin-top: 2px;
text-align: center;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
background-color: var(--grey);
}
.status-dot.active { background-color: var(--green); }
.status-dot.inactive { background-color: var(--red); }
.status-dot.loading { background-color: var(--yellow); animation: pulse-badge 1s infinite; }
.status-dot.failed { background-color: var(--red); }
.status-dot.disabled { background-color: var(--grey); }
.status-dot.needs-attention { background-color: var(--yellow); }
.status-dot.syncing { background-color: #f5a623; animation: pulse-badge 1.5s infinite; }
.status-dot.checking-reachability { background-color: var(--accent-color); animation: pulse-badge 1s infinite; }
/* ── Bitcoin IBD sync progress bar ──────────────────────────────── */
.tile-sync-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
width: 100%;
margin-top: 6px;
}
.tile-sync-label {
font-size: 0.72rem;
color: #f5a623;
font-weight: 600;
text-align: center;
white-space: nowrap;
}
.tile-sync-bar-row {
display: flex;
align-items: center;
gap: 5px;
width: 100%;
}
.tile-sync-bar-track {
flex: 1;
height: 6px;
background-color: var(--border-color);
border-radius: 3px;
overflow: hidden;
}
.tile-sync-bar-fill {
height: 100%;
background-color: #f5a623;
border-radius: 3px;
transition: width 0.6s ease;
min-width: 2px;
}
.tile-sync-percent {
font-size: 0.72rem;
font-weight: 700;
color: #f5a623;
white-space: nowrap;
min-width: 2.5em;
text-align: right;
}
.tile-sync-eta {
font-size: 0.68rem;
color: var(--text-dim);
text-align: center;
white-space: nowrap;
}
/* ── Service detail modal sections ───────────────────────────────── */
.svc-detail-section {
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.svc-detail-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.svc-detail-section-title {
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-dim);
margin-bottom: 10px;
}
.svc-detail-desc {
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.6;
}
.svc-detail-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary);
}
/* ── Service detail: Domain ──────────────────────────────────────── */
.svc-detail-domain-value {
font-size: 0.9rem;
color: var(--text-primary);
font-weight: 600;
}
.tile-domain-label--ok {
color: var(--green);
font-weight: 600;
}
.tile-domain-label--warn {
color: var(--yellow);
font-weight: 600;
}
.tile-domain-label--error {
color: var(--red);
font-weight: 600;
}
/* ── Service detail: Port table ──────────────────────────────────── */
.svc-detail-port-table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
margin-top: 8px;
}
.svc-detail-port-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);
}
.svc-detail-port-table td {
padding: 8px 10px;
border-bottom: 1px solid rgba(30, 45, 39, 0.6);
color: var(--text-primary);
}
.svc-detail-port-table tr:last-child td {
border-bottom: none;
}
.svc-detail-port-table-port {
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-weight: 600;
color: var(--accent-color);
}
.svc-detail-port-table-proto {
text-transform: uppercase;
color: var(--text-secondary);
}
.svc-detail-port-table-desc {
color: var(--text-secondary);
}
.svc-detail-port-table-status {
font-weight: 600;
}
.port-status-listening { color: var(--green); }
.port-status-open { color: var(--yellow); }
.port-status-closed { color: var(--red); }
.port-status-unknown { color: var(--text-dim); }
/* ── Service detail: Troubleshoot box ────────────────────────────── */
.svc-detail-troubleshoot {
margin-top: 12px;
padding: 14px 16px;
background-color: rgba(229, 165, 10, 0.08);
border: 1px solid rgba(229, 165, 10, 0.3);
border-radius: 10px;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.6;
}
.svc-detail-troubleshoot strong {
color: var(--yellow);
}
.svc-detail-troubleshoot ol {
margin-top: 8px;
padding-left: 20px;
}
.svc-detail-troubleshoot li {
margin-bottom: 4px;
}
.svc-detail-troubleshoot code {
background-color: rgba(94, 173, 138, 0.10);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.82rem;
color: var(--accent-color);
}
.svc-detail-troubleshoot a {
color: var(--accent-color);
text-decoration: none;
}
.svc-detail-troubleshoot a:hover {
text-decoration: underline;
}
/* ── Service detail: Domain configure button ─────────────────────── */
.svc-detail-domain-btn {
margin-top: 12px;
}
/* ── Service detail: Addon feature toggle ────────────────────────── */
.svc-detail-addon-row {
display: flex;
align-items: center;
gap: 14px;
margin-top: 12px;
}
.svc-detail-addon-status {
font-size: 0.88rem;
font-weight: 700;
}
.addon-status--on {
color: var(--green);
}
.addon-status--off {
color: var(--text-dim);
}
.feature-conflict-warning {
margin-top: 8px;
margin-bottom: 8px;
padding: 10px 14px;
background-color: rgba(229, 165, 10, 0.1);
border: 1px solid rgba(229, 165, 10, 0.3);
border-radius: 8px;
font-size: 0.82rem;
color: var(--yellow);
font-weight: 600;
}
.btn-warning {
background-color: #d97706;
color: #fff;
}
.btn-warning:hover:not(:disabled) {
background-color: #b45309;
}
.svc-detail-restart-section {
border-top: 1px solid var(--border-color);
padding-top: 16px;
}
.svc-detail-restart-btn {
margin-top: 8px;
}
.svc-detail-restart-result {
margin-top: 12px;
padding: 12px 16px;
border-radius: 8px;
font-size: 0.88rem;
line-height: 1.5;
display: none;
}
.svc-detail-restart-result.success {
background-color: rgba(109, 191, 139, 0.12);
border: 1px solid var(--green);
color: var(--green);
display: block;
}
.svc-detail-restart-result.error {
background-color: rgba(239, 68, 68, 0.12);
border: 1px solid #ef4444;
color: #f87171;
display: block;
}
/* ── Desktop launch buttons ──────────────────────────────────────── */
.svc-detail-launch-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.svc-detail-launch-btn {
font-size: 0.85rem;
padding: 8px 18px;
cursor: pointer;
}
@@ -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,34 @@
/* 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;
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-base",
"bitcoin-apps",
"communication",
"apps",
"nostr",
];
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,191 @@
"use strict";
// ── Event listeners ───────────────────────────────────────────────
// if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); // moved to sidebar in tiles.js
if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal);
if ($btnReboot) $btnReboot.addEventListener("click", doReboot);
if ($btnSave) $btnSave.addEventListener("click", saveErrorReport);
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 ($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(); });
// ── Upgrade modal functions ───────────────────────────────────────
function openUpgradeModal() {
if ($upgradeModal) $upgradeModal.classList.add("open");
}
function closeUpgradeModal() {
if ($upgradeModal) $upgradeModal.classList.remove("open");
}
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 mainContent = document.querySelector(".main-content");
if (mainContent) {
mainContent.insertAdjacentElement("beforebegin", 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() {
// Check onboarding status first — redirect to wizard if not complete
try {
var onboardingStatus = await apiFetch("/api/onboarding/status");
if (!onboardingStatus.complete) {
window.location.href = "/onboarding";
return;
}
} catch (_) {
// If we can't reach the endpoint, continue to normal dashboard
}
// Show first-login security banner only for machines that went through onboarding
// (legacy machines without the onboarding flag will never see this)
try {
var bannerData = await apiFetch("/api/security/banner-status");
if (bannerData && bannerData.show) {
showSecurityBanner();
}
} catch (_) {
// Non-fatal — silently ignore
}
try {
var cfg = await apiFetch("/api/config");
_currentRole = cfg.role || "server_plus_desktop";
if (cfg.category_order) {
for (var i = 0; i < cfg.category_order.length; i++) {
_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;
await refreshServices();
loadNetwork();
checkUpdates();
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
if (cfg.feature_manager) {
loadFeatureManager();
}
loadAutolaunchToggle();
} catch (_) {
await refreshServices();
loadNetwork();
checkUpdates();
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
loadAutolaunchToggle();
}
}
document.addEventListener("DOMContentLoaded", init);
@@ -0,0 +1,699 @@
"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 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 externalIp = _cachedExternalIp || "your external IP";
$domainSetupBody.innerHTML =
'<div class="domain-setup-intro">' +
'<p><strong>Before continuing:</strong></p>' +
'<ol>' +
'<li>Create an account at <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">https://njal.la</a></li>' +
'<li>Purchase a new domain on Njal.la, or create a subdomain from a domain you already own. Tip: Subdomains are free to create — you only need to purchase one domain, and you can add as many subdomains as you need at no extra cost.</li>' +
'<li>In the Njal.la web interface, create a <strong>Dynamic</strong> record pointing to this machine\'s external IP address:<br>' +
'<span style="display:inline-block;margin-top:4px;padding:4px 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>Njal.la will give you a curl command like:<br>' +
'<code style="font-size:0.8em;">curl &quot;https://njal.la/update/?h=sub.domain.com&amp;k=abc123&amp;auto&quot;</code></li>' +
'<li>Enter the subdomain and paste that curl command below</li>' +
'</ol>' +
'</div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Subdomain (e.g. myservice.example.com):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="myservice.example.com" /></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl &quot;https://njal.la/update/?h=myservice.example.com&amp;k=abc123&amp;auto&quot;" /><p class="domain-field-hint"> Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
npubField +
'<div class="domain-field-actions"><button class="btn btn-close-modal" id="domain-setup-cancel-btn">Cancel</button><button class="btn btn-primary" id="domain-setup-save-btn">Save &amp; Enable</button></div>';
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 & Enable";
alert("Failed to save domain. Please try again.");
}
});
$domainSetupModal.classList.add("open");
}
function openDomainReconfigureModal(feat, existingDomain, onSaved) {
if (!$domainSetupModal) return;
if ($domainSetupTitle) $domainSetupTitle.textContent = "🔄 Reconfigure Domain — " + feat.name;
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 externalIp = _cachedExternalIp || "your external IP";
var currentDomain = existingDomain || "";
$domainSetupBody.innerHTML =
'<div class="domain-setup-intro">' +
'<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></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">Subdomain (e.g. myservice.example.com):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="myservice.example.com" value="' + escHtml(currentDomain) + '" /></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl &quot;https://njal.la/update/?h=myservice.example.com&amp;k=abc123&amp;auto&quot;" /><p class="domain-field-hint"> Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
npubField +
'<div class="domain-field-actions"><button class="btn btn-close-modal" id="domain-setup-cancel-btn">Cancel</button><button class="btn btn-primary" id="domain-setup-save-btn">Save &amp; Update</button></div>';
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";
alert("Failed to save domain. Please try again.");
}
});
$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) {
var rows = ports.map(function(p) {
return '<tr><td class="port-req-port">' + escHtml(p.port) + '</td>' +
'<td class="port-req-proto">' + escHtml(p.protocol) + '</td>' +
'<td class="port-req-desc">' + escHtml(p.description) + '</td></tr>';
}).join("");
var ipLine = internalIp
? '<p class="port-req-intro">Forward each port below <strong>to this machine\'s internal IP: <code class="port-req-internal-ip">' + escHtml(internalIp) + '</code></strong></p>'
: "<p class=\"port-req-intro\">Forward each port below to this machine's internal LAN IP in your router's port forwarding settings.</p>";
$portReqBody.innerHTML =
'<p class="port-req-intro"><strong>Port Forwarding Required</strong></p>' +
'<p class="port-req-intro">For <strong>' + escHtml(featureName) + "</strong> to work with clients outside your local network, " +
"you must configure <strong>port forwarding</strong> in your router's admin panel.</p>" +
ipLine +
'<table class="port-req-table">' +
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Purpose</th></tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>' +
"<p class=\"port-req-hint\"><strong>How to verify:</strong> Router-side forwarding cannot be checked from inside your network. " +
"To confirm ports are forwarded correctly, test from a device on a different network (e.g. a phone on mobile data) " +
"or check your router's port forwarding page.</p>" +
'<p class="port-req-hint"> Search "<em>how to set up port forwarding on [your router model]</em>" for step-by-step instructions.</p>' +
'<div class="domain-field-actions">' +
'<button class="btn btn-close-modal" id="port-req-dismiss-btn">Dismiss</button>' +
continueBtn +
'</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
openFeatureConfirm(
"This will disable " + feat.name + ". The system will rebuild. Continue?",
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;
}
// Check which ports are actually closed before showing the modal
fetch("/api/ports/status", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ports: ports }),
})
.then(function(r) {
if (!r.ok) throw new Error("Port status request failed: " + r.status);
return r.json();
})
.then(function(data) {
var portStatuses = {};
(data.ports || []).forEach(function(p) {
portStatuses[p.port + "/" + p.protocol] = p.status;
});
var closedPorts = ports.filter(function(p) {
var key = p.port + "/" + p.protocol;
var status = portStatuses[key] || "unknown";
return status !== "listening" && status !== "firewall_open";
});
if (closedPorts.length === 0) {
proceedAfterPortCheck();
} else {
openPortRequirementsModal(feat.name, closedPorts, proceedAfterPortCheck);
}
})
.catch(function(err) {
console.warn("Failed to fetch port status for feature enable flow:", err);
// Safe fallback if status check fails
openPortRequirementsModal(feat.name, ports, proceedAfterPortCheck);
});
}
if (conflictNames.length > 0) {
var confirmMsg;
if (feat.id === "bip110") {
confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Knots + BIP110 will disable Bitcoin Core (if active). Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
} else if (feat.id === "bitcoin-core") {
confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Core will disable Bitcoin Knots + BIP110 (if active). Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
} else {
confirmMsg = "This will disable " + conflictNames.join(", ") + ". Continue?";
}
openFeatureConfirm(confirmMsg, proceedAfterConflictCheck);
} 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) {
// Remove existing section if any
var old = $sidebarFeatures.querySelector(".autolaunch-section");
if (old) old.parentNode.removeChild(old);
var section = document.createElement("div");
section.className = "category-section autolaunch-section";
section.innerHTML =
'<div class="section-header">Preferences</div>' +
'<hr class="section-divider" />' +
'<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>';
$sidebarFeatures.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,62 @@
"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>');
}
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 ────────────────────────────────────────────────
async function apiFetch(path, options) {
const res = await fetch(path, options || {});
if (!res.ok) {
let detail = res.status + " " + res.statusText;
try { const body = await res.json(); if (body && body.detail) detail = body.detail; } catch (e) {}
throw new Error(detail);
}
return res.json();
}
@@ -0,0 +1,91 @@
"use strict";
// ── Rebuild modal ─────────────────────────────────────────────────
function openRebuildModal() {
if (!$rebuildModal) return;
_rebuildLog = "";
_rebuildLogOffset = 0;
_rebuildServerDown = false;
_rebuildFinished = false;
if ($rebuildLog) { $rebuildLog.textContent = ""; $rebuildLog.style.display = "none"; }
var action = _rebuildIsEnabling ? "Enabling" : "Disabling";
var label = _rebuildFeatureName || "feature";
if ($rebuildStatus) $rebuildStatus.textContent = 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;
$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() {
pollRebuildStatus();
_rebuildPollTimer = setInterval(pollRebuildStatus, UPDATE_POLL_INTERVAL);
}
function stopRebuildPoll() {
if (_rebuildPollTimer) { clearInterval(_rebuildPollTimer); _rebuildPollTimer = null; }
}
async function pollRebuildStatus() {
if (_rebuildFinished) return;
try {
var data = await apiFetch("/api/rebuild/status?offset=" + _rebuildLogOffset);
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) {
if (!_rebuildServerDown) { _rebuildServerDown = true; if ($rebuildStatus) $rebuildStatus.textContent = "Applying changes…"; }
}
}
function onRebuildDone(result) {
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
if ($rebuildClose) $rebuildClose.disabled = false;
if (result === true) {
if ($rebuildStatus) $rebuildStatus.textContent = "✓ 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") {
if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done — reboot required";
if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex";
} else {
if ($rebuildStatus) $rebuildStatus.textContent = "✗ 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,247 @@
"use strict";
// ── Security Modal ────────────────────────────────────────────────
function openSecurityModal() {
if ($supportModal) $supportModal.classList.add("open");
var title = document.getElementById("support-modal-title");
if (title) title.textContent = "\uD83D\uDEE1 Security";
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 Reboot now (" + countdown + ")";
var timer = setInterval(function() {
countdown--;
if (countdown <= 0) {
clearInterval(timer);
rebootBtn.disabled = false;
rebootBtn.textContent = "I have written down my new password \u2014 Reboot now";
} else {
rebootBtn.textContent = "I have written down my new password \u2014 Reboot now (" + countdown + ")";
}
}, 1000);
rebootBtn.addEventListener("click", function() {
rebootBtn.disabled = true;
rebootBtn.textContent = "Rebooting\u2026";
if ($rebootOverlay) $rebootOverlay.classList.add("visible");
_rebootStartTime = Date.now();
_serverWentDown = false;
setTimeout(waitForServerReboot, REBOOT_INITIAL_DELAY);
var rebootCtrl = new AbortController();
setTimeout(function() { rebootCtrl.abort(); }, REBOOT_REQUEST_TIMEOUT);
fetch("/api/reboot", { method: "POST", signal: rebootCtrl.signal }).catch(function() {});
}, { 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";
});
}
}
}
@@ -0,0 +1,615 @@
"use strict";
// ── Service detail modal ──────────────────────────────────────────
function _renderCredsHtml(credentials, unit) {
var html = "";
for (var i = 0; i < credentials.length; i++) {
var cred = credentials[i];
var id = "cred-" + Math.random().toString(36).substring(2, 8);
var qrBlock = "";
if (cred.qrcode) {
qrBlock = '<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + cred.qrcode + '" alt="QR Code for ' + escHtml(cred.label) + '"><div class="creds-qr-hint">Scan with Zeus app on your phone</div></div>';
}
// If qronly, render the label + QR block only — skip value and copy button
if (cred.qronly) {
html += '<div class="creds-row"><div class="creds-label">' + escHtml(cred.label) + '</div>' + qrBlock + '</div>';
continue;
}
var displayValue = linkify(cred.value);
html += '<div class="creds-row"><div class="creds-label">' + escHtml(cred.label) + '</div>' + qrBlock + '<div class="creds-value-wrap"><div class="creds-value" id="' + id + '">' + displayValue + '</div><button class="creds-copy-btn" data-target="' + id + '">Copy</button></div></div>';
}
return html;
}
function _attachCopyHandlers(container) {
container.querySelectorAll(".creds-copy-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
var target = document.getElementById(btn.dataset.target);
if (!target) return;
var text = target.textContent;
function onSuccess() {
btn.textContent = "Copied!";
btn.classList.add("copied");
setTimeout(function() { btn.textContent = "Copy"; btn.classList.remove("copied"); }, 1500);
}
function fallbackCopy() {
var ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
try {
document.execCommand("copy");
onSuccess();
} catch (e) {}
document.body.removeChild(ta);
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(onSuccess).catch(fallbackCopy);
} else {
fallbackCopy();
}
});
});
}
async function openServiceDetailModal(unit, name, icon) {
if (!$credsModal) return;
if ($credsTitle) {
$credsTitle.innerHTML = '';
if (icon) {
var iconImg = document.createElement("img");
iconImg.className = "creds-title-icon";
iconImg.src = "/static/icons/" + escHtml(icon) + ".svg";
iconImg.alt = name;
iconImg.onerror = function() { this.style.display = "none"; };
$credsTitle.appendChild(iconImg);
}
var nameSpan = document.createElement("span");
nameSpan.textContent = name;
$credsTitle.appendChild(nameSpan);
}
if ($credsBody) $credsBody.innerHTML = '<p class="creds-loading">Loading…</p>';
$credsModal.classList.add("open");
try {
var url = "/api/service-detail/" + encodeURIComponent(unit);
if (icon) url += "?icon=" + encodeURIComponent(icon);
var data = await apiFetch(url);
var html = "";
// Section A: Description
if (data.description) {
html += '<div class="svc-detail-section">' +
'<p class="svc-detail-desc">' + escHtml(data.description) + '</p>' +
'</div>';
}
// Section B: Status
// When a feature override is present, use the feature's enabled state so the
// modal matches what the dashboard tile shows (feature toggle is authoritative).
var effectiveEnabled = data.feature ? data.feature.enabled : data.enabled;
var effectiveHealth = data.feature && !data.feature.enabled
? "disabled"
: (data.health || data.status);
var sc = statusClass(effectiveHealth);
var st = statusText(effectiveHealth, effectiveEnabled);
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Status</div>' +
'<div class="svc-detail-status">' +
'<span class="status-dot ' + sc + '"></span>' +
'<span>' + escHtml(st) + '</span>' +
'</div>' +
'</div>';
// Section C: Domain diagnostics (domain services)
if (data.needs_domain) {
var steps = data.domain_check_steps || [];
var stepsHtml = "";
steps.forEach(function(step) {
var iconLabel = "—";
if (step.status === "ok") iconLabel = "✅";
else if (step.status === "error") iconLabel = "❌";
else if (step.status === "warning") iconLabel = "⚠️";
else if (step.status === "skipped") iconLabel = "⏭️";
var detail = escHtml(step.detail || "").replace(/\n/g, "<br>");
stepsHtml += '<div class="svc-detail-troubleshoot" style="margin-bottom:10px">' +
'<strong>' + iconLabel + ' Step ' + escHtml(String(step.step)) + ': ' + escHtml(step.label || "") + '</strong>' +
(detail ? '<div style="margin-top:6px">' + detail + '</div>' : '') +
'</div>';
});
var domainActionHtml = "";
var ds = data.domain_status || {};
if (!data.domain && data.domain_name) {
domainActionHtml = '<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-config-domain-btn">🌐 Configure Domain</button>';
} else if (data.domain && (ds.status === "dns_mismatch" || ds.status === "unresolvable")) {
domainActionHtml = '<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-reconfig-domain-btn">🔄 Reconfigure Domain</button>';
}
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Domain Diagnostic Checklist</div>' +
stepsHtml +
domainActionHtml +
'</div>';
if (unit === "livekit.service" && data.extra_ports && data.extra_ports.length > 0) {
var extraRows = "";
data.extra_ports.forEach(function(p) {
var statusIcon, statusClass2;
if (p.status === "listening") {
statusIcon = "✅ Open";
statusClass2 = "port-status-listening";
} else if (p.status === "firewall_open") {
statusIcon = "🟡 Firewall open";
statusClass2 = "port-status-open";
} else if (p.status === "closed") {
statusIcon = "❌ Closed";
statusClass2 = "port-status-closed";
} else {
statusIcon = "— Unknown";
statusClass2 = "port-status-unknown";
}
extraRows += '<tr>' +
'<td class="svc-detail-port-table-port">' + escHtml(p.port) + '</td>' +
'<td class="svc-detail-port-table-proto">' + escHtml(p.protocol) + '</td>' +
'<td class="svc-detail-port-table-desc">' + escHtml(p.description || "") + '</td>' +
'<td class="svc-detail-port-table-status ' + statusClass2 + '">' + statusIcon + '</td>' +
'</tr>';
});
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Step 4: Additional Ports</div>' +
'<table class="svc-detail-port-table">' +
'<thead><tr><th>Port</th><th>Protocol</th><th>Description</th><th>Status</th></tr></thead>' +
'<tbody>' + extraRows + '</tbody>' +
'</table>' +
'</div>';
}
} else if (data.port_statuses && data.port_statuses.length > 0) {
// Non-domain services (SSH) keep local single-port checks.
var portTableRows = "";
data.port_statuses.forEach(function(p) {
var statusIcon, statusClass2;
if (p.status === "listening") {
statusIcon = "✅ Open";
statusClass2 = "port-status-listening";
} else if (p.status === "firewall_open") {
statusIcon = "🟡 Firewall open";
statusClass2 = "port-status-open";
} else if (p.status === "closed") {
statusIcon = "🔴 Closed";
statusClass2 = "port-status-closed";
} else {
statusIcon = "— Unknown";
statusClass2 = "port-status-unknown";
}
portTableRows += '<tr>' +
'<td class="svc-detail-port-table-port">' + escHtml(p.port) + '</td>' +
'<td class="svc-detail-port-table-proto">' + escHtml(p.protocol) + '</td>' +
'<td class="svc-detail-port-table-desc">' + escHtml(p.description || "") + '</td>' +
'<td class="svc-detail-port-table-status ' + statusClass2 + '">' + statusIcon + '</td>' +
'</tr>';
});
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Port Status</div>' +
'<table class="svc-detail-port-table">' +
'<thead><tr><th>Port</th><th>Protocol</th><th>Description</th><th>Status</th></tr></thead>' +
'<tbody>' + portTableRows + '</tbody>' +
'</table>' +
'</div>';
}
// Section E: Credentials & Links
if (data.has_credentials && data.credentials && data.credentials.length > 0) {
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Credentials &amp; Access</div>' +
_renderCredsHtml(data.credentials, unit) +
(unit === "matrix-synapse.service" ?
'<hr class="matrix-actions-divider"><div class="matrix-actions-row">' +
'<button class="matrix-action-btn" id="matrix-add-user-btn"> Add New User</button>' +
'<button class="matrix-action-btn" id="matrix-change-pw-btn">🔑 Change Password</button>' +
'</div>' : "") +
(unit === "root-password-setup.service" ?
'<hr class="matrix-actions-divider"><div class="matrix-actions-row">' +
'<button class="matrix-action-btn" id="sys-change-pw-btn">🔑 Change Free Account Password</button>' +
'</div>' : "") +
'</div>';
} else if (!data.enabled && !data.feature) {
html += '<div class="svc-detail-section">' +
'<p class="creds-empty">This service is not enabled in your configuration.</p>' +
'</div>';
}
// Section F: Addon Feature toggle
if (data.feature) {
var feat = data.feature;
// Sync this feature into _featuresData so handleFeatureToggle can look up conflicts / ssl state
if (!_featuresData) {
_featuresData = { features: [feat], ssl_email_configured: false };
} else {
var fidx = _featuresData.features.findIndex(function(f) { return f.id === feat.id; });
if (fidx >= 0) { _featuresData.features[fidx] = feat; }
else { _featuresData.features.push(feat); }
}
var addonStatusLabel = feat.enabled ? "Enabled \u2713" : "Disabled";
var addonStatusCls = feat.enabled ? "addon-status--on" : "addon-status--off";
var addonBtnLabel = feat.enabled ? "Disable Feature" : "Enable Feature";
var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary";
// Section title: use a more specific label for mutually-exclusive Bitcoin node features
var addonSectionTitle = (feat.id === "bip110" || feat.id === "bitcoin-core")
? "\u20BF Bitcoin Node Selection"
: "\uD83D\uDD27 Addon Feature";
// Description: prefer the feature's own description over a generic fallback
var addonDesc = feat.description
? feat.description
: "This is an optional addon feature. You can enable or disable it at any time.";
// Conflicts warning: list mutually-exclusive feature names when present
var conflictsHtml = "";
if (feat.conflicts_with && feat.conflicts_with.length > 0) {
var conflictNames = feat.conflicts_with.map(function(cid) {
if (_featuresData && Array.isArray(_featuresData.features)) {
var cf = _featuresData.features.find(function(f) { return f.id === cid; });
if (cf) return cf.name;
}
return cid;
});
conflictsHtml = '<div class="feature-conflict-warning">\u26A0 Mutually exclusive with: ' + escHtml(conflictNames.join(", ")) + '</div>';
}
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">' + addonSectionTitle + '</div>' +
'<p class="svc-detail-desc">' + escHtml(addonDesc) + '</p>' +
conflictsHtml +
'<div class="svc-detail-addon-row">' +
'<span class="svc-detail-addon-status ' + addonStatusCls + '">' + addonStatusLabel + '</span>' +
'<button class="' + addonBtnCls + '" id="svc-detail-addon-btn">' + escHtml(addonBtnLabel) + '</button>' +
'</div>' +
'</div>';
}
if ((effectiveEnabled || data.enabled) && unit !== "phpfpm-nextcloud.service" && unit !== "phpfpm-wordpress.service") {
html += '<div class="svc-detail-section svc-detail-restart-section">' +
'<div class="svc-detail-section-title">Troubleshooting</div>' +
'<p class="svc-detail-desc">If you\'re experiencing issues with this service, try restarting it.</p>' +
'<button class="btn btn-warning svc-detail-restart-btn" id="svc-detail-restart-btn">🔄 Restart Service</button>' +
'<div class="svc-detail-restart-result" id="svc-detail-restart-result"></div>' +
'</div>';
}
$credsBody.innerHTML = html;
_attachCopyHandlers($credsBody);
if (unit === "matrix-synapse.service") {
var addBtn = document.getElementById("matrix-add-user-btn");
var changePwBtn = document.getElementById("matrix-change-pw-btn");
if (addBtn) addBtn.addEventListener("click", function() { openMatrixCreateUserModal(unit, name, icon); });
if (changePwBtn) changePwBtn.addEventListener("click", function() { openMatrixChangePasswordModal(unit, name, icon); });
}
if (unit === "root-password-setup.service") {
var sysPwBtn = document.getElementById("sys-change-pw-btn");
if (sysPwBtn) sysPwBtn.addEventListener("click", function() { openSystemChangePasswordModal(unit, name, icon); });
}
if (data.feature) {
var addonBtn = document.getElementById("svc-detail-addon-btn");
if (addonBtn) {
var addonFeat = data.feature;
addonBtn.addEventListener("click", function() {
closeCredsModal();
handleFeatureToggle(addonFeat, !addonFeat.enabled);
});
}
}
var restartBtn = document.getElementById("svc-detail-restart-btn");
var restartResult = document.getElementById("svc-detail-restart-result");
if (restartBtn && restartResult) {
var RESTART_REFRESH_DELAY_MS = 3000;
restartBtn.addEventListener("click", async function() {
restartBtn.disabled = true;
restartBtn.textContent = "Restarting…";
restartResult.className = "svc-detail-restart-result";
restartResult.textContent = "";
try {
await apiFetch("/api/service/" + encodeURIComponent(unit) + "/restart", { method: "POST" });
restartResult.classList.add("success");
restartResult.textContent = "✅ Service restarted successfully.";
restartBtn.disabled = false;
restartBtn.textContent = "🔄 Restart Service";
setTimeout(function() {
openServiceDetailModal(unit, name, icon);
}, RESTART_REFRESH_DELAY_MS);
} catch (e) {
restartResult.classList.add("error");
restartResult.textContent = e && e.message ? e.message : "Failed to restart service. Please check service logs and try again.";
restartBtn.disabled = false;
restartBtn.textContent = "🔄 Restart Service";
}
});
}
// Configure / Reconfigure Domain buttons (for non-feature services that need a domain)
var configDomainBtn = document.getElementById("svc-detail-config-domain-btn");
var reconfigDomainBtn = document.getElementById("svc-detail-reconfig-domain-btn");
if ((configDomainBtn || reconfigDomainBtn) && data.needs_domain && data.domain_name) {
var pseudoFeat = {
id: data.domain_name,
name: name,
domain_name: data.domain_name,
needs_ddns: true,
extra_fields: []
};
if (configDomainBtn) configDomainBtn.addEventListener("click", function() {
closeCredsModal();
openDomainSetupModal(pseudoFeat, function() {
openServiceDetailModal(unit, name, icon);
});
});
if (reconfigDomainBtn) reconfigDomainBtn.addEventListener("click", function() {
closeCredsModal();
openDomainReconfigureModal(pseudoFeat, data.domain || "", function() {
openServiceDetailModal(unit, name, icon);
});
});
}
} catch (err) {
if ($credsBody) $credsBody.innerHTML = '<p class="creds-empty">Could not load service details.</p>';
}
}
// ── Credentials info modal ────────────────────────────────────────
async function openCredsModal(unit, name, icon) {
if (!$credsModal) return;
if ($credsTitle) {
$credsTitle.innerHTML = '';
if (icon) {
var iconImg = document.createElement("img");
iconImg.className = "creds-title-icon";
iconImg.src = "/static/icons/" + escHtml(icon) + ".svg";
iconImg.alt = name;
iconImg.onerror = function() { this.style.display = "none"; };
$credsTitle.appendChild(iconImg);
}
var nameSpan = document.createElement("span");
nameSpan.textContent = name + " — Connection Info";
$credsTitle.appendChild(nameSpan);
}
if ($credsBody) $credsBody.innerHTML = '<p class="creds-loading">Loading…</p>';
$credsModal.classList.add("open");
try {
var data = await apiFetch("/api/credentials/" + encodeURIComponent(unit));
if (!data.credentials || data.credentials.length === 0) {
$credsBody.innerHTML = '<p class="creds-empty">No connection info available yet.</p>';
return;
}
var html = _renderCredsHtml(data.credentials, unit);
if (unit === "matrix-synapse.service") {
html += '<hr class="matrix-actions-divider"><div class="matrix-actions-row">' +
'<button class="matrix-action-btn" id="matrix-add-user-btn"> Add New User</button>' +
'<button class="matrix-action-btn" id="matrix-change-pw-btn">🔑 Change Password</button>' +
'</div>';
}
$credsBody.innerHTML = html;
_attachCopyHandlers($credsBody);
if (unit === "matrix-synapse.service") {
var addBtn = document.getElementById("matrix-add-user-btn");
var changePwBtn = document.getElementById("matrix-change-pw-btn");
if (addBtn) addBtn.addEventListener("click", function() { openMatrixCreateUserModal(unit, name); });
if (changePwBtn) changePwBtn.addEventListener("click", function() { openMatrixChangePasswordModal(unit, name); });
}
} catch (err) {
$credsBody.innerHTML = '<p class="creds-empty">Could not load credentials.</p>';
}
}
function openMatrixCreateUserModal(unit, name, icon) {
if (!$credsBody) return;
$credsBody.innerHTML =
'<div class="matrix-form-group"><label class="matrix-form-label" for="matrix-new-username">Username</label>' +
'<input class="matrix-form-input" type="text" id="matrix-new-username" placeholder="alice" autocomplete="off"></div>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="matrix-new-password">Password</label>' +
'<input class="matrix-form-input" type="password" id="matrix-new-password" placeholder="Strong password" autocomplete="new-password"></div>' +
'<div class="matrix-form-checkbox-row"><input type="checkbox" id="matrix-new-admin"><label class="matrix-form-label" for="matrix-new-admin" style="margin:0">Make admin</label></div>' +
'<div class="matrix-form-actions">' +
'<button class="matrix-form-back" id="matrix-create-back-btn">← Back</button>' +
'<button class="matrix-form-submit" id="matrix-create-submit-btn">Create User</button>' +
'</div>' +
'<div class="matrix-form-result" id="matrix-create-result"></div>';
document.getElementById("matrix-create-back-btn").addEventListener("click", function() {
openServiceDetailModal(unit, name, icon);
});
document.getElementById("matrix-create-submit-btn").addEventListener("click", async function() {
var submitBtn = document.getElementById("matrix-create-submit-btn");
var resultEl = document.getElementById("matrix-create-result");
var username = (document.getElementById("matrix-new-username").value || "").trim();
var password = document.getElementById("matrix-new-password").value || "";
var isAdmin = document.getElementById("matrix-new-admin").checked;
if (!username || !password) {
resultEl.className = "matrix-form-result error";
resultEl.textContent = "Username and password are required.";
return;
}
submitBtn.disabled = true;
submitBtn.textContent = "Creating…";
resultEl.className = "matrix-form-result";
resultEl.textContent = "";
try {
var resp = await apiFetch("/api/matrix/create-user", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: username, password: password, admin: isAdmin })
});
resultEl.className = "matrix-form-result success";
resultEl.textContent = "✅ User @" + escHtml(resp.username) + " created successfully.";
submitBtn.textContent = "Create User";
submitBtn.disabled = false;
} catch (err) {
resultEl.className = "matrix-form-result error";
resultEl.textContent = "❌ " + (err.message || "Failed to create user.");
submitBtn.textContent = "Create User";
submitBtn.disabled = false;
}
});
}
function openMatrixChangePasswordModal(unit, name, icon) {
if (!$credsBody) return;
$credsBody.innerHTML =
'<div class="matrix-form-group"><label class="matrix-form-label" for="matrix-chpw-username">Username (localpart only, e.g. <em>alice</em>)</label>' +
'<input class="matrix-form-input" type="text" id="matrix-chpw-username" placeholder="alice" autocomplete="off"></div>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="matrix-chpw-password">New Password</label>' +
'<input class="matrix-form-input" type="password" id="matrix-chpw-password" placeholder="New strong password" autocomplete="new-password"></div>' +
'<div class="matrix-form-actions">' +
'<button class="matrix-form-back" id="matrix-chpw-back-btn">← Back</button>' +
'<button class="matrix-form-submit" id="matrix-chpw-submit-btn">Change Password</button>' +
'</div>' +
'<div class="matrix-form-result" id="matrix-chpw-result"></div>';
document.getElementById("matrix-chpw-back-btn").addEventListener("click", function() {
openServiceDetailModal(unit, name, icon);
});
document.getElementById("matrix-chpw-submit-btn").addEventListener("click", async function() {
var submitBtn = document.getElementById("matrix-chpw-submit-btn");
var resultEl = document.getElementById("matrix-chpw-result");
var username = (document.getElementById("matrix-chpw-username").value || "").trim();
var newPassword = document.getElementById("matrix-chpw-password").value || "";
if (!username || !newPassword) {
resultEl.className = "matrix-form-result error";
resultEl.textContent = "Username and new password are required.";
return;
}
submitBtn.disabled = true;
submitBtn.textContent = "Changing…";
resultEl.className = "matrix-form-result";
resultEl.textContent = "";
try {
var resp = await apiFetch("/api/matrix/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: username, new_password: newPassword })
});
resultEl.className = "matrix-form-result success";
resultEl.textContent = "✅ Password for @" + escHtml(resp.username) + " changed successfully.";
submitBtn.textContent = "Change Password";
submitBtn.disabled = false;
} catch (err) {
resultEl.className = "matrix-form-result error";
resultEl.textContent = "❌ " + (err.message || "Failed to change password.");
submitBtn.textContent = "Change Password";
submitBtn.disabled = false;
}
});
}
function openSystemChangePasswordModal(unit, name, icon) {
if (!$credsBody) return;
$credsBody.innerHTML =
'<div class="sys-chpw-header">' +
'<div class="sys-chpw-title">🔑 Change Free Account &amp; Hub Login Password</div>' +
'<div class="sys-chpw-desc">This updates the password for the <strong>free</strong> user account. <strong>This is also your Sovran Hub login password</strong> — both will change.</div>' +
'</div>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="sys-chpw-new">New Password</label>' +
'<div class="pw-input-wrap">' +
'<input class="matrix-form-input" type="password" id="sys-chpw-new" placeholder="New strong password" autocomplete="new-password">' +
'<button type="button" class="pw-toggle-btn" id="sys-chpw-new-toggle" aria-label="Toggle password visibility">👁</button>' +
'</div>' +
'<div class="pw-hint">Password must be at least 8 characters.</div></div>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="sys-chpw-confirm">Confirm Password</label>' +
'<div class="pw-input-wrap">' +
'<input class="matrix-form-input" type="password" id="sys-chpw-confirm" placeholder="Confirm new password" autocomplete="new-password">' +
'<button type="button" class="pw-toggle-btn" id="sys-chpw-confirm-toggle" aria-label="Toggle password visibility">👁</button>' +
'</div></div>' +
'<div class="pw-credentials-note">⚠ This will change both your desktop login and Hub login password. After changing, your updated password will appear in the System Passwords credentials tile.</div>' +
'<div class="matrix-form-actions">' +
'<button class="matrix-form-back" id="sys-chpw-back-btn">← Back</button>' +
'<button class="matrix-form-submit" id="sys-chpw-submit-btn">Change Password</button>' +
'</div>' +
'<div class="matrix-form-result" id="sys-chpw-result"></div>';
document.getElementById("sys-chpw-back-btn").addEventListener("click", function() {
openServiceDetailModal(unit, name, icon);
});
document.getElementById("sys-chpw-new-toggle").addEventListener("click", function() {
var inp = document.getElementById("sys-chpw-new");
var isHidden = inp.type === "password";
inp.type = isHidden ? "text" : "password";
this.textContent = isHidden ? "👁‍🗨" : "👁";
});
document.getElementById("sys-chpw-confirm-toggle").addEventListener("click", function() {
var inp = document.getElementById("sys-chpw-confirm");
var isHidden = inp.type === "password";
inp.type = isHidden ? "text" : "password";
this.textContent = isHidden ? "👁‍🗨" : "👁";
});
document.getElementById("sys-chpw-submit-btn").addEventListener("click", async function() {
var submitBtn = document.getElementById("sys-chpw-submit-btn");
var resultEl = document.getElementById("sys-chpw-result");
var newPassword = document.getElementById("sys-chpw-new").value || "";
var confirmPassword = document.getElementById("sys-chpw-confirm").value || "";
if (!newPassword || !confirmPassword) {
resultEl.className = "matrix-form-result error";
resultEl.textContent = "Both password fields are required.";
return;
}
if (newPassword.length < 8) {
resultEl.className = "matrix-form-result error";
resultEl.textContent = "Password must be at least 8 characters.";
return;
}
if (newPassword !== confirmPassword) {
resultEl.className = "matrix-form-result error";
resultEl.textContent = "Passwords do not match.";
return;
}
submitBtn.disabled = true;
submitBtn.textContent = "Changing…";
resultEl.className = "matrix-form-result";
resultEl.textContent = "";
try {
await apiFetch("/api/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ new_password: newPassword, confirm_password: confirmPassword })
});
resultEl.className = "matrix-form-result success";
resultEl.textContent = "✅ Free account & Hub login password changed successfully.";
submitBtn.textContent = "Change Password";
submitBtn.disabled = false;
} catch (err) {
resultEl.className = "matrix-form-result error";
resultEl.textContent = "❌ " + (err.message || "Failed to change password.");
submitBtn.textContent = "Change Password";
submitBtn.disabled = false;
}
});
}
function closeCredsModal() { if ($credsModal) $credsModal.classList.remove("open"); }
+105
View File
@@ -0,0 +1,105 @@
"use strict";
// ── State ─────────────────────────────────────────────────────────
let _servicesCache = [];
let _categoryLabels = {};
let _updateLog = "";
let _updatePollTimer = null;
let _updateLogOffset = 0;
let _serverWasDown = false;
let _updateFinished = false;
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 _rebuildFinished = false;
let _rebuildServerDown = false;
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 $btnCloseModal = document.getElementById("btn-close-modal");
const $rebootOverlay = document.getElementById("reboot-overlay");
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");
// 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");
// System status banner
// (removed — health is now shown per-tile via the composite health field)
@@ -0,0 +1,630 @@
"use strict";
// ── Tech Support modal ────────────────────────────────────────────
async function openSupportModal() {
if (!$supportModal) return;
$supportModal.classList.add("open");
$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>Our SSH key is added only to that restricted account</li>',
'<li>Wallet files are locked via access controls — not visible to support</li>',
'<li>You control if and when wallet access is granted (time-limited)</li>',
'<li>All session events are logged for your audit</li>',
'</ol></div>',
'<button class="btn support-btn-enable" id="btn-support-enable">Enable Support Access</button>',
'<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");
if (btn) { btn.disabled = true; btn.textContent = "Enabling…"; }
try {
await apiFetch("/api/support/enable", { method: "POST" });
var status = await apiFetch("/api/support/status");
_supportStatus = status;
_supportEnabledAt = status.enabled_at;
renderSupportActive(status);
} catch (err) {
if (btn) { btn.disabled = false; btn.textContent = "Enable Support Access"; }
alert("Failed to enable support access. Please try again.");
}
}
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("");
}
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">\ud83d\udcbe</div>',
'<h3 class="support-heading">Manual Backup</h3>',
'<div class="support-wallet-box support-wallet-protected" style="margin-bottom:16px;">',
'<p class="support-wallet-desc">',
'Your Sovran Pro already backs up your data automatically to its internal second drive. ',
'This manual backup lets you create an additional copy on an external USB drive \u2014 ',
'storing your data in a third location, outside the computer, for maximum protection ',
'against hardware failure or physical damage.',
'</p>',
'</div>',
'<div class="support-steps">',
'<div class="support-steps-title">Requirements</div>',
'<ol class="support-backup-steps">',
'<li>USB hard drive plugged into one of the open USB ports on your Sovran Pro</li>',
'<li>At least 500 GB of free space on the drive</li>',
'<li>Drive must be formatted as <strong>exFAT</strong></li>',
'</ol>',
'</div>',
'<div class="support-steps">',
'<div class="support-steps-title">What gets backed up</div>',
'<ol class="support-backup-steps">',
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
'<li>nix-bitcoin secrets (<code>/etc/nix-bitcoin-secrets</code>)</li>',
'<li>System service data (<code>/var/lib</code>) including Vaultwarden, bitcoind, LND, sovran-hub, domains, and secrets</li>',
'<li>Home directory (<code>/home</code>)</li>',
'</ol>',
'</div>',
'<div class="support-wallet-box support-wallet-warning">',
'<div class="support-wallet-header">',
'<span class="support-wallet-icon">\u23f1\ufe0f</span>',
'<span class="support-wallet-title">Time Estimate</span>',
'</div>',
'<p class="support-wallet-desc">This backup can take <strong>up to 4 hours</strong> depending on the amount of data stored on your Sovran Pro and the speed of your external hard drive. Be patient\u2026</p>',
'</div>',
driveSelector,
'</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;
if (!data.running) {
stopBackupPoll();
renderBackupDone(data.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 exFAT. Then try again.</p>',
'<div class="modal-log" id="backup-log-fail" style="text-align:left;"></div>',
'<button class="btn support-btn-done" id="btn-backup-close">Close</button>',
'</div>',
].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);
}
+318
View File
@@ -0,0 +1,318 @@
"use strict";
// ── Bitcoin IBD sync state (for ETA calculation) ──────────────────
// Keyed by tileId: { progress: float, timestamp: ms }
var _btcSyncPrev = {};
// ── 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 (!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>';
}
}
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 =
'<img class="sidebar-support-icon" src="/static/icons/update.svg" alt="Update" style="width:1.5rem;height:1.5rem;">' +
'<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);
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">🛟</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">💾</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">\uD83D\uDEE1</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">🚀</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);
var ver = svc.version || svc.bitcoin_version || '';
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : '';
tile.innerHTML =
'<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'">' +
'<div class="tile-icon-fallback" style="display:none">?</div>' +
'<div class="tile-name">' + escHtml(svc.name) + '</div>' +
versionLabel +
'<div class="tile-sync-container">' +
'<div class="tile-sync-label">\u23F3 Syncing Timechain</div>' +
'<div class="tile-sync-bar-row">' +
'<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;
}
var ver = svc.version || svc.bitcoin_version || '';
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : '';
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + versionLabel + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
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;
// Update or insert version label
var syncVer = svc.version || svc.bitcoin_version || '';
if (syncVer) {
var syncVerEl = tile.querySelector(".tile-version");
if (syncVerEl) {
syncVerEl.textContent = syncVer;
} else {
var syncNameEl = tile.querySelector(".tile-name");
if (syncNameEl) {
var newSyncVerEl = document.createElement("div");
newSyncVerEl.className = "tile-version";
newSyncVerEl.textContent = syncVer;
syncNameEl.insertAdjacentElement("afterend", newSyncVerEl);
}
}
}
} else {
// IBD finished or not syncing — if tile had sync layout rebuild it normally
if (tile.querySelector(".tile-sync-container")) {
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;
// Update or insert version label for all service tiles
var tileVer = svc.version || svc.bitcoin_version || '';
if (tileVer) {
var verEl = tile.querySelector(".tile-version");
if (verEl) {
verEl.textContent = tileVer;
} else {
var nameEl = tile.querySelector(".tile-name");
if (nameEl) {
var newVerEl = document.createElement("div");
newVerEl.className = "tile-version";
newVerEl.textContent = tileVer;
nameEl.insertAdjacentElement("afterend", newVerEl);
}
}
}
}
}
}
// ── 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); }
} 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 ──────────────────────────────────────────────────
async function checkUpdates() {
try {
var data = await apiFetch("/api/updates/check");
var hasUpdates = !!data.available;
var sidebarUpdateBtn = document.getElementById("sidebar-btn-update");
var sidebarUpdateHint = document.getElementById("sidebar-update-hint");
if (sidebarUpdateBtn) {
if (hasUpdates) {
sidebarUpdateBtn.style.borderColor = "#2ec27e";
sidebarUpdateBtn.style.backgroundColor = "rgba(46, 194, 126, 0.08)";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Updates available!";
} else {
sidebarUpdateBtn.style.borderColor = "";
sidebarUpdateBtn.style.backgroundColor = "";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "System is up to date";
}
}
} catch (_) {}
}
@@ -0,0 +1,222 @@
"use strict";
// ── Update modal ──────────────────────────────────────────────────
function openUpdateModal() {
if (!$modal) return;
apiFetch("/api/updates/check")
.then(function(data) {
if (!data.available) {
stopUpdatePoll();
_updateLog = "";
_updateLogOffset = 0;
_updateFinished = true;
if ($modalLog) $modalLog.textContent = "";
if ($modalStatus) $modalStatus.textContent = "✓ 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 ($btnCloseModal) $btnCloseModal.disabled = false;
$modal.classList.add("open");
return;
}
_doOpenUpdateModal();
})
.catch(function() {
_doOpenUpdateModal();
});
}
function _doOpenUpdateModal() {
if (!$modal) return;
_updateLog = "";
_updateLogOffset = 0;
_serverWasDown = false;
_updateFinished = false;
if ($modalLog) $modalLog.textContent = "";
if ($modalStatus) $modalStatus.textContent = "Starting update…";
if ($modalSpinner) $modalSpinner.classList.add("spinning");
if ($btnReboot) $btnReboot.style.display = "none";
if ($btnSave) $btnSave.style.display = "none";
if ($btnCloseModal) $btnCloseModal.disabled = true;
$modal.classList.add("open");
startUpdate();
}
function closeUpdateModal() {
if (!$modal) return;
$modal.classList.remove("open");
stopUpdatePoll();
}
function appendLog(text) {
if (!text) return;
_updateLog += text;
if ($modalLog) { $modalLog.textContent += text; $modalLog.scrollTop = $modalLog.scrollHeight; }
}
function startUpdate() {
fetch("/api/updates/run", { method: "POST" })
.then(function(response) {
if (!response.ok) return response.text().then(function(t) { throw new Error(t); });
return response.json();
})
.then(function(data) {
if (data.status === "no_updates") {
if ($modalStatus) $modalStatus.textContent = "✓ 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 ($btnCloseModal) $btnCloseModal.disabled = false;
_updateFinished = true;
return;
}
if (data.status === "already_running") appendLog("[Update already in progress, attaching…]\n\n");
if ($modalStatus) $modalStatus.textContent = "Updating…";
startUpdatePoll();
})
.catch(function(err) {
appendLog("[Error: failed to start update — " + err + "]\n");
onUpdateDone(false);
});
}
function startUpdatePoll() {
pollUpdateStatus();
_updatePollTimer = setInterval(pollUpdateStatus, UPDATE_POLL_INTERVAL);
}
function stopUpdatePoll() {
if (_updatePollTimer) { clearInterval(_updatePollTimer); _updatePollTimer = null; }
}
async function pollUpdateStatus() {
if (_updateFinished) return;
try {
var data = await apiFetch("/api/updates/status?offset=" + _updateLogOffset);
if (_serverWasDown) {
_serverWasDown = false;
if (!data.running) {
// The update finished while the server was restarting. Reset to
// offset 0 and re-fetch so the complete log is shown from the top.
_updateLog = "";
_updateLogOffset = 0;
if ($modalLog) $modalLog.textContent = "";
try {
var fullData = await apiFetch("/api/updates/status?offset=0");
if (fullData.log) appendLog(fullData.log);
_updateLogOffset = fullData.offset;
} catch (e) {
// If the re-fetch fails, fall through with whatever we have.
if (data.log) appendLog(data.log);
_updateLogOffset = data.offset;
}
if (data.result === "reboot_required") {
appendLog("[Server restarted — update completed, reboot required.]\n");
} else if (data.result === "success") {
appendLog("[Server restarted — update completed successfully.]\n");
} else {
appendLog("[Server restarted — update encountered an error.]\n");
}
_updateFinished = true;
stopUpdatePoll();
if (data.result === "reboot_required") {
onUpdateDone("reboot_required");
} else {
onUpdateDone(data.result === "success");
}
return;
}
appendLog("[Server reconnected]\n");
if ($modalStatus) $modalStatus.textContent = "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) {
if (!_serverWasDown) { _serverWasDown = true; appendLog("\n[Server restarting — waiting for it to come back…]\n"); if ($modalStatus) $modalStatus.textContent = "Server restarting…"; }
}
}
function onUpdateDone(result) {
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
if ($btnCloseModal) $btnCloseModal.disabled = false;
if (result === true) {
if ($modalStatus) $modalStatus.textContent = "✓ Update complete";
if ($btnReboot) $btnReboot.style.display = "inline-flex";
} else if (result === "reboot_required") {
if ($modalStatus) $modalStatus.textContent = "✓ Update complete — reboot required";
if ($btnReboot) $btnReboot.style.display = "inline-flex";
} else {
if ($modalStatus) $modalStatus.textContent = "✗ Update failed";
if ($btnSave) $btnSave.style.display = "inline-flex";
if ($btnReboot) $btnReboot.style.display = "inline-flex";
}
}
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;
function doReboot() {
if ($modal) $modal.classList.remove("open");
if ($rebuildModal) $rebuildModal.classList.remove("open");
stopUpdatePoll();
stopRebuildPoll();
if ($rebootOverlay) $rebootOverlay.classList.add("visible");
_rebootStartTime = Date.now();
_serverWentDown = false;
var rebootCtrl = new AbortController();
setTimeout(function() { rebootCtrl.abort(); }, REBOOT_REQUEST_TIMEOUT);
fetch("/api/reboot", { method: "POST", signal: rebootCtrl.signal }).catch(function() {});
// Wait before the first check — NixOS shutdown after an update can take 20-40s
setTimeout(waitForServerReboot, REBOOT_INITIAL_DELAY);
}
function waitForServerReboot() {
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.
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
window.location.reload();
}
})
.catch(function() {
clearTimeout(timeoutId);
_serverWentDown = true;
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,703 @@
/* Sovran_SystemsOS Hub — First-Boot Onboarding Wizard
Drives the 5-step post-install setup flow. */
"use strict";
// ── Constants ─────────────────────────────────────────────────────
const TOTAL_STEPS = 5;
// Steps to skip per role (steps 3 and 4 involve domain/port setup)
// Step 2 (timezone/locale) is NEVER skipped — all roles need it.
const ROLE_SKIP_STEPS = {
"desktop": [3, 4],
"node": [3, 4],
};
// ── 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 updateStep5Checklist() {
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();
if (step === 4) loadStep4();
// Step 5 (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, domains, and network info in parallel
var results = await Promise.all([
apiFetch("/api/services"),
apiFetch("/api/domains/status"),
apiFetch("/api/network"),
]);
_servicesData = results[0];
_domainsData = results[1];
var networkData = results[2];
} catch (err) {
body.innerHTML = '<p class="onboarding-error">⚠ Could not load service data: ' + escHtml(err.message) + '</p>';
return;
}
var externalIp = (networkData && networkData.external_ip) || "Unknown (could not retrieve)";
// 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 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 {
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
+ '<strong>Before you continue:</strong>'
+ '<ol style="margin:8px 0 0 16px; padding:0; line-height:1.7;">'
+ '<li>Create an account at <a href="https://njal.la" target="_blank" style="color:var(--accent-color);">https://njal.la</a></li>'
+ '<li>Purchase a new domain on Njal.la, or create a subdomain from a domain you already own. Tip: Subdomains are free to create — you only need to purchase one domain, and you can add as many subdomains as you need at no extra cost.</li>'
+ '<li>In the Njal.la web interface, create a <strong>Dynamic</strong> record pointing to this machine\'s external IP address:<br>'
+ '<span style="display:inline-block;margin-top:4px;padding:4px 12px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1.1em;font-weight:700;letter-spacing:0.03em;">' + escHtml(externalIp) + '</span></li>'
+ '<li>Njal.la will give you a curl command like:<br>'
+ '<code style="font-size:0.8em;">curl "https://njal.la/update/?h=sub.domain.com&amp;k=abc123&amp;auto"</code></li>'
+ '<li>Enter the subdomain and paste that curl command below for each service</li>'
+ '</ol>'
+ '</div>';
html += '<p class="onboarding-hint">Enter each fully-qualified subdomain (e.g. <code>matrix.yourdomain.com</code>) and its Njal.la DDNS curl command.</p>';
relevantDomains.forEach(function(d) {
var currentVal = (_domainsData && _domainsData[d.name]) || "";
html += '<div class="onboarding-domain-group">';
html += '<label class="onboarding-domain-label">' + escHtml(d.label) + '</label>';
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="domain-input-' + escHtml(d.name) + '" data-domain="' + escHtml(d.name) + '" placeholder="e.g. ' + escHtml(d.name) + '.yourdomain.com" value="' + escHtml(currentVal) + '" />';
html += '<label class="onboarding-domain-label onboarding-domain-label--sub">Njal.la DDNS Curl Command</label>';
html += '<input class="onboarding-domain-input domain-field-input" type="text" id="ddns-input-' + escHtml(d.name) + '" data-ddns="' + escHtml(d.name) + '" placeholder="curl &quot;https://njal.la/update/?h=' + escHtml(d.name) + '.yourdomain.com&amp;k=abc123&amp;auto&quot;" />';
html += '<p class="onboarding-hint" style="margin-top:4px;"> Paste the curl URL from your Njal.la dashboard\'s Dynamic record</p>';
html += '<button type="button" class="btn btn-primary onboarding-domain-save-btn" data-save-domain="' + escHtml(d.name) + '" style="align-self:flex-start;margin-top:8px;font-size:0.82rem;padding:6px 16px;">Save</button>';
html += '<span class="onboarding-domain-save-status" id="domain-save-status-' + escHtml(d.name) + '" style="font-size:0.82rem;min-height:1.2em;"></span>';
html += '</div>';
});
}
// SSL email section
var emailVal = (_domainsData && _domainsData["sslemail"]) || "";
html += '<div class="onboarding-domain-group onboarding-domain-group--email">';
html += '<label class="onboarding-domain-label">📧 SSL Certificate Email</label>';
html += '<p class="onboarding-hint onboarding-hint--inline">Let\'s Encrypt uses this for certificate expiry notifications.</p>';
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: Port Forwarding ───────────────────────────────────────
async function loadStep4() {
var body = document.getElementById("step-4-body");
if (!body) return;
body.innerHTML = '<p class="onboarding-loading">Checking ports…</p>';
var networkData = null;
try {
networkData = await apiFetch("/api/network");
} catch (err) {
body.innerHTML = '<p class="onboarding-error">⚠ Could not load network data: ' + escHtml(err.message) + '</p>';
return;
}
var internalIp = (networkData && networkData.internal_ip) || "unknown";
var ip = escHtml(internalIp);
var html = '<p class="onboarding-port-note" style="margin-bottom:14px;">'
+ '⚠ <strong>Each port only needs to be forwarded once — all services share the same ports.</strong>'
+ '</p>';
html += '<div class="onboarding-port-ip">';
html += ' <span class="onboarding-port-ip-label">Forward ports to this machine\'s internal IP:</span>';
html += ' <span class="port-req-internal-ip">' + ip + '</span>';
html += '</div>';
// Required ports table
html += '<div class="onboarding-port-section" style="margin-bottom:20px;">';
html += '<div class="onboarding-port-section-title" style="font-weight:700;margin-bottom:8px;">Required Ports — open these on your router:</div>';
html += '<table class="onboarding-port-table">';
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward&nbsp;to</th><th>Purpose</th></tr></thead>';
html += '<tbody>';
html += '<tr><td class="port-req-port">80</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">HTTP</td></tr>';
html += '<tr><td class="port-req-port">443</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">HTTPS</td></tr>';
html += '<tr><td class="port-req-port">22</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">SSH Remote Access</td></tr>';
html += '<tr><td class="port-req-port">8448</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">Matrix Federation</td></tr>';
html += '</tbody></table>';
html += '</div>';
// Optional ports table
html += '<div class="onboarding-port-section" style="margin-bottom:20px;">';
html += '<div class="onboarding-port-section-title" style="font-weight:700;margin-bottom:4px;">Optional — Only needed if you enable Element Calling:</div>';
html += '<div style="font-size:0.88em;margin-bottom:8px;color:var(--color-text-muted,#888);">These 5 additional port openings are required on top of the 4 required ports above.</div>';
html += '<table class="onboarding-port-table">';
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward&nbsp;to</th><th>Purpose</th></tr></thead>';
html += '<tbody>';
html += '<tr><td class="port-req-port">7881</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit WebRTC signalling</td></tr>';
html += '<tr><td class="port-req-port">78827894</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit media streams</td></tr>';
html += '<tr><td class="port-req-port">5349</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN over TLS</td></tr>';
html += '<tr><td class="port-req-port">3478</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN (STUN/relay)</td></tr>';
html += '<tr><td class="port-req-port">3000040000</td><td class="port-req-proto">TCP/UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN relay (WebRTC)</td></tr>';
html += '</tbody></table>';
html += '</div>';
// Totals
html += '<div class="onboarding-port-totals">';
html += '<strong>Total port openings: 4</strong> (without Element Calling)<br>';
html += '<strong>Total port openings: 9</strong> (with Element Calling — 4 required + 5 optional)';
html += '</div>';
html += '<div class="onboarding-port-warn" style="margin-bottom:16px;">'
+ '⚠ <strong>Ports 80 and 443 must be forwarded first.</strong> '
+ 'Caddy uses these to obtain SSL certificates from Let\'s Encrypt. '
+ 'If they are closed, HTTPS will not work and your services will be unreachable from outside your network.'
+ '</div>';
html += '<details class="onboarding-port-details" style="margin-bottom:16px;">'
+ '<summary class="onboarding-port-details-summary">How to set up port forwarding</summary>'
+ '<ol style="margin:12px 0 0 16px; padding:0; line-height:1.8;">'
+ '<li>Open your router\'s admin panel — usually <code>http://192.168.1.1</code> or <code>http://192.168.0.1</code></li>'
+ '<li>Look for <strong>"Port Forwarding"</strong>, <strong>"NAT"</strong>, or <strong>"Virtual Server"</strong> in the settings</li>'
+ '<li>Create a new rule for each port listed above</li>'
+ '<li>Set the destination/internal IP to <strong>' + ip + '</strong></li>'
+ '<li>Set both internal and external port to the same number</li>'
+ '<li>Save and apply changes</li>'
+ '</ol>'
+ '</details>';
body.innerHTML = html;
}
// ── Step 5: Complete ──────────────────────────────────────────────
async function completeOnboarding() {
var btn = document.getElementById("step-5-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;
updateStep5Checklist();
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 → 5 (port forwarding — no save needed)
var s4next = document.getElementById("step-4-next");
if (s4next) s4next.addEventListener("click", function() { showStep(nextStep(4)); });
// Step 5: finish
var s5finish = document.getElementById("step-5-finish");
if (s5finish) s5finish.addEventListener("click", completeOnboarding);
// Back buttons
document.querySelectorAll(".onboarding-btn-back").forEach(function(btn) {
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) {
updateStep5Checklist();
showMigrationStep(migration.password || "");
return;
}
} catch (_) {}
updateStep5Checklist();
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

File diff suppressed because it is too large Load Diff
+118 -9
View File
@@ -4,19 +4,28 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sovran_SystemsOS Hub</title>
<link rel="stylesheet" href="/static/style.css?v={{ style_css_hash }}" />
<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" />
<link rel="stylesheet" href="/static/css/security.css" />
</head>
<body>
<!-- Header bar -->
<header class="header-bar">
<img src="/static/sovran-hub-icon.svg" alt="Sovran Hub" class="header-logo" />
<span class="title">Sovran_SystemsOS Hub</span>
<span class="role-badge" id="role-badge">Loading…</span>
<button class="btn btn-update" id="btn-update" title="Run system update">
<span class="update-badge" id="update-badge"></span>
Update System
</button>
<button class="btn-icon" id="btn-refresh" title="Refresh service status"></button>
<div class="header-buttons">
<span class="role-badge" id="role-badge">Loading…</span>
<button class="btn btn-logout" id="btn-logout" title="Sign out">Sign Out</button>
</div>
</header>
<!-- IP bar -->
@@ -34,6 +43,10 @@
<!-- Service tiles -->
<main class="main-content">
<aside class="sidebar" id="sidebar">
<div id="sidebar-support"></div>
<div id="sidebar-features"></div>
</aside>
<div id="tiles-area"></div>
</main>
@@ -129,6 +142,17 @@
</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">
@@ -146,6 +170,81 @@
</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">⚠ What you should know:</p>
<ul class="upgrade-info-list">
<li>You will need to purchase domains for your services — <a href="https://njal.la" target="_blank" rel="noopener noreferrer">Njal.la</a> is the only supported domain provider</li>
<li>Some services require ports to be opened on your router</li>
</ul>
</div>
<div class="upgrade-info-box">
<p class="upgrade-info-title">️ Good to know:</p>
<ul class="upgrade-info-list">
<li>Your services will be accessible via your home internet connection. Your approximate geographic area may be visible through DNS records — this is normal for all self-hosted services</li>
<li>Your Bitcoin node remains fully private over Tor</li>
</ul>
</div>
<p class="support-desc">
<strong>Don't worry</strong> — the Hub will walk you through every step after the upgrade.
Domain setup, port forwarding, and configuration are all guided.
</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</em>.
</p>
<button class="security-reset-reboot-btn" id="security-reset-reboot-btn" disabled>
I have written down my new password — Reboot now
</button>
</div>
</div>
<!-- Reboot overlay -->
<div class="reboot-overlay" id="reboot-overlay">
<div class="reboot-card">
@@ -164,6 +263,16 @@
</div>
</div>
<script src="/static/app.js?v={{ app_js_hash }}"></script>
<script src="/static/js/constants.js"></script>
<script src="/static/js/state.js"></script>
<script src="/static/js/helpers.js"></script>
<script src="/static/js/tiles.js"></script>
<script src="/static/js/service-detail.js"></script>
<script src="/static/js/support.js"></script>
<script src="/static/js/update.js"></script>
<script src="/static/js/rebuild.js"></script>
<script src="/static/js/features.js"></script>
<script src="/static/js/security.js"></script>
<script src="/static/js/events.js"></script>
</body>
</html>
</html>
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sovran Hub — Login</title>
<link rel="stylesheet" href="/static/css/base.css" />
<link rel="stylesheet" href="/static/css/buttons.css" />
</head>
<body>
<div class="login-wrapper">
<div class="login-card">
<div class="login-header">
<img src="/static/sovran-hub-icon.svg" alt="Sovran Hub" class="login-logo" />
<div class="login-title">Sovran Hub</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,200 @@
<!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>
<span class="onboarding-step-connector"></span>
<span class="onboarding-step-dot" data-step="5">5</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(109, 191, 139, 0.10); border:1.5px solid rgba(109, 191, 139, 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(229, 165, 10, 0.1); border:1px solid rgba(229, 165, 10, 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">Domain Configuration</h2>
<p class="onboarding-step-desc">
Sovran_SystemsOS uses <strong><a href="https://njal.la" target="_blank" style="color: var(--accent-color);">Njal.la</a></strong> for domains and Dynamic DNS.
First, create an account at <strong>Njal.la</strong> and purchase a new domain, or create a subdomain from a domain you already own. Tip: Subdomains are free to create — you only need to purchase one domain, and you can add as many subdomains as you need at no extra cost.
Then, in the Njal.la web interface, create a <strong>Dynamic</strong> record pointing to this machine's external IP address (shown below).
Finally, paste the DDNS curl command from your Njal.la dashboard for each service below.
</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: Port Forwarding ── -->
<div class="onboarding-panel" id="step-4" style="display:none">
<div class="onboarding-step-header">
<span class="onboarding-step-icon">🔌</span>
<h2 class="onboarding-step-title">Port Forwarding Check</h2>
<p class="onboarding-step-desc">
Forward these ports on your router to this machine. Each port only needs to be opened once — they are shared across all your services.
<strong>Ports 80 and 443 must be open for SSL certificates to work.</strong>
</p>
</div>
<div class="onboarding-card" id="step-4-body">
<p class="onboarding-loading">Checking ports…</p>
</div>
<div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
<button class="btn btn-primary onboarding-btn-next" id="step-4-next">
Continue →
</button>
</div>
</div>
<!-- ── Step 5: Complete ── -->
<div class="onboarding-panel" id="step-5" style="display:none">
<div class="onboarding-hero">
<div class="onboarding-logo"></div>
<h1 class="onboarding-title">Your Sovran_SystemsOS is Ready!</h1>
<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>
<li>✅ Port forwarding reviewed</li>
</ul>
</div>
<div class="onboarding-footer">
<button class="btn btn-close-modal onboarding-btn-back" data-prev="4">← Back</button>
<button class="btn btn-primary" id="step-5-finish">
Go to Dashboard →
</button>
</div>
</div>
</div><!-- /panel-wrap -->
</div><!-- /shell -->
<script src="/static/onboarding.js?v={{ onboarding_js_hash }}"></script>
</body>
</html>
@@ -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

+42 -29
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" = {
@@ -31,15 +32,33 @@
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;
@@ -49,6 +68,18 @@
services.printing.enable = true;
systemd.enableEmergencyMode = false;
environment.gnome.excludePackages = [ pkgs.gnome-tour ];
security.pam.services.gdm-password.enableGnomeKeyring = true;
security.pam.services.gdm-autologin.enableGnomeKeyring = true;
# Declaratively guarantee the GNOME Keyring default pointer exists.
# Defining the full path ensures root doesn't accidentally lock the user out of .local
systemd.tmpfiles.rules = [
"d /home/free/.local 0700 free users -"
"d /home/free/.local/share 0700 free users -"
"d /home/free/.local/share/keyrings 0700 free users -"
"f /home/free/.local/share/keyrings/default 0600 free users - login\n"
];
# ── Audio ──────────────────────────────────────────────────
services.pulseaudio.enable = false;
@@ -67,8 +98,7 @@
extraGroups = [ "networkmanager" ];
};
services.displayManager.autoLogin.enable = true;
services.displayManager.autoLogin.user = "free";
services.displayManager.autoLogin.enable = false;
# ── Flatpak ────────────────────────────────────────────────
services.flatpak.enable = true;
@@ -84,9 +114,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
@@ -150,31 +180,14 @@ backup /etc/nix-bitcoin-secrets/ localhost/
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"
];
};
# ── 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";
}
}
+24 -110
View File
@@ -5,123 +5,37 @@
# #
# 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
# ═══════════════════════════════════════════════════════════
# ─── Custom Caddy virtual hosts ──────────────────────────
# Uncomment and edit below to add your own Caddy sites:
#
# Your initial role was selected during installation.
# To CHANGE your role, uncomment exactly ONE of the lines below.
# 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
# }
#
# Server+Desktop: Full server + desktop environment
# Desktop Only: Desktop environment, no server services
# Node (Bitcoin Only): Bitcoin ecosystem
#
# ───────────────────────────────────────────────────────────
# anotherdomain.com {
# reverse_proxy localhost:9090
# }
# '';
# sovran_systemsOS.roles.server_plus_desktop = true;
# sovran_systemsOS.roles.desktop = true;
# sovran_systemsOS.roles.node = true;
# ═══════════════════════════════════════════════════════════
# 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 = "";
}
}
+93
View File
@@ -0,0 +1,93 @@
# Sovran Hub — Manual Backup
The manual backup service copies critical system data from your Sovran Pro to an external USB drive, providing a third copy of your data (your Sovran Pro already maintains an automatic internal backup on its second drive).
Backups are written to:
```
<USB drive>/Sovran_SystemsOS_Backup/<timestamp>/
```
where `<timestamp>` is formatted as `YYYYMMDD_HHMMSS`.
---
## Backup Stages
The script always attempts all four stages, but skips stages that are irrelevant to the system's configured role (see [Per-Role Breakdown](#per-role-breakdown) below).
| Stage | Directory | Contents |
|-------|-----------|----------|
| **1/4 — NixOS config** | `/etc/nixos/` | Full NixOS system configuration: `role-state.nix`, `custom.nix`, flake files, and any other config managed by the Hub |
| **2/4 — Secrets** | `/etc/nix-bitcoin-secrets` | Bitcoin/LND secrets stored under `/etc/` |
| **3/4 — Home directory** | `/home/` | All user home directories (`.cache/` and Trash are excluded) |
| **4/4 — System data** | `/var/lib/` | Full service data tree, including Vaultwarden, bitcoind, LND, sovran-hub config, domains, secrets, and other `/var/lib` service directories (logs excluded as appropriate) |
---
## Per-Role Breakdown
The script detects the system role at runtime by reading `/var/lib/sovran-hub/config.json` (falling back to `/etc/nixos/role-state.nix`) and adjusts its behaviour accordingly.
### Server + Desktop (default)
All services are enabled: Bitcoin, Matrix Synapse, Vaultwarden, WordPress, Nextcloud.
| Stage | Status | Notes |
|-------|--------|-------|
| Stage 1 — NixOS config | ✅ Backed up | Full server configuration |
| Stage 2 — Secrets | ✅ Backed up | `/etc/nix-bitcoin-secrets` |
| Stage 3 — Home directory | ✅ Backed up | Desktop user data |
| Stage 4 — System data (`/var/lib`) | ✅ Backed up | Includes Vaultwarden, bitcoind, LND, sovran-hub config, domains, secrets, and all other service data under `/var/lib` (logs excluded) |
This produces the largest backup. All four stages generate meaningful data.
### Desktop Only
All server services are disabled (`bitcoin = false`, `synapse = false`, `vaultwarden = false`, `wordpress = false`, `nextcloud = false`). Only GNOME desktop is active.
| Stage | Status | Notes |
|-------|--------|-------|
| Stage 1 — NixOS config | ✅ Backed up | Simpler config (no server services) |
| Stage 2 — Secrets | ⏭️ Skipped | `/etc/nix-bitcoin-secrets` is not applicable for Desktop Only role |
| Stage 3 — Home directory | ✅ Backed up | **The most important data for this role** |
| Stage 4 — System data (`/var/lib`) | ✅ Backed up | Full `/var/lib` backup with `/var/lib/lnd` excluded for Desktop Only role |
This produces the smallest and fastest backup. Stages 1 and 3 are the primary sources of meaningful data.
### Node (Bitcoin-only)
Only the Bitcoin ecosystem is active: `bitcoind`, `electrs`, `lnd`, `rtl`, `btcpay`, `mempool`, and `bip110`. All other server services are disabled.
| Stage | Status | Notes |
|-------|--------|-------|
| Stage 1 — NixOS config | ✅ Backed up | Node-specific configuration |
| Stage 2 — Secrets | ✅ Backed up | `/etc/nix-bitcoin-secrets` |
| Stage 3 — Home directory | ✅ Backed up | User data |
| Stage 4 — System data (`/var/lib`) | ✅ Backed up | **Critical** — includes Lightning wallet/channel data plus all other `/var/lib` service data |
All four stages run, matching Server + Desktop behaviour. Some non-Bitcoin service directories under `/var/lib` may be sparse or absent depending on role.
---
## Backup Manifest
After all stages complete, the script writes a `BACKUP_MANIFEST.txt` file inside the timestamped backup directory. This file records the date, hostname, detected role, target drive, and a directory listing of everything that was backed up.
---
## Running the Backup
The backup is triggered from the Sovran Hub web UI. You can also run it directly:
```bash
# Auto-detect the first external USB drive
sudo bash /path/to/sovran-hub-backup.sh
# Specify a target drive explicitly
sudo BACKUP_TARGET=/run/media/<user>/<drive> bash /path/to/sovran-hub-backup.sh
```
The script requires at least **10 GB** of free space on the target drive and will refuse to write to internal system drives.
Logs are written to `/var/log/sovran-hub-backup.log` and the current status (`RUNNING`, `SUCCESS`, or `FAILED`) is tracked in `/var/log/sovran-hub-backup.status`.
+472
View File
@@ -0,0 +1,472 @@
# Remote Deployment via Headscale (Self-Hosted Tailscale)
This guide covers the Sovran Systems remote deployment system built on [Headscale](https://headscale.net) — a self-hosted, open-source implementation of the Tailscale coordination server. Freshly booted ISOs automatically join a private WireGuard mesh VPN without any per-machine key pre-generation.
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────────┐
│ Internet │
└────────────┬─────────────────────┬──────────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌─────────────────────────────────┐
│ Admin Workstation │ │ Sovran VPS │
│ │ │ ┌─────────────────────────────┐ │
│ tailscale up │ │ │ Headscale (port 8080) │ │
│ --login-server │◄──┼─►│ Coordination server │ │
│ hs.example.com │ │ ├─────────────────────────────┤ │
│ │ │ │ Provisioning API (9090) │ │
└────────────────────┘ │ │ POST /register │ │
│ │ GET /machines │ │
│ │ GET /health │ │
│ ├─────────────────────────────┤ │
│ │ Caddy (80/443) │ │
│ │ hs.example.com → :8080 │ │
│ │ prov.example.com → :9090 │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
│ WireGuard mesh (Tailnet)
┌─────────────────────────────────┐
│ Deploy Target Machine │
│ │
│ Boot live ISO → │
│ sovran-auto-provision → │
│ POST /register → │
│ tailscale up --authkey=... │
└─────────────────────────────────┘
```
**Components:**
- **`sovran-provisioner.nix`** — NixOS module deployed on a separate VPS; runs Headscale + provisioning API + Caddy.
- **Live ISO** (`iso/common.nix`) — Auto-registers with the provisioning server and joins the Tailnet on boot.
- **`remote-deploy.nix`** — Post-install NixOS module that uses Tailscale/Headscale for ongoing access.
---
## Part 1: VPS Setup — Deploy `sovran-provisioner.nix`
### Prerequisites
- A NixOS VPS (any provider) with a public IP
- Two DNS A records pointing to your VPS:
- `hs.yourdomain.com` → VPS IP (Headscale coordination server)
- `prov.yourdomain.com` → VPS IP (Provisioning API)
- Ports 80, 443 (TCP) and 3478 (UDP, STUN/DERP) open in your VPS firewall
### DNS Records
| Type | Name | Value |
|------|-----------------------|------------|
| A | `hs.yourdomain.com` | `<VPS IP>` |
| A | `prov.yourdomain.com` | `<VPS IP>` |
### NixOS Configuration
Add the following to your VPS's `/etc/nixos/configuration.nix`:
```nix
{ config, lib, pkgs, ... }:
{
imports = [
./hardware-configuration.nix
/path/to/sovran-provisioner.nix # or fetch from the repo
];
sovranProvisioner = {
enable = true;
domain = "prov.yourdomain.com";
headscaleDomain = "hs.yourdomain.com";
# Optional: customise defaults
headscaleUser = "sovran-deploy"; # namespace for deploy machines
adminUser = "admin"; # namespace for your workstation
keyExpiry = "1h"; # pre-auth keys expire after 1 hour
rateLimitMax = 10; # max registrations per window
rateLimitWindow = 60; # window in seconds
};
# Required for Caddy ACME (Let's Encrypt)
networking.hostName = "sovran-vps";
system.stateVersion = "24.11";
}
```
### Deploy
```bash
nixos-rebuild switch
```
Caddy will automatically obtain TLS certificates via Let's Encrypt.
### Retrieve the Enrollment Token
```bash
cat /var/lib/sovran-provisioner/enroll-token
```
Keep this token secret — it is used to authenticate ISO registrations. The token is auto-generated on first boot and stored at this path. You never need to set it manually. Just `cat` it from the VPS and copy it to `iso/secrets/enroll-token` before building the ISO.
---
## Part 2: Admin Workstation Setup
Join your Tailnet as an admin so you can reach deployed machines:
### Install Tailscale
Follow the [Tailscale installation guide](https://tailscale.com/download) for your OS, or on NixOS:
```nix
services.tailscale.enable = true;
```
### Join the Tailnet
```bash
sudo tailscale up --login-server https://hs.yourdomain.com --accept-dns=false
```
> **Note:** The `--accept-dns=false` flag prevents Tailscale from taking over your system DNS resolver. This is important if you are behind a VPN (see [Troubleshooting](#troubleshooting) below).
Tailscale prints a URL. Open it and copy the node key (starts with `mkey:`).
### Approve the Node in Headscale
On the VPS, first find the numeric user ID for the `admin` user, then register the node:
```bash
# Look up the numeric ID for the admin user (Headscale 0.28.0 requires -u <id>)
headscale users list -o json
# Register the node using the numeric user ID
headscale nodes register -u <admin-user-id> --key mkey:xxxxxxxxxxxxxxxx
```
Your workstation is now on the Tailnet. You can list nodes:
```bash
headscale nodes list
```
---
## Part 3: Building the Deploy ISO
### Add Secrets (gitignored)
The secrets directory `iso/secrets/` is gitignored. Populate it before building:
```bash
# Copy the enrollment token from the VPS
ssh root@<VPS> cat /var/lib/sovran-provisioner/enroll-token > iso/secrets/enroll-token
# Set the provisioner URL
echo "https://prov.yourdomain.com" > iso/secrets/provisioner-url
```
These files are baked into the ISO at build time. If the files are absent the ISO still builds — the auto-provision service exits cleanly with "No enroll token found, skipping auto-provision", leaving DIY users unaffected.
### Build the ISO
```bash
nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage
```
The resulting ISO is in `./result/iso/`.
---
## Part 4: Deployment Workflow
### Step-by-Step
1. **Hand the ISO to the remote person** — they burn it to a USB drive and boot.
2. **ISO boots and auto-registers**`sovran-auto-provision.service` runs automatically:
- Reads `enroll-token` and `provisioner-url` from `/etc/sovran/`
- `POST https://prov.yourdomain.com/register` with hostname + MAC
- Receives a Headscale pre-auth key
- Runs `tailscale up --login-server=... --authkey=...`
- The machine appears in `headscale nodes list` within ~30 seconds
3. **Approve the node (if not using auto-approve)** — on the VPS:
```bash
headscale nodes list
# Note the node key for the new machine
```
4. **SSH from your workstation** — once the machine is on the Tailnet:
```bash
# Get the machine's Tailscale IP
headscale nodes list | grep sovran-deploy-
# SSH in
ssh root@100.64.x.x # password: sovran-remote (live ISO default)
```
5. **Run the headless installer**:
The `--deploy-key` is your SSH public key that gets injected into `root`'s `authorized_keys` on the deployed machine. This grants full root access for initial setup. Generate it once on your workstation if you haven't already:
```bash
ssh-keygen -t ed25519 -f ~/.ssh/sovran-deploy -C "sovran-deploy"
```
After deployment is complete and you disable deploy mode, this key is removed.
```bash
sudo sovran-install-headless.sh \
--disk /dev/sda \
--role server \
--deploy-key "$(cat ~/.ssh/sovran-deploy.pub)" \
--headscale-server "https://hs.yourdomain.com" \
--headscale-key "$(headscale preauthkeys create -u $(headscale users list -o json | jq -r '.[] | select(.name=="sovran-deploy") | .id') -e 2h -o json | jq -r '.key')"
```
6. **Machine reboots into Sovran_SystemsOS** — `deploy-tailscale-connect.service` runs:
- Reads `/var/lib/secrets/headscale-authkey`
- Joins the Tailnet with a deterministic hostname (`sovran-<hostname>`)
7. **Post-install SSH and RDP**:
```bash
# SSH over Tailnet
ssh root@<tailscale-ip>
# RDP over Tailnet (desktop role) — Sovran_SystemsOS uses GNOME Remote Desktop (native Wayland RDP)
# Retrieve the auto-generated RDP password:
ssh root@<tailscale-ip> cat /var/lib/gnome-remote-desktop/rdp-password
# Then connect with any RDP client (Remmina, GNOME Connections, Microsoft Remote Desktop):
# Host: <tailscale-ip>:3389 User: sovran Password: <from above>
```
8. **Disable deploy mode** — edit `/etc/nixos/custom.nix` on the target, set `enable = false`, then:
```bash
sudo nixos-rebuild switch
```
---
## Part 5: Post-Install Access
### SSH
```bash
# Over Tailnet
ssh root@100.64.x.x
```
### RDP (desktop/server roles)
Sovran_SystemsOS uses **GNOME Remote Desktop** (native Wayland RDP — not xfreerdp). The RDP service auto-generates credentials on first boot.
**Username:** `sovran`
**Password:** auto-generated — retrieve it via SSH:
```bash
ssh root@<tailscale-ip> cat /var/lib/gnome-remote-desktop/rdp-password
```
Connect using any RDP client (Remmina, GNOME Connections, Microsoft Remote Desktop) to `<tailscale-ip>:3389`.
---
## Security Model
| Concern | Mitigation |
|---------|-----------|
| Enrollment token theft | Token only triggers key generation; it does not grant access to the machine itself |
| Rogue device joins Tailnet | Visible in `headscale nodes list`; removable instantly with `headscale nodes delete` |
| Pre-auth key reuse | Keys are ephemeral and expire in 1 hour (configurable via `keyExpiry`) |
| Rate limiting | Provisioning API limits to 10 registrations/minute by default (configurable) |
| SSH access | Requires ed25519 key injected at install time; password authentication disabled |
| Credential storage | Auth key written to `/var/lib/secrets/headscale-authkey` (mode 600) on the installed OS |
### Token Rotation
To rotate the enrollment token:
1. On the VPS:
```bash
openssl rand -hex 32 > /var/lib/sovran-provisioner/enroll-token
chmod 600 /var/lib/sovran-provisioner/enroll-token
```
2. Update `iso/secrets/enroll-token` and rebuild the ISO.
Old ISOs with the previous token will fail to register (receive 401).
---
## Monitoring
### List Active Tailnet Nodes
```bash
# On the VPS
headscale nodes list
```
### List Registered Machines (Provisioning API)
```bash
curl -s -H "Authorization: Bearer $(cat /var/lib/sovran-provisioner/enroll-token)" \
https://prov.yourdomain.com/machines | jq .
```
### Health Check
```bash
curl https://prov.yourdomain.com/health
# {"status": "ok"}
```
### Provisioner Logs
```bash
journalctl -u sovran-provisioner -f
```
### Headscale Logs
```bash
journalctl -u headscale -f
```
---
## Cleanup
### Remove a Machine from the Tailnet
```bash
headscale nodes list
headscale nodes delete --identifier <id>
```
### Disable Deploy Mode on an Installed Machine
Edit `/etc/nixos/custom.nix`:
```nix
sovran_systemsOS.deploy.enable = false;
```
Then rebuild:
```bash
nixos-rebuild switch
```
This stops the Tailscale connect service.
### Revoke All Active Pre-Auth Keys
```bash
# List pre-auth keys (Headscale 0.28.0: no --user flag on list)
headscale preauthkeys list
# Expire a specific key — use numeric user ID (-u <id>)
# First find the user ID:
headscale users list -o json
# Then expire the key:
headscale preauthkeys expire -u <user-id> --key <key>
```
---
## Troubleshooting
### VPN Conflicts (Mullvad, WireGuard, etc.)
**Symptom:** `tailscale up` hangs or fails with `connection refused` on port 443, even though `curl https://hs.yourdomain.com/health` works fine.
**Cause:** VPNs like Mullvad route all traffic — including Tailscale's control-plane connections — through the VPN tunnel. Additionally, Tailscale's DNS handler (`--accept-dns=true` by default) hijacks DNS resolution and may prevent correct resolution of your Headscale server even when logged out.
**Solution:**
1. Disconnect your VPN temporarily and retry `tailscale up`.
2. If you need the VPN active, use split tunneling to exclude `tailscaled`:
```bash
# Mullvad CLI
mullvad split-tunnel add $(pidof tailscaled)
```
Or in the Mullvad GUI: **Settings → Split tunneling → Add tailscaled**.
3. Always pass `--accept-dns=false` when enrolling to avoid DNS hijacking:
```bash
sudo tailscale up --login-server https://hs.yourdomain.com --authkey <key> --accept-dns=false
```
---
### "RATELIMIT" in tailscaled Logs
**Symptom:** `journalctl -u tailscaled` shows lines like:
```
[RATELIMIT] format("Received error: %v")
```
**Cause:** This is **NOT** a server-side rate limit from Headscale. It is tailscaled's internal log suppressor de-duplicating repeated connection-refused error messages. The real underlying error is `connection refused`.
**What to check:**
1. Is Headscale actually running? `curl https://hs.yourdomain.com/health`
2. Is your VPN blocking the connection? (see VPN Conflicts above)
3. Is there a firewall blocking port 443?
---
### "connection refused" on Port 443
If `tailscale up` fails but `curl` works, the issue is usually DNS or VPN:
```bash
# Does curl reach Headscale successfully?
curl -v https://hs.yourdomain.com/health
# Force IPv4 vs IPv6 to identify if it's an address-family issue
curl -4 https://hs.yourdomain.com/health
curl -6 https://hs.yourdomain.com/health
# Check what IP headscale resolves to
dig +short hs.yourdomain.com
# What resolver is the system using?
cat /etc/resolv.conf
```
If curl works but tailscale doesn't, tailscaled may be using a different DNS resolver (e.g. its own `100.100.100.100` stub resolver). Fix: pass `--accept-dns=false`.
---
### Headscale User ID Lookup (0.28.0)
Headscale 0.28.0 removed `--user <name>` in favour of `-u <numeric-id>`. To find the numeric ID for a user:
```bash
headscale users list -o json
# Output: [{"id": "1", "name": "sovran-deploy", ...}, ...]
# One-liner to get the ID for a specific user
headscale users list -o json | jq -r '.[] | select(.name=="sovran-deploy") | .id'
```
Then use the numeric ID in subsequent commands:
```bash
headscale preauthkeys create -u 1 -e 1h -o json
headscale nodes register -u 1 --key mkey:xxxx
```
---
## Reference
| Component | Port | Protocol | Description |
|-----------|------|----------|-------------|
| Caddy | 80 | TCP | HTTP → HTTPS redirect |
| Caddy | 443 | TCP | HTTPS (Let's Encrypt) |
| Headscale | 8080 | TCP | Coordination server (proxied by Caddy) |
| Provisioner | 9090 | TCP | Registration API (proxied by Caddy) |
| DERP/STUN | 3478 | UDP | WireGuard relay fallback |
| Tailscale | N/A | WireGuard | Mesh VPN between nodes |
+259
View File
@@ -0,0 +1,259 @@
# Tech Support: Security Design, User Flow, and Incident Response
## Overview
The Sovran Hub includes a **Tech Support** feature that lets Sovran Systems
staff remotely diagnose and fix issues on a user's machine via SSH — without
ever having access to private keys or wallet funds.
Wallet protection is the default. The user must make an active, time-limited
choice to grant support staff access to wallet files, and can revoke that
access at any time.
---
## Implementation Details
### Restricted User Instead of Root
When a user enables support access the Hub:
1. Ensures the `sovran-support` system user exists (declared declaratively in
`modules/core/tech-support.nix`; the Hub also provisions it on demand as a
fallback on non-NixOS systems).
2. Writes the Sovran Systems public SSH key **only** to
`/var/lib/sovran-support/.ssh/authorized_keys`, not to root's
`authorized_keys`.
3. Applies POSIX ACLs (`setfacl -R -m u:sovran-support:---`) to every wallet
directory that exists on disk, denying all access by the support user.
4. Records a timestamped `SUPPORT_ENABLED` event in the audit log at
`/var/log/sovran-support-audit.log`.
When the session ends (or if the Hub cannot create the restricted user), the
key is removed and all ACLs are revoked immediately.
### Protected Wallet Paths
The following directories are locked by default when a support session starts:
| Path | Contents |
|------|----------|
| `/etc/nix-bitcoin-secrets` | nix-bitcoin generated secrets |
| `/var/lib/bitcoind` | Bitcoin Core chainstate and wallet |
| `/var/lib/lnd` | LND wallet and channel database |
| `/home` | User home directories |
Paths are only locked if they exist on disk at the time the session starts.
### POSIX ACL Mechanics
POSIX ACLs on Linux handle access checks in this order:
1. If the process UID matches the file owner UID → use owner permissions
2. **If there is a matching named-user ACL entry → use that entry's
permissions** (clamped by the mask entry)
3. If any group matches → use group permissions
4. Otherwise → use "other" permissions
Setting `u:sovran-support:---` creates a named-user ACL entry with no
permissions. Because the named-user entry is checked before the group/other
entries, the support user cannot access those directories regardless of the
"other" permission bits.
`setfacl` and `getfacl` are provided by the `acl` package, which is added to
`environment.systemPackages` by `modules/core/tech-support.nix`.
### Fallback to Root (When Restricted User Cannot Be Created)
If the `sovran-support` user does not exist and cannot be created (e.g.,
`users.mutableUsers = false` and the declarative module has not been deployed
yet), the Hub falls back to adding the support key to root's
`authorized_keys`. The modal prominently warns the user when this has happened
so they can decide whether to end the session.
### Audit Log
Every session event is appended to `/var/log/sovran-support-audit.log`:
```
[2025-01-15 14:32:01 UTC] SUPPORT_ENABLED: restricted_user=True acl_applied=True protected_paths=4
[2025-01-15 14:45:00 UTC] WALLET_UNLOCKED: duration=3600s expires=2025-01-15 15:45:00 UTC
[2025-01-15 15:45:00 UTC] WALLET_RELOCKED: auto-expired
[2025-01-15 16:01:22 UTC] SUPPORT_DISABLED
```
The last 100 lines of this log are accessible from the Hub UI while a session
is active (or after it ends, until the page is refreshed).
---
## Security Tradeoffs
### What This Protects Against
- **Accidental wallet exposure** — support staff cannot read wallet files
during a normal session; they must ask the user to explicitly grant access.
- **Credential theft** — private keys in the wallet directories are not
visible to the `sovran-support` user by default.
- **Scope creep** — the restricted user account limits the blast radius of an
SSH session compared to direct root access.
### Known Limitations
| Limitation | Mitigation |
|------------|------------|
| Support user still has system-wide bash access | Restrict with `ForceCommand` or AppArmor in the NixOS config if a narrower scope is required |
| ACLs apply only to directories that exist at session start | If new wallet directories are created during a session, they are not auto-protected. Re-lock and re-enable support to pick up new paths |
| Root fallback grants full access | The Hub UI warns the user prominently; users should end the session if they are uncomfortable |
| `setfacl` / ACL filesystem support required | The `acl` package is declared in `tech-support.nix`; most Linux filesystems (ext4, btrfs, xfs) support ACLs by default |
| Wallet access grant is time-limited but lazy-expired | Expiry is checked on the next `/api/support/status` poll (every 10 seconds in the UI); there is a small window after expiry |
### Defense-in-Depth Recommendations
For environments that require stronger isolation, consider layering one or
more additional controls:
- **`ForceCommand`** in `sshd_config` (or `~/.ssh/authorized_keys` command
prefix) to restrict the support user to a specific diagnostic script.
- **`ChrootDirectory`** in the `sshd_config` `Match User sovran-support` block
to confine the session to a prepared directory tree.
- **AppArmor or SELinux** profiles that deny the support process read access
to wallet paths at the kernel level.
- **Namespace/bind-mount overlays** (e.g., via a wrapper systemd unit) to
present a sanitized filesystem view.
---
## User Flow
```
User opens Hub → Clicks "Tech Support" in sidebar
Modal: "Need help from Sovran Systems?"
• Explains what will happen
• Shows Wallet Protection notice
• User clicks "Enable Support Access"
Hub: 1. Creates / verifies sovran-support user
2. Writes SSH key to that user's authorized_keys
3. Applies POSIX ACL deny on all existing wallet paths
4. Saves session metadata + writes SUPPORT_ENABLED to audit log
Modal: "Support Access is Active"
• Live session duration timer
• Wallet Files: Protected panel
Optional: "Grant Wallet Access" (time-limited, user-chosen)
• "End Support Session" button
• "View Audit Log" button
(User grants wallet access)
Hub: • Removes ACL deny entries
• Records WALLET_UNLOCKED event with expiry time
• Starts countdown timer in UI
(Timer expires or user clicks "Re-lock Wallet Now")
Hub: • Re-applies ACL deny entries
• Removes WALLET_UNLOCK_FILE
• Records WALLET_RELOCKED event
(User clicks "End Support Session")
Hub: 1. Removes SSH key from sovran-support authorized_keys
2. Removes SSH key from root authorized_keys (legacy cleanup)
3. Revokes any wallet unlock, re-applies ACL deny
4. Verifies key is gone
5. Records SUPPORT_DISABLED event
Modal: "Support Session Ended — SSH key removed"
• Shows verified removal status
```
---
## Incident Response
### Scenario 1 — You accidentally granted wallet access and are unsure what was copied
**Immediate steps:**
1. Click **"Re-lock Wallet Now"** in the Hub modal, or click
**"End Support Session"** to simultaneously revoke SSH access and wallet
access.
2. Open the **Audit Log** from the Hub modal and note the timestamps of
`WALLET_UNLOCKED` and `WALLET_RELOCKED` events.
3. Check `/var/log/auth.log` (or `journalctl -u sshd`) for SSH login events
by `sovran-support` during the unlocked window.
**Assessment:**
- If no SSH login occurred during the wallet-unlocked window, your keys are
safe.
- If an SSH login did occur, treat private keys as potentially compromised.
**Recovery if keys may be compromised:**
| Wallet | Recovery action |
|--------|----------------|
| LND | Move all funds out using `lncli sendcoins` to a freshly generated on-chain address; close channels; recreate wallet |
| Sparrow | Sweep funds to a new wallet generated on an air-gapped device |
| Bisq | Withdraw all BSQ and BTC to external wallets; delete the Bisq data directory and recreate |
| nix-bitcoin secrets | Rotate all secrets with `nix-bitcoin-secrets generate` and redeploy |
**Report the incident:**
Contact Sovran Systems immediately at support@sovransystems.com with:
- The audit log output (`/var/log/sovran-support-audit.log`)
- The SSH auth log for the affected time window
- A description of what you were troubleshooting
---
### Scenario 2 — Support session cannot be ended (button fails or server is unresponsive)
**Manual key removal (run as root on the device):**
```bash
# Remove from support user's authorized_keys
rm -f /var/lib/sovran-support/.ssh/authorized_keys
# Remove from root's authorized_keys (fallback / legacy)
sed -i '/sovransystemsos-support/d' /root/.ssh/authorized_keys
# Remove wallet unlock state
rm -f /var/lib/secrets/support-wallet-unlock
# Re-apply wallet ACL protections
setfacl -R -m u:sovran-support:--- /etc/nix-bitcoin-secrets \
/var/lib/bitcoind /var/lib/lnd /home 2>/dev/null || true
# Restart sshd to drop any active connections
systemctl restart sshd
```
---
### Scenario 3 — You see an unexpected SUPPORT_ENABLED in the audit log
This should never happen without physical or remote access to the Hub web
interface. If you see an unexpected entry:
1. Immediately run the manual key removal commands above.
2. Change the Sovran Hub web interface password.
3. Check `/var/log/nginx/access.log` (or Caddy access logs) for unexpected
requests to `/api/support/enable`.
4. Consider rebooting the device to clear any in-memory state.
5. Report the incident to Sovran Systems.
---
*This document is part of the Sovran_SystemsOS repository. For the
authoritative and up-to-date version, see the repository.*
@@ -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
+24 -24
View File
@@ -5,11 +5,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1773169138,
"narHash": "sha256-6X41z8o2z8KjF4gMzLTPD41WjvCDGXTc0muPGmwcOMk=",
"lastModified": 1777892922,
"narHash": "sha256-Yo53Ae0eQa5nByGoTsdJQAIK7kR9UlSDEWT88Qy/6g8=",
"owner": "emmanuelrosa",
"repo": "bitcoin-knots-bip-110-nix",
"rev": "b9d018b71e20ce8c1567cbc2401b6edc2c1c7793",
"rev": "dfe7221629f14d81ce1b4fc96d9500982d1baa58",
"type": "github"
},
"original": {
@@ -24,11 +24,11 @@
"oldNixpkgs": "oldNixpkgs"
},
"locked": {
"lastModified": 1774797058,
"narHash": "sha256-URUOiKNjG3s7vDkTj554+3yzQ0qqNQoQwHdc7vs63X0=",
"lastModified": 1777892852,
"narHash": "sha256-A+jhf4vEQmn2/DBedQMrisrijDgYfrZOpjjSPhrJgJA=",
"owner": "emmanuelrosa",
"repo": "btc-clients-nix",
"rev": "a10dae067da04b7b170eed73efc665d27fc0e0c5",
"rev": "7576625375f510bcb432caba0e331c6ed2942350",
"type": "github"
},
"original": {
@@ -71,11 +71,11 @@
]
},
"locked": {
"lastModified": 1769996383,
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
"lastModified": 1777932387,
"narHash": "sha256-nUYVPiqrzr36ThiQOAr5MKeGHDBSDM3OFWkz0uDjOvc=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
"rev": "71a3a77326609675e9f8b51084cf23d5d1945899",
"type": "github"
},
"original": {
@@ -127,11 +127,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1772380631,
"narHash": "sha256-FhW0uxeXjefINP0vUD4yRBB52Us7fXZPk9RiPAopfiY=",
"lastModified": 1777728799,
"narHash": "sha256-z7jjYQqhkFKab92VQ3duB7QVO7f7Y62qTFrJYXO/lyo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6d3b61b190a899042ce82a5355111976ba76d698",
"rev": "4b2287113c2f9a2331c04899b2e2e5ab92dea9c5",
"type": "github"
},
"original": {
@@ -191,11 +191,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1772380631,
"narHash": "sha256-FhW0uxeXjefINP0vUD4yRBB52Us7fXZPk9RiPAopfiY=",
"lastModified": 1777728799,
"narHash": "sha256-z7jjYQqhkFKab92VQ3duB7QVO7f7Y62qTFrJYXO/lyo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6d3b61b190a899042ce82a5355111976ba76d698",
"rev": "4b2287113c2f9a2331c04899b2e2e5ab92dea9c5",
"type": "github"
},
"original": {
@@ -222,11 +222,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"lastModified": 1777954456,
"narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1",
"type": "github"
},
"original": {
@@ -238,11 +238,11 @@
},
"nixpkgs_5": {
"locked": {
"lastModified": 1770380644,
"narHash": "sha256-P7dWMHRUWG5m4G+06jDyThXO7kwSk46C1kgjEWcybkE=",
"lastModified": 1777918403,
"narHash": "sha256-7QiZv0LcW1yIOLo2LNuCQjWon1Z1r99FwK24hbtBOF4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ae67888ff7ef9dff69b3cf0cc0fbfbcd3a722abe",
"rev": "afc5551119aae6eab73a95c1960891cfe63204f6",
"type": "github"
},
"original": {
@@ -259,11 +259,11 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1774802402,
"narHash": "sha256-L1UJ/zxKTyyaGGmytH6OYlgQ0HGSMhvPkvU+iz4Mkb8=",
"lastModified": 1777991353,
"narHash": "sha256-DFwjggMV+nzCZpwK6Obxj9F+P59rbLVowGqHETfctBk=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "cbd8536a05d1aae2593cb5c9ace1010c8c5845cb",
"rev": "7986a276960b4dfaed9bb2c3c438b5ba71ae08f1",
"type": "github"
},
"original": {
+5 -15
View File
@@ -25,27 +25,17 @@
modules = [
{ nixpkgs.hostPlatform = "x86_64-linux"; }
self.nixosModules.Sovran_SystemsOS
/etc/nixos/role-state.nix
/etc/nixos/custom.nix
/etc/nixos/hub-overrides.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
./iso/common.nix
nix-bitcoin.nixosModules.default
nixvim.nixosModules.nixvim
];
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;
}
+106 -1
View File
@@ -55,7 +55,6 @@ in
gsettings-desktop-schemas
adwaita-icon-theme
util-linux
disko
parted
dosfstools
e2fsprogs
@@ -63,12 +62,118 @@ in
nixos-install-tools
git
curl
openssh
tailscale
jq
xxd
];
# Remote install support — SSH on the live ISO
services.openssh = {
enable = true;
listenAddresses = [{ addr = "0.0.0.0"; port = 22; }];
settings = {
PasswordAuthentication = true;
PermitRootLogin = "yes";
};
};
users.users.root.initialPassword = lib.mkForce "sovran-remote";
users.users.root.initialHashedPassword = lib.mkForce null;
# mDNS so the machine is discoverable as sovran-installer.local
services.avahi = {
enable = true;
hostName = "sovran-installer";
nssmdns4 = true;
publish = { enable = true; addresses = true; };
};
environment.etc."sovran/logo.png".source = ./assets/splash-logo.png;
environment.etc."sovran/flake".source = sovranSource;
environment.etc."sovran/installer.py".source = ./installer.py;
# These files are gitignored — set at build time by placing them in iso/secrets/
environment.etc."sovran/enroll-token" = lib.mkIf (builtins.pathExists ./secrets/enroll-token) {
text = builtins.readFile ./secrets/enroll-token;
mode = "0600";
};
environment.etc."sovran/provisioner-url" = lib.mkIf (builtins.pathExists ./secrets/provisioner-url) {
text = builtins.readFile ./secrets/provisioner-url;
mode = "0644";
};
# Tailscale client for mesh VPN
services.tailscale.enable = true;
# Auto-provision service — registers with provisioning server and joins Tailnet
systemd.services.sovran-auto-provision = {
description = "Auto-register with Sovran provisioning server and join Tailnet";
after = [ "network-online.target" "tailscaled.service" ];
wants = [ "network-online.target" "tailscaled.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
path = [ pkgs.tailscale pkgs.curl pkgs.jq pkgs.coreutils pkgs.iproute2 pkgs.xxd ];
script = ''
TOKEN_FILE="/etc/sovran/enroll-token"
URL_FILE="/etc/sovran/provisioner-url"
[ -f "$TOKEN_FILE" ] || { echo "No enroll token found, skipping auto-provision"; exit 0; }
[ -f "$URL_FILE" ] || { echo "No provisioner URL found, skipping auto-provision"; exit 0; }
TOKEN=$(cat "$TOKEN_FILE")
PROV_URL=$(cat "$URL_FILE")
[ -n "$TOKEN" ] || exit 0
[ -n "$PROV_URL" ] || exit 0
# Wait for network + tailscaled
sleep 10
# Collect machine info
HOSTNAME="sovran-deploy-$(head -c 8 /dev/urandom | xxd -p)"
MAC=$(ip link show | grep ether | head -1 | awk '{print $2}' || echo "unknown")
echo "Registering with provisioning server at $PROV_URL..."
# Retry up to 6 times (covers slow DHCP)
RESPONSE=""
for i in $(seq 1 6); do
RESPONSE=$(curl -sf --max-time 15 -X POST \
"$PROV_URL/register" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"hostname\": \"$HOSTNAME\", \"mac\": \"$MAC\"}" 2>/dev/null) && break
echo "Attempt $i failed, retrying in 10s..."
sleep 10
done
if [ -z "$RESPONSE" ]; then
echo "ERROR: Failed to register with provisioning server after 6 attempts"
exit 1
fi
HS_KEY=$(echo "$RESPONSE" | jq -r '.headscale_key')
LOGIN_SERVER=$(echo "$RESPONSE" | jq -r '.login_server')
if [ -z "$HS_KEY" ] || [ "$HS_KEY" = "null" ]; then
echo "ERROR: No Headscale key in response: $RESPONSE"
exit 1
fi
echo "Joining Tailnet via $LOGIN_SERVER as $HOSTNAME..."
tailscale up \
--login-server="$LOGIN_SERVER" \
--authkey="$HS_KEY" \
--hostname="$HOSTNAME"
TAILSCALE_IP=$(tailscale ip -4)
echo "Successfully joined Tailnet as $HOSTNAME ($TAILSCALE_IP)"
'';
};
environment.etc."xdg/autostart/sovran-installer.desktop".text = ''
[Desktop Entry]
Type=Application
-62
View File
@@ -1,62 +0,0 @@
{ device ? "/dev/sda", dataDevice ? "", ... }:
{
disko.devices = {
disk = {
main = {
type = "disk";
device = builtins.toString device;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
start = "1M";
end = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot/efi";
mountOptions = [ "umask=0077" "defaults" ];
};
};
root = {
name = "root";
start = "512M";
end = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
extraArgs = [ "-L" "sovran_systemsos" ];
};
};
};
};
};
} // (if dataDevice != "" then {
data = {
type = "disk";
device = builtins.toString dataDevice;
content = {
type = "gpt";
partitions = {
primary = {
name = "primary";
start = "1M";
end = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/run/media/Second_Drive";
extraArgs = [ "-L" "BTCEcoandBackup" ];
};
};
};
};
};
} else {});
};
}
+580 -127
View File
@@ -5,8 +5,10 @@ gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw, GLib
import atexit
import os
import secrets
import subprocess
import sys
import tempfile
import threading
import time
@@ -14,6 +16,48 @@ LOGO = "/etc/sovran/logo.png"
LOG = "/tmp/sovran-install.log"
FLAKE = "/etc/sovran/flake"
DEPLOYED_FLAKE = """\
{
description = "Sovran_SystemsOS for the Sovran Pro from Sovran Systems";
inputs = {
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS?ref=staging-dev";
};
outputs = { self, Sovran_Systems, ... }@inputs: {
nixosConfigurations."nixos" = Sovran_Systems.inputs.nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.hostPlatform = "x86_64-linux"; }
./hardware-configuration.nix
./role-state.nix
./custom.nix
Sovran_Systems.nixosModules.Sovran_SystemsOS
];
};
};
}
"""
DICEWARE_WORDS = [
"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",
]
def generate_diceware_password():
words = [secrets.choice(DICEWARE_WORDS) for _ in range(3)]
digit = secrets.randbelow(10)
return "-".join(words) + f"-{digit}"
try:
logfile = open(LOG, "a")
atexit.register(logfile.close)
@@ -38,7 +82,8 @@ def run(cmd):
def run_stream(cmd, buf):
log(f"$ {' '.join(cmd)}")
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
stdin=subprocess.DEVNULL, text=True)
for line in proc.stdout:
log(line.rstrip())
GLib.idle_add(append_text, buf, line)
@@ -86,7 +131,7 @@ def symbolic_icon(name):
return icon
# ── Application ────────────────────────────────────────────────────────────────
# ── Application ──────────────────────────────────────────────────────────
class InstallerApp(Adw.Application):
def __init__(self):
@@ -98,7 +143,7 @@ class InstallerApp(Adw.Application):
self.win.present()
# ── Main Window ────────────────────────────────────────────────────────────────
# ── Main Window ──────────────────────────────────────────────────────────
class InstallerWindow(Adw.ApplicationWindow):
def __init__(self, **kwargs):
@@ -112,16 +157,15 @@ class InstallerWindow(Adw.ApplicationWindow):
self.boot_size = None
self.data_disk = None
self.data_size = None
self.data_drive_has_timechain = False
self.free_password = None
# Root navigation view
self.nav = Adw.NavigationView()
self.set_content(self.nav)
# Check for internet before anything else
if check_internet():
self.push_welcome()
else:
self.push_no_internet()
# Always show the landing/welcome page first
self.push_landing()
# ── Navigation helpers ─────────────────────────────────────────────────
@@ -148,7 +192,7 @@ class InstallerWindow(Adw.ApplicationWindow):
break
self.push_page(title, child)
# ── Shared widgets ────────────────────────────────────────────────────
# ── Shared widgets ────────────────────────────────────────────────────
def make_scrolled_log(self):
sw = Gtk.ScrolledWindow()
@@ -197,54 +241,93 @@ class InstallerWindow(Adw.ApplicationWindow):
return box
# ── No Internet Screen ─────────────────────────────────────────────────
# ── Landing / Welcome Screen ───────────────────────────────────────────
def push_no_internet(self):
def push_landing(self):
"""First screen: always shown. Welcomes the user and checks connectivity."""
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
status = Adw.StatusPage()
status.set_title("No Internet Connection")
status.set_description(
"An active internet connection is required to install Sovran_SystemsOS.\n\n"
"Please connect an Ethernet cable or configure Wi-Fi,\n"
"then press Retry."
)
status.set_icon_name("network-offline-symbolic")
status.set_vexpand(True)
outer.append(status)
# Hero
hero = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
hero.set_margin_top(40)
hero.set_margin_bottom(16)
hero.set_halign(Gtk.Align.CENTER)
if os.path.exists(LOGO):
try:
img = Gtk.Image.new_from_file(LOGO)
img.set_pixel_size(320)
hero.append(img)
except Exception:
pass
title = Gtk.Label()
title.set_markup("<span size='xx-large' weight='heavy'>Welcome to Sovran_SystemsOS</span>")
title.set_margin_top(8)
hero.append(title)
sub = Gtk.Label()
sub.set_markup("<span size='large' style='italic' foreground='#888888'>Be Digitally Sovereign</span>")
hero.append(sub)
outer.append(hero)
sep = Gtk.Separator()
sep.set_margin_start(40)
sep.set_margin_end(40)
sep.set_margin_top(8)
outer.append(sep)
# Internet requirement notice
notice = Gtk.Label()
notice.set_markup(
"<span size='medium'>"
"Before installation begins, please ensure you have an <b>active internet connection</b>.\n"
"Sovran_SystemsOS downloads packages during installation and requires internet access\n"
"to complete the process. Connect via <b>Ethernet cable</b> or configure <b>Wi-Fi</b> now."
"</span>"
)
notice.set_justify(Gtk.Justification.CENTER)
notice.set_wrap(True)
notice.set_margin_top(20)
notice.set_margin_start(48)
notice.set_margin_end(48)
outer.append(notice)
# Inline offline warning banner (hidden by default)
self._offline_banner = Adw.Banner()
self._offline_banner.set_title(
"No internet connection detected. Please connect Ethernet or Wi-Fi and try again."
)
self._offline_banner.set_revealed(False)
self._offline_banner.set_margin_top(12)
self._offline_banner.set_margin_start(40)
self._offline_banner.set_margin_end(40)
outer.append(self._offline_banner)
outer.append(Gtk.Label(label="", vexpand=True))
# Check & Continue button
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
btn_box.set_halign(Gtk.Align.CENTER)
btn_box.set_margin_bottom(32)
retry_btn = Gtk.Button(label="Retry")
retry_btn.add_css_class("suggested-action")
retry_btn.add_css_class("pill")
retry_btn.connect("clicked", self.on_retry_internet)
btn_box.append(retry_btn)
connect_btn = Gtk.Button(label="Check Connection & Continue →")
connect_btn.add_css_class("suggested-action")
connect_btn.add_css_class("pill")
connect_btn.connect("clicked", self._on_landing_connect)
btn_box.append(connect_btn)
outer.append(btn_box)
self.push_page("No Internet", outer)
self.push_page("Sovran_SystemsOS Installer", outer)
def on_retry_internet(self, btn):
def _on_landing_connect(self, btn):
if check_internet():
# Pop the no-internet page and proceed to welcome
try:
self.nav.pop()
except Exception:
pass
self._offline_banner.set_revealed(False)
self.push_welcome()
else:
dlg = Adw.MessageDialog()
dlg.set_transient_for(self)
dlg.set_heading("Still Offline")
dlg.set_body(
"Could not reach the internet.\n"
"Please check your network connection and try again."
)
dlg.add_response("ok", "OK")
dlg.present()
self._offline_banner.set_revealed(True)
# ── Step 1: Welcome & Role ─────────────────────────────────────────────
@@ -266,7 +349,7 @@ class InstallerWindow(Adw.ApplicationWindow):
pass
title = Gtk.Label()
title.set_markup("<span size='xx-large' weight='heavy'>Sovran Systems</span>")
title.set_markup("<span size='xx-large' weight='heavy'>Sovran_SystemsOS</span>")
hero.append(title)
sub = Gtk.Label()
@@ -302,6 +385,23 @@ class InstallerWindow(Adw.ApplicationWindow):
"Node (Bitcoin-only)"),
]
# Detect internal (non-USB) drives to gate role availability
try:
raw = run(["lsblk", "-b", "-dno", "NAME,SIZE,TYPE,RO,TRAN", "-e", "7,11"])
internal_disks = []
for line in raw.splitlines():
parts = line.split()
if len(parts) >= 4 and parts[2] == "disk" and parts[3] == "0":
tran = parts[4] if len(parts) >= 5 else ""
if tran != "usb":
internal_disks.append(parts[0])
except Exception:
internal_disks = []
has_second_drive = len(internal_disks) >= 2
NEEDS_DATA_DRIVE = {"Server+Desktop", "Node (Bitcoin-only)"}
self._role_radios = []
radio_group = None
cards_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
@@ -311,14 +411,22 @@ class InstallerWindow(Adw.ApplicationWindow):
for label, desc, key in roles:
card = Adw.ActionRow()
card.set_title(label)
card.set_subtitle(desc)
available = has_second_drive or key not in NEEDS_DATA_DRIVE
if not available:
card.set_subtitle(desc + "\n⚠ Requires a second internal drive (not detected)")
card.set_sensitive(False)
else:
card.set_subtitle(desc)
radio = Gtk.CheckButton()
radio.set_name(key)
if radio_group is None:
radio.set_sensitive(available)
if radio_group is None and available:
radio_group = radio
radio.set_active(True)
else:
elif radio_group is not None:
radio.set_group(radio_group)
card.add_prefix(radio)
@@ -341,11 +449,12 @@ class InstallerWindow(Adw.ApplicationWindow):
if radio.get_active():
self.role = radio.get_name()
break
self.push_disk_confirm()
self.push_disk_detect()
# ── Step 2: Disk Confirm ───────────────────────────────────────────────
# ── Step 2a: Disk Detect ──────────────────────────────────────────────
def push_disk_confirm(self):
def push_disk_detect(self):
"""Detect internal drives and show the interactive disk selection page."""
try:
raw = run(["lsblk", "-b", "-dno", "NAME,SIZE,TYPE,RO,TRAN", "-e", "7,11"])
except Exception as e:
@@ -358,55 +467,252 @@ class InstallerWindow(Adw.ApplicationWindow):
if len(parts) >= 4 and parts[2] == "disk" and parts[3] == "0":
tran = parts[4] if len(parts) >= 5 else ""
if tran != "usb":
disks.append((parts[0], int(parts[1])))
disks.append((parts[0], int(parts[1]), tran))
if not disks:
self.show_error("No valid internal drives found. USB drives are excluded.")
return
disks.sort(key=lambda x: x[1])
self.boot_disk, self.boot_size = disks[0]
self.data_disk, self.data_size = None, None
self.push_disk_select(disks)
BYTES_2TB = 2 * 1024 ** 4
if len(disks) >= 2:
d, s = disks[-1]
if s >= BYTES_2TB:
self.data_disk, self.data_size = d, s
# ── Step 2b: Disk Select ──────────────────────────────────────────────
def push_disk_select(self, disks):
"""Interactive disk-selection page: pick OS drive and (optionally) data drive."""
BYTES_256GB = 256 * 1024 ** 3
BYTES_2TB = 2 * 10 ** 12
# Sort ascending by size so the default selection (index 0) is the smallest
disks = sorted(disks, key=lambda x: x[1])
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
scroll = Gtk.ScrolledWindow()
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scroll.set_vexpand(True)
inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
# ── OS Drive group ────────────────────────────────────────────
os_group = Adw.PreferencesGroup()
os_group.set_title("OS Drive (NixOS Boot + Root)")
os_group.set_description(
"Choose the drive for the NixOS installation. Minimum 256 GB required."
)
os_group.set_margin_top(24)
os_group.set_margin_start(40)
os_group.set_margin_end(40)
self._os_disk_radios = []
os_radio_group = None
for name, size, tran in disks:
row = Adw.ActionRow()
row.set_title(f"/dev/{name}")
type_label = tran.upper() if tran else "Disk"
meets = "✓ Meets 256 GB minimum" if size >= BYTES_256GB else "✗ Below 256 GB minimum"
row.set_subtitle(f"{human_size(size)} · {type_label}{meets}")
row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
radio = Gtk.CheckButton()
radio.set_name(name)
if os_radio_group is None:
os_radio_group = radio
radio.set_active(True)
else:
radio.set_group(os_radio_group)
row.add_suffix(radio)
row.set_activatable_widget(radio)
self._os_disk_radios.append(radio)
os_group.add(row)
inner.append(os_group)
# ── Data Drive group (skipped for Desktop Only) ───────────────
self._data_disk_radios = []
if self.role != "Desktop Only":
data_group = Adw.PreferencesGroup()
data_group.set_title("Bitcoin Timechain & Backups Drive")
data_group.set_description(
"💡 Tip: Always assign your LARGEST drive here. "
"The full Bitcoin timechain is over 700 GB and grows continuously — "
"a 2 TB or larger drive is required."
)
data_group.set_margin_top(20)
data_group.set_margin_start(40)
data_group.set_margin_end(40)
data_radio_group = None
# "None" option
none_row = Adw.ActionRow()
none_row.set_title("None (skip data drive)")
none_row.set_subtitle("Bitcoin node functionality will be unavailable")
none_radio = Gtk.CheckButton()
none_radio.set_name("")
data_radio_group = none_radio
none_radio.set_active(True)
none_row.add_suffix(none_radio)
none_row.set_activatable_widget(none_radio)
self._data_disk_radios.append(none_radio)
data_group.add(none_row)
for name, size, tran in disks:
row = Adw.ActionRow()
row.set_title(f"/dev/{name}")
type_label = tran.upper() if tran else "Disk"
meets = "✓ Meets 2 TB minimum" if size >= BYTES_2TB else "✗ Below 2 TB minimum"
row.set_subtitle(f"{human_size(size)} · {type_label}{meets}")
row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
radio = Gtk.CheckButton()
radio.set_name(name)
radio.set_group(data_radio_group)
row.add_suffix(radio)
row.set_activatable_widget(radio)
self._data_disk_radios.append(radio)
data_group.add(row)
inner.append(data_group)
scroll.set_child(inner)
outer.append(scroll)
outer.append(self.nav_row(
back_label="← Back",
back_cb=lambda b: self.nav.pop(),
next_label="Next →",
next_cb=lambda b: self._on_disk_select_next(disks),
))
self.push_page("Select Drives", outer, show_back=True)
def _on_disk_select_next(self, disks):
"""Validate the user's disk selections and advance to the ERASE confirmation."""
BYTES_256GB = 256 * 1024 ** 3
BYTES_2TB = 2 * 10 ** 12
size_map = {name: size for name, size, _ in disks}
# Read OS disk selection
os_name = None
for radio in self._os_disk_radios:
if radio.get_active():
os_name = radio.get_name()
break
# Read data disk selection (empty string = None chosen)
data_name = None
if self._data_disk_radios:
for radio in self._data_disk_radios:
if radio.get_active():
sel = radio.get_name()
data_name = sel if sel else None
break
os_size = size_map.get(os_name, 0)
data_size = size_map.get(data_name, 0) if data_name else 0
# Validate OS drive size
if os_size < BYTES_256GB:
dlg = Adw.MessageDialog()
dlg.set_transient_for(self)
dlg.set_heading("OS Drive Too Small")
dlg.set_body(
f"The selected OS drive (/dev/{os_name}, {human_size(os_size)}) "
f"does not meet the 256 GB minimum. Please choose a larger drive."
)
dlg.add_response("ok", "OK")
dlg.present()
return
# Validate data drive size (when one was selected)
if data_name and data_size < BYTES_2TB:
dlg = Adw.MessageDialog()
dlg.set_transient_for(self)
dlg.set_heading("Bitcoin Drive Too Small")
dlg.set_body(
f"The selected Bitcoin Timechain & Backups drive "
f"(/dev/{data_name}, {human_size(data_size)}) "
f"does not meet the 2 TB minimum. "
f"Please choose a larger drive or select \"None\"."
)
dlg.add_response("ok", "OK")
dlg.present()
return
# Validate no duplicate selection
if data_name and data_name == os_name:
dlg = Adw.MessageDialog()
dlg.set_transient_for(self)
dlg.set_heading("Same Drive Selected Twice")
dlg.set_body(
"You cannot use the same drive for both the OS and "
"Bitcoin Timechain & Backups. Please choose different drives."
)
dlg.add_response("ok", "OK")
dlg.present()
return
# Commit selections
self.boot_disk = os_name
self.boot_size = os_size
self.data_disk = data_name
self.data_size = data_size if data_name else None
self.push_disk_confirm()
# ── Step 2c: Disk Confirm (ERASE confirmation) ────────────────────────
def push_disk_confirm(self):
"""Show the selected drives and ask the user to type ERASE to confirm."""
self.data_drive_has_timechain = False
if self.data_disk:
data_path = f"/dev/{self.data_disk}"
self.data_drive_has_timechain = self.detect_existing_timechain_data(data_path)
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
# Disk info group
disk_group = Adw.PreferencesGroup()
disk_group.set_title("Drives to be erased")
if self.data_disk and self.data_drive_has_timechain:
disk_group.set_title("OS drive to be erased (data drive preserved)")
else:
disk_group.set_title("Drives to be erased")
disk_group.set_margin_top(24)
disk_group.set_margin_start(40)
disk_group.set_margin_end(40)
boot_row = Adw.ActionRow()
boot_row.set_title("Boot Disk")
boot_row.set_title("OS Disk")
boot_row.set_subtitle(f"/dev/{self.boot_disk}{human_size(self.boot_size)}")
boot_row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
disk_group.add(boot_row)
if self.data_disk:
data_row = Adw.ActionRow()
data_row.set_title("Data Disk")
data_row.set_title("Bitcoin Timechain & Backups Disk")
data_row.set_subtitle(f"/dev/{self.data_disk}{human_size(self.data_size)}")
data_row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
disk_group.add(data_row)
else:
no_row = Adw.ActionRow()
no_row.set_title("Data Disk")
no_row.set_subtitle("None detected (requires 2 TB or larger)")
no_row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
disk_group.add(no_row)
if self.data_drive_has_timechain:
note_row = Adw.ActionRow()
note_row.set_title(f"Existing Bitcoin timechain detected on /dev/{self.data_disk}")
note_row.set_subtitle("Data will be preserved and mounted as-is.")
note_row.add_prefix(symbolic_icon("emblem-ok-symbolic"))
disk_group.add(note_row)
outer.append(disk_group)
# Warning banner
banner = Adw.Banner()
banner.set_title("⚠ All data on the above disk(s) will be permanently destroyed.")
if self.data_disk and self.data_drive_has_timechain:
banner.set_title("⚠ All data on the OS disk will be permanently destroyed. Existing Bitcoin data disk will be preserved.")
else:
banner.set_title("⚠ All data on the above disk(s) will be permanently destroyed.")
banner.set_revealed(True)
banner.set_margin_top(16)
banner.set_margin_start(40)
@@ -459,7 +765,6 @@ class InstallerWindow(Adw.ApplicationWindow):
status = Adw.StatusPage()
status.set_title(title)
status.set_description(subtitle)
status.set_icon_name("emblem-synchronizing-symbolic")
status.set_vexpand(False)
outer.append(status)
@@ -489,38 +794,123 @@ class InstallerWindow(Adw.ApplicationWindow):
# ── Worker: partition ─────────────────────────────────────────────────
def partition_path(self, dev_path, num):
return f"{dev_path}p{num}" if "nvme" in dev_path else f"{dev_path}{num}"
def detect_existing_timechain_data(self, data_path, buf=None):
data_p1 = self.partition_path(data_path, 1)
if not os.path.exists(data_p1):
return False
label = ""
for cmd in (
["sudo", "lsblk", "-no", "LABEL", data_p1],
["sudo", "blkid", "-o", "value", "-s", "LABEL", data_p1],
):
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode == 0:
stdout = proc.stdout.strip()
label = stdout.splitlines()[0] if stdout else ""
if label:
break
if label != "BTCEcoandBackup":
return False
check_mount = tempfile.mkdtemp(prefix="sovran-installer-data-check-")
mounted = False
try:
run(["sudo", "mount", "-o", "ro", data_p1, check_mount])
mounted = True
has_bitcoin = os.path.isdir(f"{check_mount}/BTCEcoandBackup/Bitcoin_Node")
has_electrs = os.path.isdir(f"{check_mount}/BTCEcoandBackup/Electrs_Data")
if has_bitcoin and has_electrs:
if buf is not None:
GLib.idle_add(
append_text,
buf,
"=== Existing Bitcoin timechain detected on data drive — preserving data ===\n",
)
return True
return False
except Exception as e:
log(f"Timechain detection failed for {data_p1} at mount/check step ({check_mount}): {e}")
return False
finally:
if mounted:
subprocess.run(["sudo", "umount", check_mount], capture_output=True, text=True)
subprocess.run(["sudo", "rmdir", check_mount], capture_output=True, text=True)
def do_partition(self, buf):
boot_path = f"/dev/{self.boot_disk}"
data_path = f"/dev/{self.data_disk}" if self.data_disk else None
self.data_drive_has_timechain = False
# ── Wipe disk(s) to clear stale GPT/MBR data before disko ──
if data_path:
self.data_drive_has_timechain = self.detect_existing_timechain_data(data_path, buf)
# ── Wipe disk(s) ──
GLib.idle_add(append_text, buf, "=== Wiping disk(s) ===\n")
run_stream(["sudo", "sgdisk", "--zap-all", boot_path], buf)
run_stream(["sudo", "wipefs", "--all", "--force", boot_path], buf)
if self.data_disk:
data_path = f"/dev/{self.data_disk}"
if data_path and not self.data_drive_has_timechain:
run_stream(["sudo", "sgdisk", "--zap-all", data_path], buf)
run_stream(["sudo", "wipefs", "--all", "--force", data_path], buf)
# Inform the kernel of the wiped partition tables
run_stream(["sudo", "partprobe", boot_path], buf)
if self.data_disk:
if data_path and not self.data_drive_has_timechain:
run_stream(["sudo", "partprobe", data_path], buf)
# Short settle so the kernel finishes re-reading
time.sleep(2)
# ── Now run disko on a clean disk ──
GLib.idle_add(append_text, buf, "\n=== Partitioning drives ===\n")
cmd = [
"sudo", "disko", "--mode", "disko",
f"{FLAKE}/iso/disko.nix",
"--arg", "device", f'"{boot_path}"'
]
if self.data_disk:
cmd += ["--arg", "dataDevice", f'"/dev/{self.data_disk}"']
run_stream(cmd, buf)
# ── Partition boot disk: 512M ESP + rest as root ──
GLib.idle_add(append_text, buf, "\n=== Partitioning boot disk ===\n")
run_stream(["sudo", "sgdisk",
"-n", "1:1M:+512M", "-t", "1:EF00", "-c", "1:ESP",
"-n", "2:0:0", "-t", "2:8300", "-c", "2:root",
boot_path], buf)
run_stream(["sudo", "partprobe", boot_path], buf)
time.sleep(2)
# ── Partition data disk (if selected) ──
if data_path and not self.data_drive_has_timechain:
GLib.idle_add(append_text, buf, "\n=== Partitioning data disk ===\n")
run_stream(["sudo", "sgdisk",
"-n", "1:1M:0", "-t", "1:8300", "-c", "1:primary",
data_path], buf)
run_stream(["sudo", "partprobe", data_path], buf)
time.sleep(2)
# ── Format partitions ──
GLib.idle_add(append_text, buf, "\n=== Formatting partitions ===\n")
boot_p1 = self.partition_path(boot_path, 1)
boot_p2 = self.partition_path(boot_path, 2)
run_stream(["sudo", "mkfs.vfat", "-F", "32", boot_p1], buf)
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "sovran_systemsos", boot_p2], buf)
if data_path and not self.data_drive_has_timechain:
data_p1 = self.partition_path(data_path, 1)
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "BTCEcoandBackup", data_p1], buf)
# ── Mount filesystems ──
GLib.idle_add(append_text, buf, "\n=== Mounting filesystems ===\n")
run_stream(["sudo", "mount", boot_p2, "/mnt"], buf)
run_stream(["sudo", "mkdir", "-p", "/mnt/boot/efi"], buf)
run_stream(["sudo", "mount", "-o", "umask=0077,defaults", boot_p1, "/mnt/boot/efi"], buf)
if data_path:
data_p1 = self.partition_path(data_path, 1)
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive"], buf)
run_stream(["sudo", "mount", data_p1, "/mnt/run/media/Second_Drive"], buf)
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"], buf)
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/Electrs_Data"], buf)
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/NixOS_Snapshot_Backup"], buf)
GLib.idle_add(append_text, buf, "\n=== Generating hardware config ===\n")
run_stream(["sudo", "nixos-generate-config", "--root", "/mnt"], buf)
@@ -558,8 +948,9 @@ class InstallerWindow(Adw.ApplicationWindow):
if proc.returncode != 0:
raise RuntimeError(f"Failed to write role-state.nix: {proc.stderr}")
run(["sudo", "cp", "/mnt/etc/nixos/custom.template.nix", "/mnt/etc/nixos/custom.nix"])
run(["sudo", "chmod", "644", "/mnt/etc/nixos/custom.nix"])
# ── Step 4: Ready to install ──────────────────────────────────────────
# ── Step 4: Ready to install ──────────────────────────────────────────
def push_ready(self):
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
@@ -567,7 +958,6 @@ class InstallerWindow(Adw.ApplicationWindow):
status = Adw.StatusPage()
status.set_title("Drives Ready")
status.set_description("Your drives have been partitioned successfully.")
status.set_icon_name("emblem-ok-symbolic")
status.set_vexpand(True)
details = Adw.PreferencesGroup()
@@ -611,7 +1001,7 @@ class InstallerWindow(Adw.ApplicationWindow):
outer.append(self.nav_row(
next_label="Install Now",
next_cb=lambda b: self.push_progress(
"Installing Sovran SystemsOS",
"Installing Sovran_SystemsOS",
"Building and installing your system. Please wait...",
self.do_install
)
@@ -629,65 +1019,128 @@ class InstallerWindow(Adw.ApplicationWindow):
if not os.path.exists(f):
raise RuntimeError(f"Required file missing: {f}")
# The flake.nix imports /etc/nixos/role-state.nix and /etc/nixos/custom.nix
# as absolute paths. With --impure, Nix resolves these on the live ISO host,
# not under /mnt. Copy them so they exist on the host filesystem too.
GLib.idle_add(append_text, buf, "Copying config files to host /etc/nixos for flake evaluation...\n")
run(["sudo", "mkdir", "-p", "/etc/nixos"])
run(["sudo", "cp", "/mnt/etc/nixos/role-state.nix", "/etc/nixos/role-state.nix"])
run(["sudo", "cp", "/mnt/etc/nixos/custom.nix", "/etc/nixos/custom.nix"])
run(["sudo", "cp", "/mnt/etc/nixos/hardware-configuration.nix", "/etc/nixos/hardware-configuration.nix"])
run_stream([
"sudo", "nixos-install",
"--root", "/mnt",
"--flake", "/mnt/etc/nixos#nixos"
"--flake", "/mnt/etc/nixos#nixos",
"--no-root-password",
"--impure"
], buf)
# Clean up /mnt/etc/nixos — only 5 files are needed post-install.
# configuration.nix and modules/ were needed during nixos-install
# for flake evaluation, but are now baked into the Nix store via
# self.nixosModules.Sovran_SystemsOS.
GLib.idle_add(append_text, buf, "Cleaning up /mnt/etc/nixos...\n")
keep = {"flake.nix", "flake.lock", "hardware-configuration.nix",
"role-state.nix", "custom.nix"}
nixos_dir = "/mnt/etc/nixos"
for entry in os.listdir(nixos_dir):
if entry not in keep:
path = os.path.join(nixos_dir, entry)
run(["sudo", "rm", "-rf", path])
GLib.idle_add(append_text, buf, "Writing deployed flake.nix...\n")
proc = subprocess.run(
["sudo", "tee", "/mnt/etc/nixos/flake.nix"],
input=DEPLOYED_FLAKE,
capture_output=True,
text=True,
)
log(proc.stdout)
if proc.returncode != 0:
log(proc.stderr)
raise RuntimeError(proc.stderr.strip() or "Failed to write deployed flake.nix")
GLib.idle_add(append_text, buf, "Locking flake to staging-dev...\n")
run_stream(["sudo", "nix", "--extra-experimental-features", "nix-command flakes",
"flake", "lock", "/mnt/etc/nixos"], buf)
# Generate diceware passwords and write them to the installed system
GLib.idle_add(append_text, buf, "Setting up user passwords...\n")
self.free_password = generate_diceware_password()
root_password = generate_diceware_password()
run(["sudo", "mkdir", "-p", "/mnt/var/lib/secrets"])
run(["sudo", "chmod", "700", "/mnt/var/lib/secrets"])
proc = subprocess.run(
["sudo", "tee", "/mnt/var/lib/secrets/free-password"],
input=self.free_password, capture_output=True, text=True
)
if proc.returncode != 0:
log(proc.stderr)
raise RuntimeError(proc.stderr.strip() or "Failed to write free-password")
run(["sudo", "chmod", "600", "/mnt/var/lib/secrets/free-password"])
proc = subprocess.run(
["sudo", "tee", "/mnt/var/lib/secrets/root-password"],
input=root_password, capture_output=True, text=True
)
if proc.returncode != 0:
log(proc.stderr)
raise RuntimeError(proc.stderr.strip() or "Failed to write root-password")
run(["sudo", "chmod", "600", "/mnt/var/lib/secrets/root-password"])
GLib.idle_add(self.push_complete)
# ── Step 6: Complete ───────────────────────────────────────────────────
# ── Complete ───────────────────────────────────────────────────────────
def push_complete(self):
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
status = Adw.StatusPage()
status.set_title("Installation Complete!")
status.set_description("Welcome to Sovran SystemsOS.")
status.set_icon_name("emblem-ok-symbolic")
status.set_vexpand(True)
status.set_description("Before rebooting, write down your login password.")
status.set_icon_name("dialog-password-symbolic")
outer.append(status)
creds_group = Adw.PreferencesGroup()
creds_group.set_title("⚠ Write down your login details before rebooting")
creds_group.set_margin_start(40)
creds_group.set_margin_end(40)
user_row = Adw.ActionRow()
user_row.set_title("Username")
user_row.set_subtitle("free")
creds_group.add(user_row)
pass_row = Adw.ActionRow()
pass_row.set_title("Password")
pass_row.set_subtitle("free")
creds_group.add(pass_row)
note_row = Adw.ActionRow()
note_row.set_title("App Passwords")
note_row.set_subtitle(
"After rebooting, all app passwords (Nextcloud, Bitcoin, Matrix, etc.) "
"will be saved to a secure PDF in your Documents folder."
pw_frame = Gtk.Frame()
pw_frame.set_margin_start(60)
pw_frame.set_margin_end(60)
pw_label = Gtk.Label()
pw_label.set_markup(
f"<span font_family='monospace' size='xx-large' weight='bold'>"
f"{GLib.markup_escape_text(self.free_password)}</span>"
)
creds_group.add(note_row)
pw_label.set_selectable(True)
pw_label.set_margin_top(16)
pw_label.set_margin_bottom(16)
pw_frame.set_child(pw_label)
outer.append(pw_frame)
content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
content_box.append(status)
content_box.append(creds_group)
outer.append(content_box)
warning = Gtk.Label()
warning.set_markup(
"<span foreground='#e8a838' size='medium' weight='bold'>"
"⚠ Write this password down now.\n"
"You will need it to log in to your computer and the Sovran Hub.\n"
"This password cannot be recovered.</span>"
)
warning.set_justify(Gtk.Justification.CENTER)
warning.set_wrap(True)
warning.set_margin_top(20)
warning.set_margin_start(48)
warning.set_margin_end(48)
outer.append(warning)
reboot_btn = Gtk.Button(label="Reboot Now")
outer.append(Gtk.Label(label="", vexpand=True))
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
btn_box.set_halign(Gtk.Align.CENTER)
btn_box.set_margin_bottom(32)
reboot_btn = Gtk.Button(label="I Have Written Down My Password — Reboot Now")
reboot_btn.add_css_class("suggested-action")
reboot_btn.add_css_class("pill")
reboot_btn.add_css_class("destructive-action")
reboot_btn.connect("clicked", lambda b: subprocess.run(["sudo", "reboot"]))
nav = Gtk.Box()
nav.set_margin_bottom(24)
nav.set_margin_end(40)
nav.set_halign(Gtk.Align.END)
nav.append(reboot_btn)
outer.append(nav)
btn_box.append(reboot_btn)
outer.append(btn_box)
self.push_page("Complete", outer)
return False
-44
View File
@@ -1,44 +0,0 @@
{ pkgs, lib }:
pkgs.stdenv.mkDerivation {
pname = "sovran-plymouth-theme";
version = "1.0";
src = ./.;
installPhase = ''
mkdir -p $out/share/plymouth/themes/sovran
cp ${./assets/splash-logo.png} $out/share/plymouth/themes/sovran/logo.png
cat > $out/share/plymouth/themes/sovran/sovran.plymouth <<'EOF'
[Plymouth Theme]
Name=Sovran Systems
Description=Sovran Systems Splash
ModuleName=script
[script]
ImageDir=/share/plymouth/themes/sovran
ScriptFile=/share/plymouth/themes/sovran/sovran.script
EOF
cat > $out/share/plymouth/themes/sovran/sovran.script <<'EOF'
# Background color: #CFFFD7 (RGB 207,255,215)
bg_r = 207/255.0
bg_g = 255/255.0
bg_b = 215/255.0
Window.SetBackgroundTopColor (bg_r, bg_g, bg_b);
Window.SetBackgroundBottomColor (bg_r, bg_g, bg_b);
logo = Image("logo.png");
logo_sprite = Sprite(logo);
logo_sprite.SetX((Window.GetWidth() - logo.GetWidth()) / 2);
logo_sprite.SetY((Window.GetHeight() - logo.GetHeight()) / 2);
spinner = Sprite();
spinner.SetImage(Spinner());
spinner.SetX((Window.GetWidth() - spinner.GetImage().GetWidth()) / 2);
spinner.SetY((Window.GetHeight() + logo.GetHeight()) / 2 + 20);
EOF
'';
}
View File
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env bash
# sovran-install-headless.sh — Non-interactive remote installer for Sovran_SystemsOS
usage() {
cat <<'USAGE'
Usage: sovran-install-headless.sh [OPTIONS]
Options:
--disk /dev/sda Target OS disk (required)
--data-disk /dev/sdb Data disk for Bitcoin (optional)
--role server|desktop|node Installation role (default: server)
--deploy-key "ssh-ed25519 AAAA..." SSH pubkey for remote access after install
--headscale-server URL Headscale login server for post-install Tailnet
--headscale-key KEY Headscale pre-auth key for the installed OS
USAGE
}
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────────────
DISK=""
DATA_DISK=""
ROLE="server"
DEPLOY_KEY=""
HEADSCALE_SERVER=""
HEADSCALE_KEY=""
DATA_DISK_HAS_TIMECHAIN=false
FLAKE="/etc/sovran/flake"
LOG="/tmp/sovran-headless-install.log"
BYTES_256GB=$((256 * 1024 * 1024 * 1024))
BYTES_2TB=$((2 * 1000 * 1000 * 1000 * 1000))
# ── Logging ───────────────────────────────────────────────────────────────────
exec > >(tee -a "$LOG") 2>&1
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
die() {
log "ERROR: $*"
exit 1
}
# ── Argument parsing ─────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--disk) DISK="$2"; shift 2 ;;
--data-disk) DATA_DISK="$2"; shift 2 ;;
--role) ROLE="$2"; shift 2 ;;
--deploy-key) DEPLOY_KEY="$2"; shift 2 ;;
--headscale-server) HEADSCALE_SERVER="$2"; shift 2 ;;
--headscale-key) HEADSCALE_KEY="$2"; shift 2 ;;
-h|--help)
usage
exit 0
;;
*) die "Unknown argument: $1" ;;
esac
done
# ── Validate required arguments ───────────────────────────────────────────────
[[ -n "$DISK" ]] || die "--disk is required"
case "$ROLE" in
server|desktop|node) ;;
*) die "--role must be one of: server, desktop, node" ;;
esac
# ── Validate disk existence and size ─────────────────────────────────────────
log "=== Validating disks ==="
[[ -b "$DISK" ]] || die "OS disk not found: $DISK"
disk_size_bytes() {
local dev="$1"
lsblk -b -dno SIZE "$dev" 2>/dev/null || echo 0
}
OS_SIZE=$(disk_size_bytes "$DISK")
log "OS disk $DISK: $OS_SIZE bytes"
[[ "$OS_SIZE" -ge "$BYTES_256GB" ]] \
|| die "OS disk $DISK is too small ($(( OS_SIZE / 1024 / 1024 / 1024 )) GB). Minimum is 256 GB."
if [[ -n "$DATA_DISK" ]]; then
[[ -b "$DATA_DISK" ]] || die "Data disk not found: $DATA_DISK"
[[ "$DATA_DISK" != "$DISK" ]] || die "OS disk and data disk cannot be the same device"
DATA_SIZE=$(disk_size_bytes "$DATA_DISK")
log "Data disk $DATA_DISK: $DATA_SIZE bytes"
[[ "$DATA_SIZE" -ge "$BYTES_2TB" ]] \
|| die "Data disk $DATA_DISK is too small ($(( DATA_SIZE / 1024 / 1024 / 1024 )) GB). Minimum is 2 TB."
fi
# ── Helper: partition suffix ──────────────────────────────────────────────────
part_suffix() {
local dev="$1" n="$2"
if [[ "$dev" == *nvme* ]]; then
echo "${dev}p${n}"
else
echo "${dev}${n}"
fi
}
# ── Detect existing Bitcoin timechain data on data disk ───────────────────────
if [[ -n "$DATA_DISK" ]]; then
DATA_P1=$(part_suffix "$DATA_DISK" 1)
if [[ -b "$DATA_P1" ]]; then
DATA_LABEL=$(lsblk -no LABEL "$DATA_P1" 2>/dev/null | head -n1 || true)
if [[ -z "$DATA_LABEL" ]]; then
DATA_LABEL=$(blkid -o value -s LABEL "$DATA_P1" 2>/dev/null || true)
fi
if [[ "$DATA_LABEL" == "BTCEcoandBackup" ]]; then
CHECK_MOUNT=$(mktemp -d /tmp/sovran-data-check.XXXXXX)
if mount -o ro "$DATA_P1" "$CHECK_MOUNT" 2>/dev/null; then
if [[ -d "$CHECK_MOUNT/BTCEcoandBackup/Bitcoin_Node" && -d "$CHECK_MOUNT/BTCEcoandBackup/Electrs_Data" ]]; then
DATA_DISK_HAS_TIMECHAIN=true
log "Existing Bitcoin timechain detected on data drive — preserving data"
fi
umount "$CHECK_MOUNT" || true
fi
rmdir "$CHECK_MOUNT" 2>/dev/null || true
fi
fi
fi
# ── Step 1: Wipe disks ────────────────────────────────────────────────────────
log "=== Wiping disk(s) ==="
sgdisk --zap-all "$DISK"
wipefs --all --force "$DISK"
if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
sgdisk --zap-all "$DATA_DISK"
wipefs --all --force "$DATA_DISK"
fi
partprobe "$DISK"
[[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]] && partprobe "$DATA_DISK"
sleep 2
# ── Step 2: Partition OS disk ─────────────────────────────────────────────────
log "=== Partitioning OS disk ==="
sgdisk \
-n "1:1M:+512M" -t "1:EF00" -c "1:ESP" \
-n "2:0:0" -t "2:8300" -c "2:root" \
"$DISK"
partprobe "$DISK"
sleep 2
# ── Step 3: Partition data disk (if present) ──────────────────────────────────
if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
log "=== Partitioning data disk ==="
sgdisk \
-n "1:1M:0" -t "1:8300" -c "1:primary" \
"$DATA_DISK"
partprobe "$DATA_DISK"
sleep 2
fi
# ── Step 4: Format partitions ─────────────────────────────────────────────────
log "=== Formatting partitions ==="
BOOT_P1=$(part_suffix "$DISK" 1)
BOOT_P2=$(part_suffix "$DISK" 2)
mkfs.vfat -F 32 "$BOOT_P1"
mkfs.ext4 -F -L sovran_systemsos "$BOOT_P2"
if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
DATA_P1=$(part_suffix "$DATA_DISK" 1)
mkfs.ext4 -F -L BTCEcoandBackup "$DATA_P1"
fi
# ── Step 5: Mount filesystems ─────────────────────────────────────────────────
log "=== Mounting filesystems ==="
mount "$BOOT_P2" /mnt
mkdir -p /mnt/boot/efi
mount -o umask=0077,defaults "$BOOT_P1" /mnt/boot/efi
if [[ -n "$DATA_DISK" ]]; then
DATA_P1=$(part_suffix "$DATA_DISK" 1)
mkdir -p /mnt/run/media/Second_Drive
mount "$DATA_P1" /mnt/run/media/Second_Drive
# ── Step 6: Create Bitcoin data directories ─────────────────────────────
log "=== Creating Bitcoin data directories ==="
mkdir -p /mnt/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node
mkdir -p /mnt/run/media/Second_Drive/BTCEcoandBackup/Electrs_Data
mkdir -p /mnt/run/media/Second_Drive/BTCEcoandBackup/NixOS_Snapshot_Backup
fi
# ── Step 7: Generate hardware config ─────────────────────────────────────────
log "=== Generating hardware config ==="
nixos-generate-config --root /mnt
# ── Step 8: Copy flake source ─────────────────────────────────────────────────
log "=== Copying flake to /mnt ==="
cp /mnt/etc/nixos/hardware-configuration.nix /tmp/hardware-configuration.nix
rm -rf /mnt/etc/nixos/
mkdir -p /mnt/etc/nixos
cp -a "${FLAKE}/." /mnt/etc/nixos/
cp /tmp/hardware-configuration.nix /mnt/etc/nixos/hardware-configuration.nix
# ── Step 9: Write role-state.nix ─────────────────────────────────────────────
log "=== Writing role config ==="
case "$ROLE" in
server)
IS_SERVER=true; IS_DESKTOP=false; IS_NODE=false ;;
desktop)
IS_SERVER=false; IS_DESKTOP=true; IS_NODE=false ;;
node)
IS_SERVER=false; IS_DESKTOP=false; IS_NODE=true ;;
esac
cat > /mnt/etc/nixos/role-state.nix <<EOF
# THIS FILE IS AUTO-GENERATED BY THE INSTALLER. DO NOT EDIT.
{ config, lib, ... }:
{
sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault ${IS_SERVER};
sovran_systemsOS.roles.desktop = lib.mkDefault ${IS_DESKTOP};
sovran_systemsOS.roles.node = lib.mkDefault ${IS_NODE};
}
EOF
# ── Step 10: Write custom.nix with deploy config ──────────────────────────────
log "=== Writing custom.nix ==="
if [[ -n "$DEPLOY_KEY" || -n "$HEADSCALE_SERVER" ]]; then
{
echo '{ config, lib, ... }:'
echo '{'
echo ' sovran_systemsOS.deploy = {'
echo ' enable = true;'
[[ -n "$DEPLOY_KEY" ]] && echo " authorizedKey = \"${DEPLOY_KEY}\";"
[[ -n "$HEADSCALE_SERVER" ]] && echo " headscaleServer = \"${HEADSCALE_SERVER}\";"
echo ' };'
echo '}'
} > /mnt/etc/nixos/custom.nix
else
cp /mnt/etc/nixos/custom.template.nix /mnt/etc/nixos/custom.nix
chmod 644 /mnt/etc/nixos/custom.nix
fi
# ── Write Headscale auth key if provided ─────────────────────────────────────
if [[ -n "$HEADSCALE_KEY" ]]; then
mkdir -p /mnt/var/lib/secrets
echo "$HEADSCALE_KEY" > /mnt/var/lib/secrets/headscale-authkey
chmod 600 /mnt/var/lib/secrets/headscale-authkey
log "Headscale auth key written to /mnt/var/lib/secrets/headscale-authkey"
fi
# ── Step 11: Copy configs to host for flake evaluation ───────────────────────
log "=== Copying config files to host /etc/nixos for flake evaluation ==="
mkdir -p /etc/nixos
cp /mnt/etc/nixos/role-state.nix /etc/nixos/role-state.nix
cp /mnt/etc/nixos/custom.nix /etc/nixos/custom.nix
cp /mnt/etc/nixos/hardware-configuration.nix /etc/nixos/hardware-configuration.nix
# ── Step 12: Run nixos-install ────────────────────────────────────────────────
log "=== Running nixos-install ==="
nixos-install \
--root /mnt \
--flake /mnt/etc/nixos#nixos \
--no-root-password \
--impure
log "=== Installation complete! ==="
log "You can now reboot into Sovran_SystemsOS."
log "After reboot, the machine will be accessible via SSH on port 22 (if --deploy-key was provided)."
[[ -n "$HEADSCALE_SERVER" ]] && \
log "Tailscale will connect to Headscale at ${HEADSCALE_SERVER} on first boot."
@@ -1,24 +0,0 @@
{config, pkgs, lib, ...}:
{
systemd.services.Sovran_SystemsOS_File_Fixes_And_New_Services = {
unitConfig = {
After = "btcpayserver.service";
Requires = "network-online.target";
};
serviceConfig = {
ExecStartPre= "/run/current-system/sw/bin/sleep 30";
ExecStart = "/run/current-system/sw/bin/wget https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/raw/branch/main/file_fixes_and_new_services/Sovran_SystemsOS_File_Fixes_And_New_Services.sh -O /home/free/Downloads/Sovran_SystemsOS_File_Fixes_And_New_Services.sh ; /run/current-system/sw/bin/bash /home/free/Downloads/Sovran_SystemsOS_File_Fixes_And_New_Services.sh";
RemainAfterExit = "yes";
User = "root";
Type = "oneshot";
};
wantedBy = [ "multi-user.target" ];
};
}
+43 -3
View File
@@ -55,7 +55,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
};
services.btcpayserver = {
enable = true;
enable = config.sovran_systemsOS.web.btcpayserver;
};
services.btcpayserver.lightningBackend = "lnd";
@@ -69,8 +69,48 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
};
nix-bitcoin.useVersionLockedPkgs = false;
sovran_systemsOS.domainRequirements = [
systemd.services.bitcoind = {
requires = [ "run-media-Second_Drive.mount" ];
after = [ "run-media-Second_Drive.mount" ];
serviceConfig.PrivateUsers = lib.mkForce false;
};
systemd.services.electrs = {
requires = lib.mkForce [ "run-media-Second_Drive.mount" ];
after = [ "run-media-Second_Drive.mount" "bitcoind.service" ];
wants = [ "bitcoind.service" ];
};
systemd.services.lnd = {
wants = [ "bitcoind.service" ];
# requires for bitcoind set by nix-bitcoin; mkForce removes it
requires = lib.mkForce [ ];
};
systemd.services.sovran-btc-permissions = {
description = "Fix Bitcoin/Electrs data directory ownership on second drive";
wantedBy = [ "multi-user.target" ];
after = [ "run-media-Second_Drive.mount" ];
before = [ "bitcoind.service" "electrs.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
if [ -d /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node ]; then
chown -R bitcoin:bitcoin /run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node
fi
if [ -d /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data ]; then
chown -R electrs:electrs /run/media/Second_Drive/BTCEcoandBackup/Electrs_Data
fi
'';
};
networking.firewall.allowedTCPPorts = [ 3051 ];
networking.firewall.allowedUDPPorts = [ 3051 ];
sovran_systemsOS.domainRequirements = [
{ name = "btcpayserver"; label = "BTCPay Server"; example = "pay.yourdomain.com"; }
];
}
+79 -4
View File
@@ -2,13 +2,43 @@
let
exposeBtcpay = config.sovran_systemsOS.web.btcpayserver;
extraVhosts = config.sovran_systemsOS.caddy.extraVirtualHosts;
# True when any service needs HTTPS/ACME (domain-based vhosts)
needsHttpsPorts =
config.sovran_systemsOS.web.btcpayserver
|| config.sovran_systemsOS.services.synapse
|| config.sovran_systemsOS.services.wordpress
|| config.sovran_systemsOS.services.nextcloud
|| config.sovran_systemsOS.services.vaultwarden
|| config.sovran_systemsOS.features.haven
|| config.sovran_systemsOS.features.element-calling;
in
{
services.caddy = {
enable = true;
user = "caddy";
group = "root";
configFile = "/run/caddy/Caddyfile";
};
# Only open ports 80/443 when at least one domain-based service is active
networking.firewall.allowedTCPPorts = lib.mkIf needsHttpsPorts [ 80 443 ];
networking.firewall.allowedUDPPorts = lib.mkIf needsHttpsPorts [ 80 443 ];
systemd.tmpfiles.rules = [
"d /var/lib/domains 0755 caddy root -"
];
# Override ExecStart + ExecReload to point at the runtime-generated Caddyfile
systemd.services.caddy.serviceConfig = {
ExecStart = lib.mkForce [
""
"${pkgs.caddy}/bin/caddy run --config /run/caddy/Caddyfile --adapter caddyfile"
];
ExecReload = lib.mkForce [
""
"${pkgs.caddy}/bin/caddy reload --config /run/caddy/Caddyfile --adapter caddyfile --force"
];
};
systemd.services.caddy-generate-config = {
@@ -39,12 +69,20 @@ in
HAVEN=$(read_domain haven)
ACME_EMAIL=$(read_domain sslemail)
# Start with global config
# Start with global config use ACME only when domain-based services are active
${if needsHttpsPorts then ''
cat > /run/caddy/Caddyfile <<EOF
{
email $ACME_EMAIL
}
EOF
'' else ''
cat > /run/caddy/Caddyfile <<EOF
{
auto_https off
}
EOF
''}
# Matrix
if [ -n "$MATRIX" ]; then
@@ -72,7 +110,7 @@ EOF
$WORDPRESS {
encode gzip zstd
root * /var/lib/www/wordpress
php_fastcgi unix//run/phpfpm/mypool.sock
php_fastcgi unix//run/phpfpm/wordpress.sock
file_server browse
}
EOF
@@ -85,7 +123,7 @@ EOF
$NEXTCLOUD {
encode gzip zstd
root * /var/lib/www/nextcloud
php_fastcgi unix//run/phpfpm/mypool.sock {
php_fastcgi unix//run/phpfpm/nextcloud.sock {
trusted_proxies private_ranges
}
file_server
@@ -144,6 +182,43 @@ $HAVEN {
}
EOF
fi
# Sovran Hub (LAN access via mDNS)
cat >> /run/caddy/Caddyfile <<EOF
http://sovransystemsos.local {
reverse_proxy localhost:8937
header {
Clear-Site-Data "\"cache\""
Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
Pragma "no-cache"
Expires "0"
}
}
EOF
# RTL (LAN access)
cat >> /run/caddy/Caddyfile <<EOF
:3051 {
reverse_proxy :3050
encode gzip zstd
}
EOF
# Mempool (LAN access)
cat >> /run/caddy/Caddyfile <<EOF
:60847 {
reverse_proxy :60845
encode gzip zstd
}
EOF
# Custom vhosts from custom.nix
cat >> /run/caddy/Caddyfile <<'CUSTOM_VHOSTS_EOF'
${extraVhosts}
CUSTOM_VHOSTS_EOF
'';
};
}
+52
View File
@@ -0,0 +1,52 @@
# ── modules/core/cpu-performance.nix ──────────────────────────────────────────
# Forces all CPU cores to run at maximum frequency on node and server_plus_desktop
# roles. Desktop-only installs retain normal OS power management behaviour.
#
# Three layers:
# 1. power-profiles-daemon disabled — removes the GNOME power profile picker;
# no user can switch profiles
# 2. cpufreq performance governor — pins every core to max frequency via
# kernel, enforced at boot by a oneshot unit
# 3. systemd oneshot enforcement — belt-and-suspenders; applies the governor
# after every boot even if module loads late
{ config, lib, pkgs, ... }:
{
config = lib.mkIf (!config.sovran_systemsOS.roles.desktop) {
# ── Layer 1: disable power-profiles-daemon ───────────────────────────────
# This removes the power-profile switcher from GNOME Settings entirely.
services.power-profiles-daemon.enable = false;
# ── Layer 2: set cpufreq governor to performance ─────────────────────────
# Pins all cores to max frequency. Works on Intel (intel_pstate) and AMD
# (amd-pstate / acpi-cpufreq) alike.
powerManagement.cpuFreqGovernor = "performance";
# ── Layer 3: enforce at boot via systemd oneshot ─────────────────────────
# Belt-and-suspenders: ensures the governor is applied after every boot even
# if the kernel module loads late.
systemd.services.cpu-performance = {
description = "Set CPU governor to performance on all cores";
wantedBy = [ "multi-user.target" ];
after = [ "systemd-modules-load.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
found=0
for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
if [ -w "$gov" ]; then
echo performance > "$gov"
found=1
fi
done
if [ "$found" -eq 0 ]; then
echo "cpu-performance: no writable cpufreq governors found (VM or unsupported hardware)" >&2
fi
'';
};
};
}
+33
View File
@@ -0,0 +1,33 @@
{ config, lib, pkgs, ... }:
{
# ── Legacy Cleanup ─────────────────────────────────────────────
# Removes deprecated apps and folders from the old Sovran_Systems
# repo that are no longer needed under staging_alpha.
# This runs on every activation but is idempotent (no-ops if
# the files are already gone).
system.activationScripts.cleanupLegacySovran = lib.stringAfter [ "users" ] ''
echo " Cleaning up legacy Sovran_Systems artifacts "
# Remove deprecated .desktop files
for f in \
/home/free/.local/share/applications/Sovran_SystemsOS_External_Backup.desktop \
/home/free/.local/share/applications/Sovran_SystemsOS_Updater.desktop \
/home/free/.local/share/applications/Sovran_SystemsOS_Resetter.desktop
do
if [ -f "$f" ]; then
rm -f "$f"
echo " Removed: $f"
fi
done
# Remove legacy Sovran_Systems folder (skip if it's a symlink)
if [ -d /home/free/.Sovran_Systems ] && [ ! -L /home/free/.Sovran_Systems ]; then
rm -rf /home/free/.Sovran_Systems
echo " Removed: /home/free/.Sovran_Systems/"
fi
echo " Legacy cleanup complete "
'';
}
+1 -1
View File
@@ -23,7 +23,7 @@
IP=$(dig @resolver4.opendns.com myip.opendns.com +short -4)
## Add DDNS entries below one curl per line
## Run 'sudo sovran-setup-domains' to configure automatically
## Managed via Sovran Hub web interface
SCRIPT
chmod 700 /var/lib/njalla/njalla.sh
+40
View File
@@ -0,0 +1,40 @@
# ── modules/core/no-sleep.nix ─────────────────────────────────────────────────
# Prevents the machine from ever sleeping or suspending at the system level.
#
# Only applies to server_plus_desktop and node roles. Desktop-only installs
# retain normal OS sleep/suspend behaviour.
#
# This operates at two layers below GNOME:
# 1. systemd-logind — ignores all hardware power events (lid, suspend key, etc.)
# 2. systemd targets — masks sleep/suspend/hibernate targets so nothing can
# trigger them, not even `systemctl suspend` or D-Bus calls.
#
# This is intentional for a 24/7 server/node. The GNOME-layer power settings in
# sovran_systemsos-desktop.nix remain in place as a belt-and-suspenders complement
# for active user sessions.
{ config, lib, ... }:
{
config = lib.mkIf (!config.sovran_systemsOS.roles.desktop) {
# ── Layer 1: logind hardware event handling ──────────────────────────────
services.logind.settings.Login = {
HandleLidSwitch = "ignore";
HandleLidSwitchDocked = "ignore";
HandleLidSwitchExternalPower = "ignore";
HandleSuspendKey = "ignore";
HandleHibernateKey = "ignore";
HandlePowerKey = "ignore";
IdleAction = "ignore";
IdleActionSec = 0;
};
# ── Layer 2: mask systemd sleep targets ─────────────────────────────────
# Nothing on the system can suspend/hibernate — not root, not GNOME, not D-Bus.
systemd.targets.sleep.enable = false;
systemd.targets.suspend.enable = false;
systemd.targets.hibernate.enable = false;
systemd.targets.hybrid-sleep.enable = false;
};
}
+121
View File
@@ -0,0 +1,121 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sovran_systemsOS.deploy;
in
{
options.sovran_systemsOS.deploy = {
enable = lib.mkEnableOption "Remote deploy mode";
authorizedKey = lib.mkOption {
type = lib.types.str;
default = "";
description = "Deployer's SSH public key for root access";
};
headscaleServer = lib.mkOption {
type = lib.types.str;
default = "";
description = "Headscale login server URL (e.g. https://hs.sovransystems.com). If set, Tailscale is used for post-install connectivity.";
};
headscaleAuthKeyFile = lib.mkOption {
type = lib.types.str;
default = "/var/lib/secrets/headscale-authkey";
description = "Path to file containing the Headscale pre-auth key for post-install enrollment";
};
};
config = lib.mkIf cfg.enable {
# ── Force SSH open on all interfaces ────────────────────────────────────
services.openssh = {
enable = true;
listenAddresses = lib.mkForce [
{ addr = "0.0.0.0"; port = 22; }
{ addr = "127.0.0.1"; port = 22; }
];
settings = {
PermitRootLogin = lib.mkForce "prohibit-password";
PasswordAuthentication = lib.mkForce false;
};
};
networking.firewall.allowedTCPPorts = [ 22 ];
# ── Inject deployer's SSH public key into root's authorized keys ─────────
users.users.root.openssh.authorizedKeys.keys =
lib.mkIf (cfg.authorizedKey != "") [ cfg.authorizedKey ];
# ── Force RDP on ─────────────────────────────────────────────────────────
sovran_systemsOS.features.rdp = lib.mkForce true;
# ── Enable Fail2Ban for SSH protection ───────────────────────────────────
services.fail2ban = {
enable = true;
ignoreIP = [ "127.0.0.0/8" ];
};
# ── Tailscale / Headscale VPN (only when headscaleServer is configured) ──
services.tailscale = lib.mkIf (cfg.headscaleServer != "") {
enable = true;
};
environment.systemPackages = lib.mkIf (cfg.headscaleServer != "") [ pkgs.tailscale ];
systemd.services.deploy-tailscale-connect = lib.mkIf (cfg.headscaleServer != "") {
description = "Connect to Headscale Tailnet for post-install remote access";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" "tailscaled.service" ];
wants = [ "network-online.target" "tailscaled.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
AUTH_KEY_FILE="${cfg.headscaleAuthKeyFile}"
if [ ! -f "$AUTH_KEY_FILE" ]; then
echo "Headscale auth key file not found: $AUTH_KEY_FILE skipping Tailscale enrollment"
exit 0
fi
AUTH_KEY=$(cat "$AUTH_KEY_FILE")
[ -n "$AUTH_KEY" ] || { echo "Auth key file is empty, skipping"; exit 0; }
HOSTNAME_SUFFIX=$(hostname | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g; s/-\{2,\}/-/g; s/^-//; s/-$//')
HOSTNAME="sovran-$HOSTNAME_SUFFIX"
echo "Joining Tailnet via ${cfg.headscaleServer} as $HOSTNAME..."
${pkgs.tailscale}/bin/tailscale up \
--login-server="${cfg.headscaleServer}" \
--authkey="$AUTH_KEY" \
--hostname="$HOSTNAME"
echo "Tailscale IP: $(${pkgs.tailscale}/bin/tailscale ip -4 2>/dev/null || echo 'pending')"
'';
path = [ pkgs.tailscale pkgs.coreutils ];
};
# ── Safety auto-expiry service ────────────────────────────────────────────
systemd.services.deploy-auto-expire = {
description = "Auto-expire remote deploy mode after 48 hours";
wantedBy = [ "multi-user.target" ];
after = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = false;
};
script = ''
# 48 hours = 172800 seconds
sleep $((48 * 60 * 60))
systemctl stop deploy-tailscale-connect || true
mkdir -p /etc/sovran
echo "expired" > /etc/sovran/deploy-mode
'';
path = [ pkgs.coreutils ];
};
# ── Deploy-mode indicator file ────────────────────────────────────────────
environment.etc."sovran/deploy-mode".text = "active";
};
}
+1
View File
@@ -5,6 +5,7 @@
# ── Server+Desktop Role (default) ─────────────────────────
(lib.mkIf config.sovran_systemsOS.roles.server_plus_desktop {
sovran_systemsOS.web.btcpayserver = lib.mkDefault true;
})
# ── Desktop Only Role ─────────────────────────────────────
+11 -1
View File
@@ -48,17 +48,27 @@
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
bitcoin-core = lib.mkEnableOption "Bitcoin Core";
rdp = lib.mkEnableOption "Gnome Remote Desktop";
sshd = lib.mkEnableOption "SSH remote access";
};
# ── Web exposure (controls Caddy vhosts) ──────────────────
web = {
btcpayserver = lib.mkOption {
type = lib.types.bool;
default = true;
default = false;
description = "Expose BTCPay Server via Caddy";
};
};
# ── Caddy customisation ───────────────────────────────────
caddy = {
extraVirtualHosts = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Additional raw Caddyfile blocks appended to the generated Caddy config. Use this in custom.nix to add custom domains and reverse proxies.";
};
};
# ── Domain setup registry ─────────────────────────────────
domainRequirements = lib.mkOption {
type = lib.types.listOf (lib.types.submodule {
+181 -50
View File
@@ -4,69 +4,85 @@ let
cfg = config.sovran_systemsOS;
monitoredServices =
# ── Infrastructure (always present) ────────────────────────
# ── Infrastructure — System Passwords (always present) ─────
[
{ name = "System Passwords"; unit = "root-password-setup.service"; type = "system"; icon = "passwords"; enabled = true; category = "infrastructure"; credentials = [
{ label = "Free Account Username"; value = "free"; }
{ label = "Free Account / Hub Login Password"; file = "/var/lib/secrets/free-password"; }
{ label = "Administrator (root) Password"; file = "/var/lib/secrets/root-password"; }
{ label = "SSH Passphrase use via: ssh root@localhost"; file = "/var/lib/secrets/ssh-passphrase"; }
]; }
]
# ── Infrastructure — Caddy + Tor (NOT desktop-only) ────────
++ lib.optionals (!cfg.roles.desktop) [
{ name = "Caddy"; unit = "caddy.service"; type = "system"; icon = "caddy"; enabled = true; category = "infrastructure"; credentials = []; }
{ name = "Tor"; unit = "tor.service"; type = "system"; icon = "tor"; enabled = true; category = "infrastructure"; credentials = []; }
{ name = "System Passwords"; unit = "root-password-setup.service"; type = "system"; icon = "system"; enabled = true; category = "infrastructure"; credentials = [
{ label = "Free Account Username"; value = "free"; }
{ label = "Free Account Password"; file = "/var/lib/secrets/free-password"; }
{ label = "Root Password"; file = "/var/lib/secrets/root-password"; }
{ label = "SSH Local Access"; value = "ssh root@localhost / Passphrase: gosovransystems"; }
]; }
]
# ── Infrastructure — Remote Desktop (all roles) ─────────────
++ [
{ name = "Remote Desktop"; unit = "gnome-remote-desktop.service"; type = "system"; icon = "rdp"; enabled = cfg.features.rdp; category = "infrastructure"; credentials = [
{ label = "Username"; file = "/var/lib/gnome-remote-desktop/rdp-username"; }
{ label = "Password"; file = "/var/lib/gnome-remote-desktop/rdp-password"; }
{ label = "Address"; file = "/var/lib/secrets/internal-ip"; suffix = ":3389"; }
{ label = "How to Connect"; value = "1. Install an RDP client (e.g. Remmina, Microsoft Remote Desktop)\n2. Create a new RDP connection\n3. Enter the Address above as the host\n4. Enter the Username and Password above\n5. Connect you will see your desktop remotely"; }
{ label = "How to Connect"; value = "1. Install an RDP client (e.g. Remmina, Microsoft Remote Desktop)\n2. Create a new RDP connection\n3. Enter the Address above as the host\n4. Enter the Username and Password above"; }
]; }
]
# ── Bitcoin Base (node implementations) ────────────────────
++ [
++ lib.optionals cfg.services.bitcoin [
{ name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.features.bip110; category = "bitcoin-base"; credentials = [
{ label = "Tor Address"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
]; }
{ name = "Bitcoin Knots"; unit = "bitcoind.service"; type = "system"; icon = "bitcoind"; enabled = cfg.services.bitcoin && !cfg.features.bitcoin-core && !cfg.features.bip110; category = "bitcoin-base"; credentials = [
{ label = "Tor Address"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
]; }
{ name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
{ label = "Tor Address"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
]; }
]
# ── Bitcoin Apps (services on top of the node) ─────────────
++ [
++ lib.optionals cfg.services.bitcoin [
{ name = "Electrs"; unit = "electrs.service"; type = "system"; icon = "electrs"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
{ label = "Tor Address"; file = "/var/lib/tor/onion/electrs/hostname"; prefix = "http://"; }
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/electrs/hostname"; prefix = "http://"; }
{ label = "Port"; value = "50001"; }
]; }
{ name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = []; }
{ name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
{ label = "Tor Access"; file = "/var/lib/tor/onion/rtl/hostname"; prefix = "http://"; }
{ label = "Local Network"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":3050"; }
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/rtl/hostname"; prefix = "http://"; }
{ label = "Local Network Access on your home network only"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":3051"; }
{ label = "Password"; file = "/etc/nix-bitcoin-secrets/rtl-password"; }
{ label = "How to Access"; value = " Tor Address: Open in Tor Browser from any device, anywhere in the world\n Local Network: Open in any browser, but only when connected to your home network"; }
]; }
{ name = "BTCPayserver"; unit = "btcpayserver.service"; type = "system"; icon = "btcpayserver"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
{ name = "BTCPayserver"; unit = "btcpayserver.service"; type = "system"; icon = "btcpayserver"; enabled = cfg.web.btcpayserver; category = "bitcoin-apps"; credentials = [
{ label = "URL"; file = "/var/lib/domains/btcpayserver"; prefix = "https://"; }
{ label = "Note"; value = "Create your admin account on first visit"; }
]; }
{ name = "Zeus Connect"; unit = "zeus-connect-setup.service"; type = "system"; icon = "zeus"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
{ label = "Connection URL"; file = "/var/lib/secrets/zeus-connect-url"; qrcode = true; }
{ label = "How to Connect"; value = "1. Download Zeus from App Store or Google Play\n2. Open Zeus Scan Node Config\n3. Scan the QR code above or paste the Connection URL"; }
{ label = "QR Code"; file = "/var/lib/secrets/zeus-connect-url"; qrcode = true; qronly = true; }
{ label = "How to Connect"; value = "1. Download Zeus from App Store or Google Play\n2. Open Zeus Scan Node Config\n3. Scan the QR code above"; }
]; }
{ name = "Sparrow Auto-Link"; unit = "sparrow-autoconnect.service"; type = "system"; icon = "sparrow"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
{ label = "Server"; value = "tcp://127.0.0.1:50001 (Electrs)"; }
{ label = "Status"; value = "Auto-configured on first boot"; }
]; }
{ name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [
{ label = "Tor Access"; file = "/var/lib/tor/onion/mempool-frontend/hostname"; prefix = "http://"; }
{ label = "Local Network"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":60847"; }
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/mempool-frontend/hostname"; prefix = "http://"; }
{ label = "Local Network Access on your home network only"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":60847"; }
{ label = "How to Access"; value = " Tor Address: Open in Tor Browser from any device, anywhere in the world\n Local Network: Open in any browser, but only when connected to your home network"; }
]; }
]
# ── Communication ──────────────────────────────────────────
++ [
# ── Communication (server+desktop only) ────────────────────
++ lib.optionals cfg.roles.server_plus_desktop [
{ name = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; enabled = cfg.services.synapse; category = "communication"; credentials = [
{ label = "Users"; file = "/var/lib/secrets/matrix-users"; multiline = true; }
{ label = "Homeserver URL"; file = "/var/lib/secrets/matrix-homeserver-url"; }
{ label = "Admin Username"; file = "/var/lib/secrets/matrix-admin-username"; }
{ label = "Admin Password"; file = "/var/lib/secrets/matrix-admin-password"; }
{ label = "Test Username"; file = "/var/lib/secrets/matrix-test-username"; }
{ label = "Test Password"; file = "/var/lib/secrets/matrix-test-password"; }
]; }
{ name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "livekit"; enabled = cfg.features.element-calling; category = "communication"; credentials = []; }
{ name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "element-calling"; enabled = cfg.features.element-calling; category = "communication"; credentials = []; }
]
# ── Self-Hosted Apps ───────────────────────────────────────
++ [
# ── Self-Hosted Apps (server+desktop only) ─────────────────
++ lib.optionals cfg.roles.server_plus_desktop [
{ name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; enabled = cfg.services.vaultwarden; category = "apps"; credentials = [
{ label = "URL"; file = "/var/lib/domains/vaultwarden"; prefix = "https://"; }
{ label = "Admin Panel"; file = "/var/lib/domains/vaultwarden"; prefix = "https://"; suffix = "/admin"; }
@@ -79,11 +95,11 @@ let
{ label = "Credentials"; file = "/var/lib/secrets/wordpress-admin"; multiline = true; }
]; }
]
# ── Nostr / Relay ──────────────────────────────────────────
++ [
# ── Nostr / Relay (server+desktop only) ────────────────────
++ lib.optionals cfg.roles.server_plus_desktop [
{ name = "Haven Relay"; unit = "haven-relay.service"; type = "system"; icon = "haven"; enabled = cfg.features.haven; category = "nostr"; credentials = []; }
]
# ── Support ────────────────────────────────────────────────
# ── Support (always present) ────────────────────────────────
++ [
{ name = "Tech Support"; unit = "sovran-tech-support"; type = "support"; icon = "support"; enabled = true; category = "support"; credentials = []; }
];
@@ -129,8 +145,24 @@ let
echo ""
if [ "$RC" -eq 0 ]; then
echo " Step 2/3: nixos-rebuild switch "
if ! nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1; then
echo " Step 2/3: nixos-rebuild "
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1)
SWITCH_RC=$?
echo "$SWITCH_OUT"
if [ "$SWITCH_RC" -eq 0 ]; then
echo "[OK] switch succeeded"
elif echo "$SWITCH_OUT" | grep -q "switchInhibitors\|Pre-switch checks failed"; then
echo ""
echo " Build succeeded a reboot is required to apply this update"
echo " (Critical system components changed; running nixos-rebuild boot instead)"
if nixos-rebuild boot --flake /etc/nixos --print-build-logs 2>&1; then
echo "REBOOT_REQUIRED" > "$STATUS"
exit 0
else
echo "[ERROR] nixos-rebuild boot also failed"
RC=1
fi
else
echo "[ERROR] nixos-rebuild switch failed"
RC=1
fi
@@ -177,12 +209,26 @@ let
echo ""
echo ""
echo " Rebuilding system configuration "
if nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1; then
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1)
SWITCH_RC=$?
echo "$SWITCH_OUT"
if [ "$SWITCH_RC" -eq 0 ]; then
echo ""
echo ""
echo " Rebuild completed successfully"
echo ""
echo "SUCCESS" > "$STATUS"
elif echo "$SWITCH_OUT" | grep -q "switchInhibitors\|Pre-switch checks failed"; then
echo ""
echo " Build succeeded a reboot is required to apply this rebuild"
echo " (Critical system components changed; running nixos-rebuild boot instead)"
if nixos-rebuild boot --flake /etc/nixos --print-build-logs 2>&1; then
echo "REBOOT_REQUIRED" > "$STATUS"
else
echo "[ERROR] nixos-rebuild boot also failed"
echo "FAILED" > "$STATUS"
exit 1
fi
else
echo ""
echo ""
@@ -193,6 +239,49 @@ let
fi
'';
# ── Brave launcher wrapper: stable profile dir so Wayland app_id is
# deterministic and GNOME Shell can match the window to the .desktop
# entry (fixes generic gear icon appearing in the dock).
hub-brave-wrapper = pkgs.writeShellScript "sovran-hub-brave.sh" ''
export PATH="${lib.makeBinPath [ pkgs.brave pkgs.coreutils ]}:$PATH"
HUB_DATA="/tmp/sovran-hub-brave-$(id -u)"
mkdir -p "$HUB_DATA"
trap '[ -n "$HUB_DATA" ] && rm -rf "$HUB_DATA"' EXIT INT TERM
export BAMF_DESKTOP_FILE_HINT="/run/current-system/sw/share/applications/sovran-hub.desktop"
export GIO_LAUNCHED_DESKTOP_FILE="/run/current-system/sw/share/applications/sovran-hub.desktop"
brave --app=http://localhost:8937/auto-login \
--class=sovran-hub \
--user-data-dir="$HUB_DATA" \
--password-store=basic \
--disable-gpu \
--disable-features=WebRtcPipeWireCapturer \
--ozone-platform=wayland
'';
# ── Hub auto-launch wrapper script ────────────────────────────────
hub-autolaunch-script = pkgs.writeShellScript "sovran-hub-autolaunch.sh" ''
export PATH="${lib.makeBinPath [ pkgs.curl pkgs.coreutils ]}:$PATH"
DISABLE_FLAG="/var/lib/sovran/hub-autolaunch-disabled"
BOOT_FLAG="/run/sovran-hub-autolaunch-done"
# User disabled auto-launch via Hub toggle
[ -f "$DISABLE_FLAG" ] && exit 0
# Already launched this boot
[ -f "$BOOT_FLAG" ] && exit 0
touch "$BOOT_FLAG"
# Wait for Hub server to become ready (max ~15 seconds)
for i in $(seq 1 15); do
curl -s -o /dev/null http://localhost:8937 && break
sleep 1
done
${hub-brave-wrapper}
'';
sovran-hub-web = pkgs.python3Packages.buildPythonApplication {
pname = "sovran-systemsos-hub-web";
version = "1.0.0";
@@ -200,6 +289,8 @@ let
src = ../../app;
nativeBuildInputs = [ pkgs.librsvg ];
propagatedBuildInputs = with pkgs.python3Packages; [
fastapi
uvicorn
@@ -220,6 +311,29 @@ let
install -d $out/share/sovran-hub/icons
cp icons/* $out/share/sovran-hub/icons/ 2>/dev/null || true
install -d $out/share/icons/hicolor/scalable/apps
cp sovran_systemsos_web/static/sovran-hub-icon.svg $out/share/icons/hicolor/scalable/apps/sovran-hub.svg
for size in 48 128 256 512; do
install -d $out/share/icons/hicolor/''${size}x''${size}/apps
rsvg-convert -w ''${size} -h ''${size} sovran_systemsos_web/static/sovran-hub-icon.svg -o $out/share/icons/hicolor/''${size}x''${size}/apps/sovran-hub.png
done
install -d $out/share/applications
cat > $out/share/applications/sovran-hub.desktop <<DESKTOP
[Desktop Entry]
Type=Application
Name=Sovran Hub
Comment=Open Sovran_SystemsOS Hub dashboard
Exec=${hub-brave-wrapper}
Icon=sovran-hub
Terminal=false
Categories=System;
StartupNotify=true
StartupWMClass=brave-localhost__auto-login-Default
X-GNOME-SingleWindow=true
DESKTOP
install -d $out/bin
cat > $out/bin/sovran-hub-web <<LAUNCHER
#!${pkgs.python3}/bin/python3
@@ -254,21 +368,32 @@ in
description = "Sovran_SystemsOS Hub Web Interface";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
conflicts = [ "sovran-hub-reboot.service" ];
serviceConfig = {
ExecStart = "${sovran-hub-web}/bin/sovran-hub-web";
Restart = "on-failure";
RestartPreventExitStatus = "SIGTERM";
RestartSec = "5s";
User = "root";
StandardOutput = "journal";
StandardError = "journal";
};
path = [ pkgs.qrencode ];
path = [
pkgs.qrencode
pkgs.curl
pkgs.iproute2
pkgs.nftables
pkgs.iptables
pkgs.hostname
] ++ lib.optional cfg.services.bitcoin config.services.bitcoind.package;
};
systemd.services.sovran-hub-update = {
description = "Sovran_SystemsOS System Update";
restartIfChanged = false; # Don't let nixos-rebuild kill an in-flight update
stopIfChanged = false; # Don't stop it during activation either
serviceConfig = {
Type = "oneshot";
ExecStart = "${update-script}";
@@ -277,30 +402,36 @@ in
systemd.services.sovran-hub-rebuild = {
description = "Sovran_SystemsOS System Rebuild";
restartIfChanged = false; # Don't let nixos-rebuild kill an in-flight rebuild
stopIfChanged = false; # Don't stop it during activation either
serviceConfig = {
Type = "oneshot";
ExecStart = "${rebuild-script}";
};
};
systemd.services.hub-overrides-init = {
description = "Initialize hub-overrides.nix if missing";
wantedBy = [ "multi-user.target" ];
systemd.services.sovran-hub-reboot = {
description = "Sovran_SystemsOS System Reboot";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
Type = "oneshot";
ExecStart = "/run/current-system/sw/bin/systemctl --force reboot";
};
unitConfig.ConditionPathExists = "!/etc/nixos/hub-overrides.nix";
script = ''
cat > /etc/nixos/hub-overrides.nix <<'EOF'
# Auto-generated by Sovran Hub do not edit manually
{ ... }:
{
}
EOF
'';
};
networking.firewall.allowedTCPPorts = [ 8937 ];
environment.systemPackages = [ sovran-hub-web ];
networking.firewall.allowedTCPPorts = [ 8937 60847 ];
# ── Auto-launch Hub in browser on login ───────────────────────
environment.etc."xdg/autostart/sovran-hub-autolaunch.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Sovran Hub Auto-Launch
Exec=${hub-autolaunch-script}
Terminal=false
X-GNOME-Autostart-enabled=true
NoDisplay=true
'';
};
}
}
-377
View File
@@ -1,377 +0,0 @@
{ config, pkgs, lib, ... }:
let
domains = config.sovran_systemsOS.domainRequirements;
domainNamesList = lib.concatMapStringsSep " " (d: d.name) domains;
ddnsPrompt = ''
read -p " Njal.la DDNS curl command (paste full line, or Enter to skip): " DDNS_LINE
if [ -n "$DDNS_LINE" ]; then
# Strip any leading "curl " if they pasted the whole command
DDNS_LINE="''${DDNS_LINE#curl }"
# Strip surrounding quotes
DDNS_LINE="''${DDNS_LINE%\"}"
DDNS_LINE="''${DDNS_LINE#\"}"
# Replace &auto with &a=''${IP} at the end
DDNS_LINE="''${DDNS_LINE%auto}&a=''${DOLLAR}{IP}"
# Remove any trailing double &a= if they already had &a=
DDNS_LINE=$(echo "$DDNS_LINE" | sed 's/&a=&a=/\&a=/g')
'';
confirmDomain = name: ''
while true; do
echo ""
printf "%b%s%b\n" "$YELLOW" " You entered:" "$NC"
printf "%b%s%b\n" "$CYAN" " Domain: $DOMAIN" "$NC"
if [ -n "''${DDNS_DISPLAY:-}" ]; then
printf "%b%s%b\n" "$CYAN" " DDNS URL: $DDNS_DISPLAY" "$NC"
fi
echo ""
read -p " Is this correct? (y/n): " CONFIRM
case "$CONFIRM" in
[yY])
echo "$DOMAIN" > "/var/lib/domains/${name}"
printf "%b%s%b\n" "$GREEN" " Saved." "$NC"
break
;;
[nN])
echo " Let's try again."
REDO=true
break
;;
*)
echo " Please enter y or n."
;;
esac
done
'';
domainPrompts = lib.concatMapStringsSep "\n" (d: ''
REDO=true
while [ "$REDO" = true ]; do
REDO=false
DDNS_DISPLAY=""
echo ""
printf "%b%s%b\n" "$GREEN" " ${d.label} " "$NC"
EXISTING=""
if [ -f "/var/lib/domains/${d.name}" ]; then
EXISTING=$(cat "/var/lib/domains/${d.name}")
printf "%b%s%b\n" "$CYAN" " Current: $EXISTING" "$NC"
fi
read -p " Subdomain (e.g. ${d.example}) or Enter to keep current: " DOMAIN_INPUT
DOMAIN="''${DOMAIN_INPUT:-$EXISTING}"
if [ -n "$DOMAIN" ]; then
${lib.optionalString d.needsDDNS ''
${ddnsPrompt}
DDNS_DISPLAY="$DDNS_LINE"
PENDING_NJALLA="curl \"$DDNS_LINE\""
fi
''}
${confirmDomain d.name}
if [ "$REDO" = false ] && [ -n "''${PENDING_NJALLA:-}" ]; then
NJALLA_ENTRIES="$NJALLA_ENTRIES
$PENDING_NJALLA"
PENDING_NJALLA=""
fi
else
echo " Skipped."
fi
done
'') domains;
missingDomainPrompts = lib.concatMapStringsSep "\n" (d: ''
if [ ! -f "/var/lib/domains/${d.name}" ]; then
MISSING=true
REDO=true
while [ "$REDO" = true ]; do
REDO=false
DDNS_DISPLAY=""
echo ""
printf "%b%s%b\n" "$GREEN" " ${d.label} (NEW) " "$NC"
read -p " Subdomain (e.g. ${d.example}): " DOMAIN
if [ -n "$DOMAIN" ]; then
${lib.optionalString d.needsDDNS ''
${ddnsPrompt}
DDNS_DISPLAY="$DDNS_LINE"
PENDING_NJALLA="curl \"$DDNS_LINE\""
fi
''}
${confirmDomain d.name}
if [ "$REDO" = false ] && [ -n "''${PENDING_NJALLA:-}" ]; then
NEW_NJALLA_ENTRIES="$NEW_NJALLA_ENTRIES
$PENDING_NJALLA"
PENDING_NJALLA=""
fi
else
echo " Skipped."
fi
done
fi
'') domains;
domainSummary = lib.concatMapStringsSep "\n" (d: ''
if [ -f "/var/lib/domains/${d.name}" ]; then
printf "%b%s%b\n" "$NC" " ${d.label}: $(cat /var/lib/domains/${d.name})" "$NC"
fi
'') domains;
setupScript = pkgs.writeShellScriptBin "sovran-setup-domains" ''
set -euo pipefail
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
DOLLAR='$'
echo ""
printf "%b%s%b\n" "$CYAN" "" "$NC"
printf "%b%s%b\n" "$CYAN" " Sovran_SystemsOS Domain & DDNS Setup" "$NC"
printf "%b%s%b\n" "$CYAN" "" "$NC"
echo ""
printf "%b%s%b\n" "$YELLOW" "Before running this, you need:" "$NC"
echo ""
echo " 1. Domains/subdomains purchased on https://njal.la"
echo " 2. For each subdomain, add a Dynamic record in"
echo " your Njal.la dashboard."
echo " 3. Njal.la will give you a curl command like:"
echo ""
printf "%b%s%b\n" "$CYAN" " curl \"https://njal.la/update/?h=sub.domain.com&k=abc123&auto\"" "$NC"
echo ""
echo " Have those curl commands ready."
echo ""
read -p "Press Enter to continue..."
# Create directories
mkdir -p /var/lib/domains
NJALLA_ENTRIES=""
PENDING_NJALLA=""
# SSL Email
REDO=true
while [ "$REDO" = true ]; do
REDO=false
echo ""
printf "%b%s%b\n" "$GREEN" " SSL Certificate Email " "$NC"
echo "Let's Encrypt needs an email for certificate notifications."
EXISTING_EMAIL=""
if [ -f "/var/lib/domains/sslemail" ]; then
EXISTING_EMAIL=$(cat /var/lib/domains/sslemail)
printf "%b%s%b\n" "$CYAN" " Current: $EXISTING_EMAIL" "$NC"
fi
read -p " Email address (or Enter to keep current): " EMAIL_INPUT
SSL_EMAIL="''${EMAIL_INPUT:-$EXISTING_EMAIL}"
if [ -n "$SSL_EMAIL" ]; then
while true; do
echo ""
printf "%b%s%b\n" "$YELLOW" " You entered:" "$NC"
printf "%b%s%b\n" "$CYAN" " Email: $SSL_EMAIL" "$NC"
echo ""
read -p " Is this correct? (y/n): " CONFIRM
case "$CONFIRM" in
[yY])
echo "$SSL_EMAIL" > /var/lib/domains/sslemail
printf "%b%s%b\n" "$GREEN" " Saved." "$NC"
break
;;
[nN])
echo " Let's try again."
REDO=true
break
;;
*)
echo " Please enter y or n."
;;
esac
done
fi
done
# All module domains
${domainPrompts}
# Final review
echo ""
printf "%b%s%b\n" "$CYAN" "" "$NC"
printf "%b%s%b\n" "$CYAN" " Review All Entries" "$NC"
printf "%b%s%b\n" "$CYAN" "" "$NC"
echo ""
echo " Configured domains:"
${domainSummary}
echo ""
echo " DDNS entries:"
if [ -n "$NJALLA_ENTRIES" ]; then
echo "$NJALLA_ENTRIES"
else
echo " (none)"
fi
echo ""
read -p " Does everything look correct? (y/n): " FINAL_CONFIRM
if [ "$FINAL_CONFIRM" != "y" ] && [ "$FINAL_CONFIRM" != "Y" ]; then
echo ""
printf "%b%s%b\n" "$YELLOW" " Setup cancelled. Run 'sudo sovran-setup-domains' to start over." "$NC"
echo ""
exit 1
fi
# Append curl entries to njalla.sh
if [ -n "$NJALLA_ENTRIES" ]; then
echo ""
printf "%b%s%b\n" "$GREEN" " Updating DDNS script " "$NC"
echo "$NJALLA_ENTRIES" >> /var/lib/njalla/njalla.sh
echo " Appended entries to /var/lib/njalla/njalla.sh"
fi
# Run DDNS update now
echo ""
read -p "Update Njal.la DNS records now? (y/n): " RUN_NOW
if [ "$RUN_NOW" = "y" ]; then
bash /var/lib/njalla/njalla.sh
echo " DNS records updated."
fi
# Mark setup complete
touch /var/lib/domains/.setup-complete
# Summary
echo ""
printf "%b%s%b\n" "$CYAN" "" "$NC"
printf "%b%s%b\n" "$CYAN" " Setup Complete!" "$NC"
printf "%b%s%b\n" "$CYAN" "" "$NC"
echo ""
echo " Domain files: /var/lib/domains/"
echo " DDNS script: /var/lib/njalla/njalla.sh"
echo " DDNS cron: Every 15 minutes (already configured)"
echo ""
printf "%b%s%b\n" "$YELLOW" " Rebuilding to activate services with new domains..." "$NC"
echo ""
nixos-rebuild switch --flake /etc/nixos#nixos
'';
addDomainScript = pkgs.writeShellScriptBin "sovran-add-domains" ''
set -euo pipefail
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
DOLLAR='$'
MISSING=false
NEW_NJALLA_ENTRIES=""
PENDING_NJALLA=""
echo ""
printf "%b%s%b\n" "$CYAN" "" "$NC"
printf "%b%s%b\n" "$CYAN" " Sovran_SystemsOS New Feature Domains" "$NC"
printf "%b%s%b\n" "$CYAN" "" "$NC"
echo ""
echo " Checking for newly enabled features that need domains..."
mkdir -p /var/lib/domains
${missingDomainPrompts}
if [ "$MISSING" = false ]; then
echo ""
printf "%b%s%b\n" "$GREEN" " All domains are already configured. Nothing to do." "$NC"
echo ""
exit 0
fi
# Final review
echo ""
printf "%b%s%b\n" "$CYAN" "" "$NC"
printf "%b%s%b\n" "$CYAN" " Review New Entries" "$NC"
printf "%b%s%b\n" "$CYAN" "" "$NC"
echo ""
echo " All configured domains:"
${domainSummary}
echo ""
echo " New DDNS entries:"
if [ -n "$NEW_NJALLA_ENTRIES" ]; then
echo "$NEW_NJALLA_ENTRIES"
else
echo " (none)"
fi
echo ""
read -p " Does everything look correct? (y/n): " FINAL_CONFIRM
if [ "$FINAL_CONFIRM" != "y" ] && [ "$FINAL_CONFIRM" != "Y" ]; then
echo ""
printf "%b%s%b\n" "$YELLOW" " Setup cancelled. Run 'sudo sovran-add-domains' to start over." "$NC"
echo ""
exit 1
fi
# Append new entries to njalla.sh
if [ -n "$NEW_NJALLA_ENTRIES" ]; then
echo ""
printf "%b%s%b\n" "$GREEN" " Updating DDNS script " "$NC"
echo "$NEW_NJALLA_ENTRIES" >> /var/lib/njalla/njalla.sh
echo " Appended new entries to /var/lib/njalla/njalla.sh"
echo ""
read -p "Update Njal.la DNS records now? (y/n): " RUN_NOW
if [ "$RUN_NOW" = "y" ]; then
bash /var/lib/njalla/njalla.sh
echo " DNS records updated."
fi
fi
# Summary
echo ""
printf "%b%s%b\n" "$CYAN" "" "$NC"
printf "%b%s%b\n" "$CYAN" " New Domains Added!" "$NC"
printf "%b%s%b\n" "$CYAN" "" "$NC"
echo ""
echo " All configured domains:"
${domainSummary}
echo ""
printf "%b%s%b\n" "$YELLOW" " Rebuilding to activate services with new domains..." "$NC"
echo ""
nixos-rebuild switch --impure
'';
needsSetup = pkgs.writeShellScriptBin "sovran-domains-need-setup" ''
# First boot no setup done at all
if [ ! -f /var/lib/domains/.setup-complete ]; then
exit 0
fi
# Existing machine check for missing domain files
for NAME in ${domainNamesList}; do
if [ ! -f "/var/lib/domains/$NAME" ]; then
exit 0
fi
done
# Everything is configured
exit 1
'';
in
{
environment.systemPackages = [
setupScript
addDomainScript
needsSetup
];
environment.etc."xdg/autostart/sovran-setup-domains.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Sovran_SystemsOS Domain Setup
Comment=Configure domains for newly enabled features
Exec=${pkgs.bash}/bin/bash -c 'if ${needsSetup}/bin/sovran-domains-need-setup; then if [ ! -f /var/lib/domains/.setup-complete ]; then ${pkgs.gnome-terminal}/bin/gnome-terminal -- sudo ${setupScript}/bin/sovran-setup-domains; else ${pkgs.gnome-terminal}/bin/gnome-terminal -- sudo ${addDomainScript}/bin/sovran-add-domains; fi; fi'
Terminal=false
X-GNOME-Autostart-enabled=true
'';
}
+423
View File
@@ -0,0 +1,423 @@
{ config, lib, pkgs, ... }:
# ── sovran-provisioner.nix ────────────────────────────────────────────────────
# NixOS module for the Sovran Systems VPS provisioning server.
#
# Deploys:
# - Headscale (coordination server, listening on 127.0.0.1:8080)
# - Python Flask provisioning API (port 9090)
# - Caddy reverse proxy (80/443 with automatic TLS)
# - Bootstrap service (creates Headscale users + enrollment token on first boot)
#
# Headscale 0.28.0 compatible — uses numeric user IDs (-u <id>) throughout.
# ─────────────────────────────────────────────────────────────────────────────
let
cfg = config.sovranProvisioner;
# ── Python Flask provisioner script ────────────────────────────────────────
provisionerScript = pkgs.writeText "sovran-provisioner.py" ''
#!/usr/bin/env python3
"""
Sovran Systems provisioning API Headscale 0.28.0 compatible.
Endpoints:
POST /register register a new machine and return a Headscale pre-auth key
GET /machines list registered machines (requires Bearer token)
GET /health liveness check
"""
import json
import os
import subprocess
import time
from collections import defaultdict
from functools import wraps
from pathlib import Path
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
# Configuration
DATA_DIR = Path(os.environ.get("PROVISIONER_DATA_DIR", "/var/lib/sovran-provisioner"))
TOKEN_FILE = DATA_DIR / "enroll-token"
MACHINES_FILE = DATA_DIR / "machines.json"
HEADSCALE_USER = os.environ.get("HEADSCALE_USER", "sovran-deploy")
KEY_EXPIRY = os.environ.get("KEY_EXPIRY", "1h")
RATE_LIMIT_MAX = int(os.environ.get("RATE_LIMIT_MAX", "10"))
RATE_LIMIT_WIN = int(os.environ.get("RATE_LIMIT_WINDOW", "60"))
# Simple in-memory rate limiter
_rate_buckets: dict = defaultdict(list)
def _rate_limit_check(key: str) -> bool:
"""Return True if the request is allowed, False if rate-limited."""
now = time.monotonic()
bucket = _rate_buckets[key]
# Purge entries outside the window
_rate_buckets[key] = [t for t in bucket if now - t < RATE_LIMIT_WIN]
if len(_rate_buckets[key]) >= RATE_LIMIT_MAX:
return False
_rate_buckets[key].append(now)
return True
# Helper: read enrollment token
def _get_token() -> str:
try:
return TOKEN_FILE.read_text().strip()
except FileNotFoundError:
return ""
# Helper: require Bearer token
def require_token(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
abort(401)
token = auth[len("Bearer "):].strip()
expected = _get_token()
if not expected or token != expected:
abort(401)
return f(*args, **kwargs)
return decorated
# Helper: persist machine record
def _save_machine(hostname: str, mac: str, tailscale_ip: str = ""):
machines = _load_machines()
machines[mac] = {
"hostname": hostname,
"mac": mac,
"registered_at": time.time(),
"tailscale_ip": tailscale_ip,
}
MACHINES_FILE.write_text(json.dumps(machines, indent=2))
def _load_machines() -> dict:
try:
return json.loads(MACHINES_FILE.read_text())
except (FileNotFoundError, json.JSONDecodeError):
return {}
# Headscale helpers (0.28.0 compatible)
def get_user_id(username: str):
"""Look up numeric user ID from username for Headscale 0.28.0."""
result = subprocess.run(
["headscale", "users", "list", "-o", "json"],
capture_output=True, text=True
)
if result.returncode != 0:
app.logger.error("headscale users list failed: %s", result.stderr)
return None
try:
users = json.loads(result.stdout)
except json.JSONDecodeError:
app.logger.error("headscale users list returned invalid JSON: %s", result.stdout)
return None
for user in users:
if user.get("name") == username:
return user.get("id")
return None
def create_preauthkey(user_id, expiry: str = "1h") -> str | None:
"""Create a pre-auth key using the numeric user ID (Headscale 0.28.0)."""
result = subprocess.run(
["headscale", "preauthkeys", "create",
"-u", str(user_id),
"-e", expiry,
"-o", "json"],
capture_output=True, text=True
)
if result.returncode != 0:
app.logger.error("headscale preauthkeys create failed: %s", result.stderr)
return None
try:
key_data = json.loads(result.stdout)
except json.JSONDecodeError:
app.logger.error("preauthkeys create returned invalid JSON: %s", result.stdout)
return None
return key_data.get("key")
# Routes
@app.route("/health")
def health():
return jsonify({"status": "ok"})
@app.route("/register", methods=["POST"])
@require_token
def register():
# Rate-limit by source IP
client_ip = request.remote_addr or "unknown"
if not _rate_limit_check(client_ip):
return jsonify({"error": "rate limit exceeded"}), 429
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "JSON body required"}), 400
hostname = data.get("hostname", "").strip()
mac = data.get("mac", "").strip()
if not hostname or not mac:
return jsonify({"error": "hostname and mac are required"}), 400
# Look up the numeric user ID (Headscale 0.28.0 requires -u <id>)
user_id = get_user_id(HEADSCALE_USER)
if user_id is None:
app.logger.error("Headscale user '%s' not found", HEADSCALE_USER)
return jsonify({"error": "provisioning user not found on Headscale server"}), 500
# Create a single-use pre-auth key
key = create_preauthkey(user_id, expiry=KEY_EXPIRY)
if key is None:
return jsonify({"error": "failed to create pre-auth key"}), 500
# Persist the registration record
_save_machine(hostname, mac)
login_server = os.environ.get("HEADSCALE_URL", "")
return jsonify({
"headscale_key": key,
"login_server": login_server,
"hostname": hostname,
})
@app.route("/machines")
@require_token
def machines():
return jsonify(list(_load_machines().values()))
# Entry point
if __name__ == "__main__":
DATA_DIR.mkdir(parents=True, exist_ok=True)
app.run(host="127.0.0.1", port=9090)
'';
# ── Headscale YAML config ──────────────────────────────────────────────────
headscaleConfig = pkgs.writeText "headscale.yaml" ''
server_url: https://${cfg.headscaleDomain}
listen_addr: 127.0.0.1:8080
metrics_listen_addr: 127.0.0.1:9090
# Logging
log:
level: info
# Database
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
# DERP (relay/STUN)
derp:
server:
enabled: false
urls:
- https://controlplane.tailscale.com/derpmap/default
auto_update_enabled: true
update_frequency: 24h
# Disable magic DNS by default (clients opt in)
dns:
magic_dns: false
base_domain: sovran.internal
# Node expiry
node_update_check_interval: 10s
'';
in
{
# ── Module options ─────────────────────────────────────────────────────────
options.sovranProvisioner = {
enable = lib.mkEnableOption "Sovran Systems provisioning server (Headscale + Flask API + Caddy)";
domain = lib.mkOption {
type = lib.types.str;
description = "Public FQDN for the provisioning API (e.g. prov.yourdomain.com)";
};
headscaleDomain = lib.mkOption {
type = lib.types.str;
description = "Public FQDN for the Headscale coordination server (e.g. hs.yourdomain.com)";
};
headscaleUser = lib.mkOption {
type = lib.types.str;
default = "sovran-deploy";
description = "Headscale user namespace for deployed machines";
};
adminUser = lib.mkOption {
type = lib.types.str;
default = "admin";
description = "Headscale user namespace for admin workstations";
};
keyExpiry = lib.mkOption {
type = lib.types.str;
default = "1h";
description = "Lifetime of generated pre-auth keys (e.g. 1h, 2h, 24h)";
};
rateLimitMax = lib.mkOption {
type = lib.types.int;
default = 10;
description = "Maximum number of /register calls per rateLimitWindow seconds per IP";
};
rateLimitWindow = lib.mkOption {
type = lib.types.int;
default = 60;
description = "Rate-limit sliding window in seconds";
};
};
# ── Module implementation ──────────────────────────────────────────────────
config = lib.mkIf cfg.enable {
# ── Headscale ─────────────────────────────────────────────────────────────
services.headscale = {
enable = true;
address = "127.0.0.1";
port = 8080;
settings = {
server_url = "https://${cfg.headscaleDomain}";
listen_addr = "127.0.0.1:8080";
database = {
type = "sqlite";
sqlite = { path = "/var/lib/headscale/db.sqlite"; };
};
dns = {
magic_dns = false;
base_domain = "sovran.internal";
};
derp = {
server.enabled = false;
urls = [ "https://controlplane.tailscale.com/derpmap/default" ];
auto_update_enabled = true;
update_frequency = "24h";
};
log.level = "info";
};
};
# ── Python / Flask dependencies ────────────────────────────────────────────
environment.systemPackages = [
pkgs.headscale
(pkgs.python3.withPackages (ps: [ ps.flask ]))
];
# ── Provisioner systemd service ────────────────────────────────────────────
systemd.services.sovran-provisioner = {
description = "Sovran provisioning API";
after = [ "network-online.target" "headscale.service" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
PROVISIONER_DATA_DIR = "/var/lib/sovran-provisioner";
HEADSCALE_USER = cfg.headscaleUser;
KEY_EXPIRY = cfg.keyExpiry;
RATE_LIMIT_MAX = toString cfg.rateLimitMax;
RATE_LIMIT_WINDOW = toString cfg.rateLimitWindow;
HEADSCALE_URL = "https://${cfg.headscaleDomain}";
};
serviceConfig = {
Type = "simple";
Restart = "on-failure";
RestartSec = "5s";
DynamicUser = false;
User = "sovran-provisioner";
Group = "sovran-provisioner";
StateDirectory = "sovran-provisioner";
RuntimeDirectory = "sovran-provisioner";
ExecStart = "${pkgs.python3.withPackages (ps: [ ps.flask ])}/bin/python3 ${provisionerScript}";
};
};
# ── Dedicated system user for the provisioner ──────────────────────────────
users.users.sovran-provisioner = {
isSystemUser = true;
group = "sovran-provisioner";
description = "Sovran provisioning API service user";
};
users.groups.sovran-provisioner = {};
# Allow the provisioner user to call headscale CLI
security.sudo.extraRules = [{
users = [ "sovran-provisioner" ];
commands = [{
command = "${pkgs.headscale}/bin/headscale";
options = [ "NOPASSWD" ];
}];
}];
# ── Bootstrap service (first-boot: create Headscale users + enroll token) ──
systemd.services.sovran-provisioner-bootstrap = {
description = "Bootstrap Headscale users and enrollment token";
after = [ "headscale.service" ];
wants = [ "headscale.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
StateDirectory = "sovran-provisioner";
};
path = [ pkgs.headscale pkgs.coreutils pkgs.openssl ];
script = ''
DATA_DIR="/var/lib/sovran-provisioner"
TOKEN_FILE="$DATA_DIR/enroll-token"
STAMP="$DATA_DIR/.bootstrap-done"
# Idempotent only run once
[ -f "$STAMP" ] && exit 0
# Wait for headscale socket to be ready
for i in $(seq 1 30); do
headscale users list -o json >/dev/null 2>&1 && break
sleep 2
done
# Create headscale users if they don't exist
headscale users list -o json | grep -q '"name":"${cfg.headscaleUser}"' \
|| headscale users create ${cfg.headscaleUser}
headscale users list -o json | grep -q '"name":"${cfg.adminUser}"' \
|| headscale users create ${cfg.adminUser}
# Generate enrollment token if not already present
if [ ! -f "$TOKEN_FILE" ] || [ ! -s "$TOKEN_FILE" ]; then
openssl rand -hex 32 > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
fi
touch "$STAMP"
echo "Bootstrap complete."
'';
};
# ── Caddy reverse proxy ────────────────────────────────────────────────────
services.caddy = {
enable = true;
virtualHosts."${cfg.headscaleDomain}" = {
extraConfig = ''
reverse_proxy 127.0.0.1:8080
'';
};
virtualHosts."${cfg.domain}" = {
extraConfig = ''
reverse_proxy 127.0.0.1:9090
'';
};
};
# ── Firewall ────────────────────────────────────────────────────────────────
networking.firewall = {
allowedTCPPorts = [ 80 443 ];
allowedUDPPorts = [ 3478 ];
};
};
}
+310 -14
View File
@@ -2,25 +2,196 @@
let
wallpaperSrc = ../../assets/wallpapers;
customWallpaper = pkgs.stdenvNoCC.mkDerivation {
pname = "sovran-systemsos-wallpaper";
version = "1.0";
src = pkgs.fetchurl {
url = "https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS_iso/raw/branch/main/post-install-scripts/Wallpaper_Dark_Wide.png";
sha256 = "0609gy0vp92fywl7pcr4y3mg05ca6pwxsnlsax14jd371fj4y7fn";
};
dontUnpack = true;
version = "2.0";
src = wallpaperSrc;
nativeBuildInputs = [ pkgs.librsvg ];
installPhase = ''
mkdir -p $out/share/backgrounds/sovran
cp $src $out/share/backgrounds/sovran/Wallpaper_Dark_Wide.png
'';
rsvg-convert -w 3440 -h 1440 \
$src/sovran-wallpaper-12-ultrawide-3440x1440.svg \
-o $out/share/backgrounds/sovran/sovran-ultrawide.png
'';
};
sovranThemeInit = pkgs.writeShellScriptBin "sovran-theme-init" ''
STAMP="$HOME/.config/sovran-theme-applied"
USER_DB="$HOME/.config/dconf/user"
# Always apply wallpaper on version change
WALLPAPER_VERSION="${customWallpaper.version}"
WALLPAPER_STAMP="$HOME/.config/sovran-wallpaper-version"
BG_DIR="/run/current-system/sw/share/backgrounds/sovran"
ULTRAWIDE="$BG_DIR/sovran-ultrawide.png"
CURRENT_WALLPAPER_VERSION=""
if [ -r "$WALLPAPER_STAMP" ]; then
read -r CURRENT_WALLPAPER_VERSION < "$WALLPAPER_STAMP"
fi
if [ "$CURRENT_WALLPAPER_VERSION" != "$WALLPAPER_VERSION" ]; then
if [ -f "$ULTRAWIDE" ]; then
${pkgs.dconf}/bin/dconf write /org/gnome/desktop/background/picture-uri "'file://$ULTRAWIDE'"
${pkgs.dconf}/bin/dconf write /org/gnome/desktop/background/picture-uri-dark "'file://$ULTRAWIDE'"
${pkgs.dconf}/bin/dconf write /org/gnome/desktop/background/picture-options "'zoom'"
mkdir -p "$(dirname "$WALLPAPER_STAMP")"
echo "$WALLPAPER_VERSION" > "$WALLPAPER_STAMP"
fi
fi
# Already applied skip
if [ -f "$STAMP" ]; then
exit 0
fi
# Existing machine updating user already has their own settings, don't overwrite
if [ -f "$USER_DB" ]; then
mkdir -p "$HOME/.config"
touch "$STAMP"
exit 0
fi
# Fresh install no user-db exists yet, apply full Sovran theme below
mkdir -p "$HOME/.config"
cat > "$HOME/.config/mimeapps.list" << EOF
[Default Applications]
text/html=brave-browser.desktop
x-scheme-handler/http=brave-browser.desktop
x-scheme-handler/https=brave-browser.desktop
x-scheme-handler/about=brave-browser.desktop
x-scheme-handler/unknown=brave-browser.desktop
EOF
${pkgs.dconf}/bin/dconf load / << EOF
[org/gnome/desktop/interface]
color-scheme='prefer-dark'
enable-animations=true
icon-theme='Papirus-Dark'
[org/gnome/settings-daemon/plugins/power]
sleep-inactive-ac-type='nothing'
sleep-inactive-ac-timeout=0
sleep-inactive-battery-type='nothing'
sleep-inactive-battery-timeout=0
idle-dim=false
ambient-enabled=false
power-button-action='nothing'
[org/gnome/desktop/session]
idle-delay=uint32 0
[org/gnome/desktop/screensaver]
lock-enabled=false
idle-activation-enabled=false
[org/gnome/mutter]
edge-tiling=false
[org/gnome/nautilus/icon-view]
default-zoom-level='large'
[org/gnome/nautilus/preferences]
default-folder-viewer='icon-view'
migrated-gtk-settings=true
search-filter-time-type='last_modified'
[org/gnome/shell]
disabled-extensions=['just-perfection-desktop@just-perfection']
enabled-extensions=['appindicatorsupport@rgcjonas.gmail.com', 'dash-to-dock-cosmic-@halfmexicanhalfamazing@gmail.com', 'Vitals@CoreCoding.com', 'dash-to-dock@micxgx.gmail.com', 'pop-shell@system76.com', 'date-menu-formatter@marcinjakubowski.github.com', 'light-style@gnome-shell-extensions.gcampax.github.com']
favorite-apps=['brave-browser.desktop', 'org.gnome.Settings.desktop', 'org.gnome.Nautilus.desktop', 'sovran-hub.desktop', 'org.gnome.Software.desktop', 'org.gnome.Geary.desktop', 'org.gnome.Contacts.desktop', 'org.gnome.Calendar.desktop', 'sparrow.desktop', 'Bisq.desktop', 'bisq2.desktop']
welcome-dialog-last-shown-version='48.4'
[org/gnome/desktop/app-folders]
folder-children=['Browsers', 'Office', 'Terminal', 'Chat', 'Bitcoin', 'Media', 'System']
[org/gnome/desktop/app-folders/folders/Browsers]
name='Browsers'
apps=['brave-browser.desktop', 'firefox.desktop', 'org.gnome.Epiphany.desktop']
[org/gnome/desktop/app-folders/folders/Office]
name='Office'
apps=['libreoffice-writer.desktop', 'libreoffice-calc.desktop', 'libreoffice-impress.desktop', 'libreoffice-draw.desktop', 'libreoffice-base.desktop', 'libreoffice-math.desktop', 'libreoffice-startcenter.desktop', 'org.gnome.TextEditor.desktop', 'org.gnome.gedit.desktop', 'org.gnome.Calculator.desktop', 'org.gnome.Calendar.desktop', 'org.gnome.Contacts.desktop', 'org.gnome.Geary.desktop', 'org.gnome.Evince.desktop', 'onlyoffice-desktopeditors.desktop', 'simple-scan.desktop', 'system-config-printer.desktop']
[org/gnome/desktop/app-folders/folders/Terminal]
name='Terminal'
apps=['org.gnome.Terminal.desktop', 'org.gnome.tweaks.desktop', 'gparted.desktop', 'htop.desktop', 'btop.desktop', 'ranger.desktop', 'org.gnome.Console.desktop']
[org/gnome/desktop/app-folders/folders/Chat]
name='Chat'
apps=['element-desktop.desktop']
[org/gnome/desktop/app-folders/folders/Bitcoin]
name='Bitcoin'
apps=['sparrow.desktop', 'Bisq.desktop', 'bisq2.desktop']
[org/gnome/desktop/app-folders/folders/Media]
name='Media'
apps=['org.gnome.Loupe.desktop', 'org.gnome.Totem.desktop', 'org.gnome.Snapshot.desktop', 'org.gnome.Weather.desktop', 'org.gnome.Maps.desktop', 'org.gnome.Clocks.desktop', 'org.gnome.Music.desktop', 'org.gnome.Characters.desktop', 'org.gnome.font-viewer.desktop']
[org/gnome/desktop/app-folders/folders/System]
name='System'
apps=['org.gnome.Settings.desktop', 'org.gnome.Nautilus.desktop', 'org.gnome.Software.desktop', 'sovran-hub.desktop', 'bitwarden.desktop', 'org.gnome.DiskUtility.desktop', 'org.gnome.SystemMonitor.desktop', 'org.gnome.Logs.desktop', 'org.gnome.Connections.desktop', 'org.gnome.baobab.desktop', 'zenity.desktop']
[org/gnome/shell/extensions/dash-to-dock]
background-color='rgb(0,0,0)'
background-opacity=0.50000000000000001
custom-background-color=true
dash-max-icon-size=47
dock-position='BOTTOM'
height-fraction=0.90000000000000002
preferred-monitor=-2
preferred-monitor-by-connector='Virtual-1'
show-trash=false
transparency-mode='FIXED'
[org/gnome/shell/extensions/date-menu-formatter]
font-size=12
pattern='EEEE, MMM d h:mm a'
text-align='center'
update-level=1
[org/gnome/shell/extensions/just-perfection]
support-notifier-showed-version=34
support-notifier-type=0
[org/gnome/shell/extensions/pop-shell]
tile-by-default=true
[org/gnome/shell/extensions/vitals]
hot-sensors=['_storage_free_', '_processor_usage_', '_memory_usage_']
[org/gnome/software]
first-run=false
[org/gtk/gtk4/settings/color-chooser]
selected-color=(true, 0.0, 0.0, 0.0, 1.0)
EOF
mkdir -p "$HOME/.config"
touch "$STAMP"
'';
in
{
environment.systemPackages = [ customWallpaper ];
environment.systemPackages = [ customWallpaper sovranThemeInit ];
environment.etc."xdg/autostart/sovran-theme-init.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Sovran Theme Init
Exec=${sovranThemeInit}/bin/sovran-theme-init
X-GNOME-Autostart-enabled=true
X-GNOME-Autostart-Phase=Application
NoDisplay=true
'';
programs.dconf.enable = true;
@@ -29,8 +200,8 @@ in
settings = {
"org/gnome/desktop/background" = {
picture-uri = "file:///run/current-system/sw/share/backgrounds/sovran/Wallpaper_Dark_Wide.png";
picture-uri-dark = "file:///run/current-system/sw/share/backgrounds/sovran/Wallpaper_Dark_Wide.png";
picture-uri = "file:///run/current-system/sw/share/backgrounds/sovran/sovran-ultrawide.png";
picture-uri-dark = "file:///run/current-system/sw/share/backgrounds/sovran/sovran-ultrawide.png";
picture-options = "zoom";
primary-color = "#000000";
secondary-color = "#000000";
@@ -47,6 +218,25 @@ in
icon-theme = "Papirus-Dark";
};
"org/gnome/settings-daemon/plugins/power" = {
sleep-inactive-ac-type = "nothing";
sleep-inactive-ac-timeout = lib.gvariant.mkInt32 0;
sleep-inactive-battery-type = "nothing";
sleep-inactive-battery-timeout = lib.gvariant.mkInt32 0;
idle-dim = false;
ambient-enabled = false;
power-button-action = "nothing";
};
"org/gnome/desktop/session" = {
idle-delay = lib.gvariant.mkUint32 0;
};
"org/gnome/desktop/screensaver" = {
lock-enabled = false;
idle-activation-enabled = false;
};
"org/gnome/evolution-data-server" = {
migrated = true;
};
@@ -82,13 +272,12 @@ in
"brave-browser.desktop"
"org.gnome.Settings.desktop"
"org.gnome.Nautilus.desktop"
"Sovran_SystemsOS_Updater.desktop"
"sovran-hub.desktop"
"org.gnome.Software.desktop"
"org.gnome.Geary.desktop"
"org.gnome.Contacts.desktop"
"org.gnome.Calendar.desktop"
"sparrow-desktop.desktop"
"sparrow.desktop"
"Bisq.desktop"
"bisq2.desktop"
];
@@ -96,6 +285,103 @@ in
welcome-dialog-last-shown-version = "48.4";
};
"org/gnome/desktop/app-folders" = {
folder-children = [ "Browsers" "Office" "Terminal" "Chat" "Bitcoin" "Media" "System" ];
};
"org/gnome/desktop/app-folders/folders/Browsers" = {
name = "Browsers";
apps = [
"brave-browser.desktop"
"firefox.desktop"
"org.gnome.Epiphany.desktop"
];
};
"org/gnome/desktop/app-folders/folders/Office" = {
name = "Office";
apps = [
"libreoffice-writer.desktop"
"libreoffice-calc.desktop"
"libreoffice-impress.desktop"
"libreoffice-draw.desktop"
"libreoffice-base.desktop"
"libreoffice-math.desktop"
"libreoffice-startcenter.desktop"
"org.gnome.TextEditor.desktop"
"org.gnome.gedit.desktop"
"org.gnome.Calculator.desktop"
"org.gnome.Calendar.desktop"
"org.gnome.Contacts.desktop"
"org.gnome.Geary.desktop"
"org.gnome.Evince.desktop"
"onlyoffice-desktopeditors.desktop"
"simple-scan.desktop"
"system-config-printer.desktop"
];
};
"org/gnome/desktop/app-folders/folders/Terminal" = {
name = "Terminal";
apps = [
"org.gnome.Terminal.desktop"
"org.gnome.tweaks.desktop"
"gparted.desktop"
"htop.desktop"
"btop.desktop"
"ranger.desktop"
"org.gnome.Console.desktop"
];
};
"org/gnome/desktop/app-folders/folders/Chat" = {
name = "Chat";
apps = [
"element-desktop.desktop"
];
};
"org/gnome/desktop/app-folders/folders/Bitcoin" = {
name = "Bitcoin";
apps = [
"sparrow.desktop"
"Bisq.desktop"
"bisq2.desktop"
];
};
"org/gnome/desktop/app-folders/folders/Media" = {
name = "Media";
apps = [
"org.gnome.Loupe.desktop"
"org.gnome.Totem.desktop"
"org.gnome.Snapshot.desktop"
"org.gnome.Weather.desktop"
"org.gnome.Maps.desktop"
"org.gnome.Clocks.desktop"
"org.gnome.Music.desktop"
"org.gnome.Characters.desktop"
"org.gnome.font-viewer.desktop"
];
};
"org/gnome/desktop/app-folders/folders/System" = {
name = "System";
apps = [
"org.gnome.Settings.desktop"
"org.gnome.Nautilus.desktop"
"org.gnome.Software.desktop"
"sovran-hub.desktop"
"bitwarden.desktop"
"org.gnome.DiskUtility.desktop"
"org.gnome.SystemMonitor.desktop"
"org.gnome.Logs.desktop"
"org.gnome.Connections.desktop"
"org.gnome.baobab.desktop"
"zenity.desktop"
];
};
"org/gnome/shell/extensions/dash-to-dock" = {
background-color = "rgb(0,0,0)";
background-opacity = 0.50000000000000001;
@@ -146,4 +432,14 @@ in
}
];
}
xdg.mime.defaultApplications = {
"text/html" = "brave-browser.desktop";
"x-scheme-handler/http" = "brave-browser.desktop";
"x-scheme-handler/https" = "brave-browser.desktop";
"x-scheme-handler/about" = "brave-browser.desktop";
"x-scheme-handler/unknown" = "brave-browser.desktop";
};
environment.sessionVariables.BROWSER = "brave-browser";
}
+22 -1
View File
@@ -12,9 +12,29 @@ lib.mkIf userExists {
"d /home/${userName}/.ssh 0700 ${userName} users -"
];
systemd.services.ssh-passphrase-setup = {
description = "Generate per-device SSH key passphrase";
wantedBy = [ "multi-user.target" ];
before = [ "factory-ssh-keygen.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
path = [ pkgs.pwgen pkgs.coreutils ];
script = ''
if [ ! -f "/var/lib/secrets/ssh-passphrase" ]; then
mkdir -p /var/lib/secrets
pwgen -s 20 1 > /var/lib/secrets/ssh-passphrase
chmod 600 /var/lib/secrets/ssh-passphrase
fi
'';
};
systemd.services.factory-ssh-keygen = {
description = "Generate factory SSH key for ${userName} if missing";
wantedBy = [ "multi-user.target" ];
after = [ "ssh-passphrase-setup.service" ];
requires = [ "ssh-passphrase-setup.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
@@ -22,7 +42,8 @@ lib.mkIf userExists {
path = [ pkgs.openssh pkgs.coreutils ];
script = ''
if [ ! -f "${keyPath}" ]; then
ssh-keygen -q -N "gosovransystems" -t ed25519 -f "${keyPath}"
PASSPHRASE=$(cat /var/lib/secrets/ssh-passphrase)
ssh-keygen -q -N "$PASSPHRASE" -t ed25519 -f "${keyPath}"
chown ${userName}:users "${keyPath}" "${keyPath}.pub"
chmod 600 "${keyPath}"
chmod 644 "${keyPath}.pub"
+21
View File
@@ -0,0 +1,21 @@
{ config, lib, pkgs, ... }:
{
# ── Always-on localhost SSH ────────────────────────────────────
# Provides "ssh root@localhost" for local root access and Hub
# operations. Binds exclusively to 127.0.0.1 — zero network exposure.
# The sshd *feature flag* in sshd.nix extends this to 0.0.0.0 and
# opens port 22 on the firewall when the user enables remote SSH.
services.openssh = {
enable = true;
listenAddresses = lib.mkDefault [
{ addr = "127.0.0.1"; port = 22; }
];
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "yes";
};
};
}
+60
View File
@@ -0,0 +1,60 @@
{ config, lib, pkgs, ... }:
# ── Tech Support — restricted support user & tooling ─────────────────────────
#
# This module declaratively provisions the `sovran-support` system account that
# the Sovran Hub uses when a user enables remote tech support access.
#
# Security design:
# • Support staff log in as `sovran-support`, not as root.
# • Protected directories (LND, bitcoind, nix-bitcoin-secrets, /home) are locked with POSIX ACLs
# (u:sovran-support:---) by the Hub API as soon as a session is started.
# • The Hub web UI lets the user grant time-limited access to wallet files
# and view a full audit log of every session event.
# • Scoped sudo rules allow support staff to edit custom.nix, trigger rebuilds,
# restart services, and read logs — without full root or wallet access.
#
# The `acl` package provides the `setfacl` / `getfacl` utilities required by
# the Hub's _apply_wallet_acls() and _revoke_wallet_acls() helpers.
{
# ── System packages ────────────────────────────────────────────────────────
environment.systemPackages = [ pkgs.acl ];
# ── Restricted support user and group ─────────────────────────────────────
users.groups.sovran-support = {};
users.users.sovran-support = {
isSystemUser = true;
group = "sovran-support";
description = "Sovran Systems restricted tech support account";
home = "/var/lib/sovran-support";
createHome = false;
# Use a real interactive shell so support staff can run diagnostic commands;
# the Hub API limits *when* they can connect (key present only while active).
shell = pkgs.bashInteractive;
};
# ── Home and SSH directories ───────────────────────────────────────────────
# tmpfiles ensures the directories exist at boot with the correct ownership
# even before the first support session is started.
systemd.tmpfiles.rules = [
"d /var/lib/sovran-support 0700 sovran-support sovran-support -"
"d /var/lib/sovran-support/.ssh 0700 sovran-support sovran-support -"
];
# ── Scoped sudo rules for support staff ───────────────────────────────────
# Grants only the minimum privileges needed for a support session.
# Support staff cannot stop/disable/mask services or access wallet files.
security.sudo.extraRules = [
{
users = [ "sovran-support" ];
commands = [
{ command = "/run/current-system/sw/bin/nano /etc/nixos/custom.nix"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/nano /etc/nixos/configuration.nix"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/nixos-rebuild switch --flake /etc/nixos"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/systemctl restart *"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/journalctl *"; options = [ "NOPASSWD" ]; }
];
}
];
}

Some files were not shown because too many files have changed in this diff Show More