updated layout in hub

This commit is contained in:
2026-03-31 16:20:13 -05:00
parent b24870dcb1
commit 4178ed07fb
3 changed files with 153 additions and 47 deletions

View File

@@ -19,6 +19,21 @@ APP_ID = "com.sovransystems.hub"
# Initialize libadwaita BEFORE any widget creation # Initialize libadwaita BEFORE any widget creation
Adw.init() 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): class SovranHubWindow(Adw.ApplicationWindow):
@@ -27,7 +42,7 @@ class SovranHubWindow(Adw.ApplicationWindow):
application=app, application=app,
title="Sovran_SystemsOS Hub", title="Sovran_SystemsOS Hub",
default_width=680, default_width=680,
default_height=700, default_height=780,
) )
self._config = config self._config = config
self._tiles = [] self._tiles = []
@@ -42,6 +57,18 @@ class SovranHubWindow(Adw.ApplicationWindow):
) )
header = Adw.HeaderBar() 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( refresh_btn = Gtk.Button(
icon_name="view-refresh-symbolic", icon_name="view-refresh-symbolic",
tooltip_text="Refresh now", tooltip_text="Refresh now",
@@ -49,26 +76,17 @@ class SovranHubWindow(Adw.ApplicationWindow):
refresh_btn.connect("clicked", lambda _b: self._refresh_all()) refresh_btn.connect("clicked", lambda _b: self._refresh_all())
header.pack_end(refresh_btn) header.pack_end(refresh_btn)
self._flowbox = Gtk.FlowBox( # Main vertical layout
max_children_per_line=4, self._main_box = Gtk.Box(
min_children_per_line=2, orientation=Gtk.Orientation.VERTICAL,
selection_mode=Gtk.SelectionMode.NONE, spacing=0,
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,
) )
scrolled = Gtk.ScrolledWindow( scrolled = Gtk.ScrolledWindow(
hscrollbar_policy=Gtk.PolicyType.NEVER, hscrollbar_policy=Gtk.PolicyType.NEVER,
vscrollbar_policy=Gtk.PolicyType.AUTOMATIC, vscrollbar_policy=Gtk.PolicyType.AUTOMATIC,
vexpand=True, vexpand=True,
child=self._flowbox, child=self._main_box,
) )
toolbar_view = Adw.ToolbarView() toolbar_view = Adw.ToolbarView()
@@ -82,19 +100,85 @@ class SovranHubWindow(Adw.ApplicationWindow):
if interval and interval > 0: if interval and interval > 0:
GLib.timeout_add_seconds(interval, self._auto_refresh) 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): def _build_tiles(self):
method = self._config.get("command_method", "systemctl") method = self._config.get("command_method", "systemctl")
for entry in self._config.get("services", []): services = self._config.get("services", [])
tile = ServiceTile(
name=entry.get("name", entry["unit"]), # Group services by category
unit=entry["unit"], grouped = {}
scope=entry.get("type", "system"), for entry in services:
method=method, cat = entry.get("category", "other")
icon_name=entry.get("icon", ""), grouped.setdefault(cat, []).append(entry)
enabled=entry.get("enabled", True),
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._main_box.append(section_label)
self._tiles.append(tile)
# 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 # Defer first status poll so the window renders immediately
GLib.idle_add(self._refresh_all) GLib.idle_add(self._refresh_all)

View File

@@ -8,6 +8,12 @@
.sovran-tile:hover { .sovran-tile:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
} }
.success { color: #2ec27e; } .success { color: #2ec27e; }
.warning { color: #e5a50a; } .warning { color: #e5a50a; }
.error { color: #e01b24; }
.disabled-label { color: #888888; font-style: italic; } .disabled-label { color: #888888; font-style: italic; }
.role-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75em;
}

View File

@@ -3,38 +3,54 @@
let let
cfg = config.sovran_systemsOS; 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 = monitoredServices =
# ── Always-on infrastructure ─────────────────────────────── # ── Infrastructure (always present) ────────────────────────
[ [
{ name = "Caddy"; unit = "caddy.service"; type = "system"; icon = "caddy"; 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; } { 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 = 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; } { 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; } { 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; } { 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; } { 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 = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; enabled = cfg.services.synapse; category = "communication"; }
{ name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; enabled = cfg.services.vaultwarden; } { name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "livekit"; enabled = cfg.features.element-calling; category = "communication"; }
{ 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; }
] ]
# ── Optional features ────────────────────────────────────── # ── Self-Hosted Apps ──────────────────────────────────────
++ [ ++ [
{ name = "Haven Relay"; unit = "haven-relay.service"; type = "system"; icon = "haven"; enabled = cfg.features.haven; } { name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; enabled = cfg.services.vaultwarden; category = "apps"; }
{ name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; } { name = "Nextcloud"; unit = "phpfpm-nextcloud.service"; type = "system"; icon = "nextcloud"; enabled = cfg.services.nextcloud; category = "apps"; }
{ name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "livekit"; enabled = cfg.features.element-calling; } { 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" generatedConfig = pkgs.writeText "sovran-hub-config.json"
(builtins.toJSON { (builtins.toJSON {
refresh_interval = 5; refresh_interval = 5;
command_method = "systemctl"; command_method = "systemctl";
role = activeRole;
services = monitoredServices; services = monitoredServices;
}); });
@@ -66,7 +82,7 @@ let
installPhase = '' installPhase = ''
runHook preInstall runHook preInstall
# Python source <EFBFBD><EFBFBD><EFBFBD> # Python source
install -d $out/lib/sovran-hub install -d $out/lib/sovran-hub
cp -r sovran_systemsos_hub $out/lib/sovran-hub/ cp -r sovran_systemsos_hub $out/lib/sovran-hub/