diff --git a/app/sovran_systemsos_hub/application.py b/app/sovran_systemsos_hub/application.py index 7e2f4d5..4f52e87 100644 --- a/app/sovran_systemsos_hub/application.py +++ b/app/sovran_systemsos_hub/application.py @@ -19,6 +19,21 @@ APP_ID = "com.sovransystems.hub" # Initialize libadwaita BEFORE any widget creation Adw.init() +# Category display order and labels +CATEGORY_ORDER = [ + ("infrastructure", "Infrastructure"), + ("bitcoin", "Bitcoin"), + ("communication", "Communication"), + ("apps", "Self-Hosted Apps"), + ("nostr", "Nostr"), +] + +ROLE_LABELS = { + "server_plus_desktop": "Server + Desktop", + "desktop": "Desktop Only", + "node": "Bitcoin Node", +} + class SovranHubWindow(Adw.ApplicationWindow): @@ -27,7 +42,7 @@ class SovranHubWindow(Adw.ApplicationWindow): application=app, title="Sovran_SystemsOS Hub", default_width=680, - default_height=700, + default_height=780, ) self._config = config self._tiles = [] @@ -42,6 +57,18 @@ class SovranHubWindow(Adw.ApplicationWindow): ) header = Adw.HeaderBar() + + # Show active role in header + role = config.get("role", "server_plus_desktop") + role_label = ROLE_LABELS.get(role, role) + role_tag = Gtk.Label( + label=role_label, + css_classes=["caption", "role-badge"], + ) + header.set_title_widget( + self._build_title_box(role_label) + ) + refresh_btn = Gtk.Button( icon_name="view-refresh-symbolic", tooltip_text="Refresh now", @@ -49,26 +76,17 @@ class SovranHubWindow(Adw.ApplicationWindow): refresh_btn.connect("clicked", lambda _b: self._refresh_all()) header.pack_end(refresh_btn) - self._flowbox = Gtk.FlowBox( - max_children_per_line=4, - min_children_per_line=2, - selection_mode=Gtk.SelectionMode.NONE, - homogeneous=True, - row_spacing=12, - column_spacing=12, - margin_top=16, - margin_bottom=16, - margin_start=16, - margin_end=16, - halign=Gtk.Align.CENTER, - valign=Gtk.Align.START, + # Main vertical layout + self._main_box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + spacing=0, ) scrolled = Gtk.ScrolledWindow( hscrollbar_policy=Gtk.PolicyType.NEVER, vscrollbar_policy=Gtk.PolicyType.AUTOMATIC, vexpand=True, - child=self._flowbox, + child=self._main_box, ) toolbar_view = Adw.ToolbarView() @@ -82,19 +100,85 @@ class SovranHubWindow(Adw.ApplicationWindow): if interval and interval > 0: GLib.timeout_add_seconds(interval, self._auto_refresh) + def _build_title_box(self, role_label): + box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + halign=Gtk.Align.CENTER, + ) + box.append(Gtk.Label( + label="Sovran_SystemsOS Hub", + css_classes=["title"], + )) + box.append(Gtk.Label( + label=role_label, + css_classes=["caption", "dim-label"], + )) + return box + def _build_tiles(self): method = self._config.get("command_method", "systemctl") - for entry in self._config.get("services", []): - tile = ServiceTile( - name=entry.get("name", entry["unit"]), - unit=entry["unit"], - scope=entry.get("type", "system"), - method=method, - icon_name=entry.get("icon", ""), - enabled=entry.get("enabled", True), + services = self._config.get("services", []) + + # Group services by category + grouped = {} + for entry in services: + cat = entry.get("category", "other") + grouped.setdefault(cat, []).append(entry) + + for cat_key, cat_label in CATEGORY_ORDER: + entries = grouped.get(cat_key, []) + if not entries: + continue + + # Section header + section_label = Gtk.Label( + label=cat_label, + css_classes=["title-4"], + halign=Gtk.Align.START, + margin_top=20, + margin_bottom=4, + margin_start=24, ) - self._flowbox.append(tile) - self._tiles.append(tile) + self._main_box.append(section_label) + + # Separator + sep = Gtk.Separator( + orientation=Gtk.Orientation.HORIZONTAL, + margin_start=24, + margin_end=24, + margin_bottom=8, + ) + self._main_box.append(sep) + + # FlowBox for this category + flowbox = Gtk.FlowBox( + max_children_per_line=4, + min_children_per_line=2, + selection_mode=Gtk.SelectionMode.NONE, + homogeneous=True, + row_spacing=12, + column_spacing=12, + margin_top=4, + margin_bottom=8, + margin_start=16, + margin_end=16, + halign=Gtk.Align.CENTER, + valign=Gtk.Align.START, + ) + + for entry in entries: + tile = ServiceTile( + name=entry.get("name", entry["unit"]), + unit=entry["unit"], + scope=entry.get("type", "system"), + method=method, + icon_name=entry.get("icon", ""), + enabled=entry.get("enabled", True), + ) + flowbox.append(tile) + self._tiles.append(tile) + + self._main_box.append(flowbox) # Defer first status poll so the window renders immediately GLib.idle_add(self._refresh_all) diff --git a/app/style.css b/app/style.css index d958fd9..b0214da 100644 --- a/app/style.css +++ b/app/style.css @@ -8,6 +8,12 @@ .sovran-tile:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); } -.success { color: #2ec27e; } -.warning { color: #e5a50a; } -.disabled-label { color: #888888; font-style: italic; } \ No newline at end of file +.success { color: #2ec27e; } +.warning { color: #e5a50a; } +.error { color: #e01b24; } +.disabled-label { color: #888888; font-style: italic; } +.role-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 0.75em; +} \ No newline at end of file diff --git a/modules/core/sovran-hub.nix b/modules/core/sovran-hub.nix index 4fc24cc..2980a2e 100644 --- a/modules/core/sovran-hub.nix +++ b/modules/core/sovran-hub.nix @@ -3,38 +3,54 @@ let cfg = config.sovran_systemsOS; + # ── Determine Bitcoin implementation label ─────────────────── + bitcoinImplName = + if cfg.features.bitcoin-core then "Bitcoin Core" + else if cfg.features.bip110 then "Bitcoin Knots + BIP110" + else "Bitcoin Knots"; + monitoredServices = - # ── Always-on infrastructure ─────────────────────────────── + # ── Infrastructure (always present) ──────────────────────── [ - { name = "Caddy"; unit = "caddy.service"; type = "system"; icon = "caddy"; enabled = true; } - { name = "Tor"; unit = "tor.service"; type = "system"; icon = "tor"; enabled = true; } + { name = "Caddy"; unit = "caddy.service"; type = "system"; icon = "caddy"; enabled = true; category = "infrastructure"; } + { name = "Tor"; unit = "tor.service"; type = "system"; icon = "tor"; enabled = true; category = "infrastructure"; } ] - # ── Bitcoin ecosystem ────────────────────────────────────── + # ── Bitcoin Ecosystem ────────────────────────────────────── ++ [ - { name = "Bitcoind"; unit = "bitcoind.service"; type = "system"; icon = "bitcoind"; enabled = cfg.services.bitcoin; } - { name = "Electrs"; unit = "electrs.service"; type = "system"; icon = "electrs"; enabled = cfg.services.bitcoin; } - { name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; enabled = cfg.services.bitcoin; } - { name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; enabled = cfg.services.bitcoin; } - { name = "BTCPayserver"; unit = "btcpayserver.service"; type = "system"; icon = "btcpayserver"; enabled = cfg.services.bitcoin; } + { name = bitcoinImplName; unit = "bitcoind.service"; type = "system"; icon = "bitcoind"; enabled = cfg.services.bitcoin; category = "bitcoin"; } + { name = "Electrs"; unit = "electrs.service"; type = "system"; icon = "electrs"; enabled = cfg.services.bitcoin; category = "bitcoin"; } + { name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; enabled = cfg.services.bitcoin; category = "bitcoin"; } + { name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; enabled = cfg.services.bitcoin; category = "bitcoin"; } + { name = "BTCPayserver"; unit = "btcpayserver.service"; type = "system"; icon = "btcpayserver"; enabled = cfg.services.bitcoin; category = "bitcoin"; } + { name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin"; } ] - # ── Other services ───────────────────────────────────────── + # ── Communication ────────────────────────────────────────── ++ [ - { name = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; enabled = cfg.services.synapse; } - { name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; enabled = cfg.services.vaultwarden; } - { name = "Nextcloud"; unit = "phpfpm-nextcloud.service"; type = "system"; icon = "nextcloud"; enabled = cfg.services.nextcloud; } - { name = "WordPress"; unit = "phpfpm-wordpress.service"; type = "system"; icon = "wordpress"; enabled = cfg.services.wordpress; } + { name = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; enabled = cfg.services.synapse; category = "communication"; } + { name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "livekit"; enabled = cfg.features.element-calling; category = "communication"; } ] - # ── Optional features ────────────────────────────────────── + # ── Self-Hosted Apps ─────────────────────────────────────── ++ [ - { name = "Haven Relay"; unit = "haven-relay.service"; type = "system"; icon = "haven"; enabled = cfg.features.haven; } - { name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; } - { name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "livekit"; enabled = cfg.features.element-calling; } + { name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; enabled = cfg.services.vaultwarden; category = "apps"; } + { name = "Nextcloud"; unit = "phpfpm-nextcloud.service"; type = "system"; icon = "nextcloud"; enabled = cfg.services.nextcloud; category = "apps"; } + { name = "WordPress"; unit = "phpfpm-wordpress.service"; type = "system"; icon = "wordpress"; enabled = cfg.services.wordpress; category = "apps"; } + ] + # ── Nostr / Relay ────────────────────────────────────────── + ++ [ + { name = "Haven Relay"; unit = "haven-relay.service"; type = "system"; icon = "haven"; enabled = cfg.features.haven; category = "nostr"; } ]; + # ── Determine active role name ─────────────────────────────── + activeRole = + if cfg.roles.desktop then "desktop" + else if cfg.roles.node then "node" + else "server_plus_desktop"; + generatedConfig = pkgs.writeText "sovran-hub-config.json" (builtins.toJSON { refresh_interval = 5; command_method = "systemctl"; + role = activeRole; services = monitoredServices; }); @@ -66,7 +82,7 @@ let installPhase = '' runHook preInstall - # ── Python source ───────────────────────────────���───────── + # ── Python source ───────────────────────────────────────── install -d $out/lib/sovran-hub cp -r sovran_systemsos_hub $out/lib/sovran-hub/