diff --git a/iso/assets/splash-logo.png b/iso/assets/splash-logo.png
index 788d6d0..cbfe64f 100755
Binary files a/iso/assets/splash-logo.png and b/iso/assets/splash-logo.png differ
diff --git a/iso/installer.py b/iso/installer.py
index 8ae9f48..1ab7c17 100644
--- a/iso/installer.py
+++ b/iso/installer.py
@@ -117,11 +117,8 @@ class InstallerWindow(Adw.ApplicationWindow):
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 ─────────────────────────────────────────────────
@@ -197,54 +194,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("Welcome to Sovran SystemsOS")
+ title.set_margin_top(8)
+ hero.append(title)
+
+ sub = Gtk.Label()
+ sub.set_markup("Be Digitally Sovereign")
+ 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(
+ ""
+ "Before installation begins, please ensure you have an active internet connection.\n"
+ "Sovran SystemsOS downloads packages during installation and requires internet access\n"
+ "to complete the process. Connect via Ethernet cable or configure Wi-Fi now."
+ ""
+ )
+ 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 ─────────────────────────────────────────────
@@ -341,140 +377,7 @@ class InstallerWindow(Adw.ApplicationWindow):
if radio.get_active():
self.role = radio.get_name()
break
- self.push_port_requirements()
-
- # ── Step 1b: Port Requirements Notice ─────────────────────────────────
-
- def push_port_requirements(self):
- """Inform the user about required router/firewall ports before install."""
- outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
-
- # Detect internal IP at install time
- internal_ip = "this machine's LAN IP"
- try:
- import subprocess as _sp
- _r = _sp.run(["hostname", "-I"], capture_output=True, text=True, timeout=5)
- if _r.returncode == 0:
- _parts = _r.stdout.strip().split()
- if _parts:
- internal_ip = _parts[0]
- except Exception:
- pass
-
- # Warning banner
- banner = Adw.Banner()
- banner.set_title(
- "⚠ Port Forwarding Setup Required — configure your router before install"
- )
- banner.set_revealed(True)
- banner.set_margin_top(16)
- banner.set_margin_start(40)
- banner.set_margin_end(40)
- outer.append(banner)
-
- intro = Gtk.Label()
- intro.set_markup(
- ""
- "Many Sovran_SystemsOS features require port forwarding to be configured "
- "in your router's admin panel. This means telling your router to forward "
- "specific ports to this machine's internal LAN IP.\n\n"
- "Services like Element Video/Audio Calling and Matrix Federation "
- "will not work for clients outside your LAN unless these ports are "
- "forwarded to this machine."
- ""
- )
- intro.set_wrap(True)
- intro.set_justify(Gtk.Justification.FILL)
- intro.set_margin_top(14)
- intro.set_margin_start(40)
- intro.set_margin_end(40)
- outer.append(intro)
-
- ip_label = Gtk.Label()
- ip_label.set_markup(
- f""
- f" Forward ports to this machine's internal IP: {internal_ip}"
- f""
- )
- ip_label.set_margin_top(10)
- ip_label.set_margin_start(40)
- ip_label.set_margin_end(40)
- outer.append(ip_label)
-
- sw = Gtk.ScrolledWindow()
- sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
- sw.set_vexpand(True)
- sw.set_margin_start(40)
- sw.set_margin_end(40)
- sw.set_margin_top(12)
- sw.set_margin_bottom(8)
-
- ports_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
-
- port_sections = [
- (
- "🌐 Web / HTTPS (all domain-based services)",
- [("80", "TCP", "HTTP (redirects to HTTPS)"),
- ("443", "TCP", "HTTPS")],
- ),
- (
- "💬 Matrix Federation (Matrix-Synapse)",
- [("8448", "TCP", "Server-to-server federation")],
- ),
- (
- "🎥 Element Video & Audio Calling (LiveKit / Element-call)",
- [("7881", "TCP", "LiveKit WebRTC signalling"),
- ("7882-7894", "UDP", "LiveKit media streams"),
- ("5349", "TCP", "TURN over TLS"),
- ("3478", "UDP", "TURN (STUN / relay)"),
- ("30000-40000", "TCP/UDP", "TURN relay (WebRTC media)")],
- ),
- (
- "🖥 Remote SSH (optional — only if you want WAN SSH access)",
- [("22", "TCP", "SSH")],
- ),
- ]
-
- for section_title, rows in port_sections:
- group = Adw.PreferencesGroup()
- group.set_title(section_title)
-
- for port, proto, desc in rows:
- row = Adw.ActionRow()
- row.set_title(f"Port {port} ({proto})")
- row.set_subtitle(desc)
- group.add(row)
-
- ports_box.append(group)
-
- note = Gtk.Label()
- note.set_markup(
- ""
- "ℹ In your router's admin panel (usually at 192.168.1.1), find the "
- "\"Port Forwarding\" section and add a rule for each port above with "
- "the destination set to this machine's internal IP. "
- "These ports only need to be forwarded to this specific machine — "
- "this does NOT expose your entire network.\n"
- "To verify forwarding is working, test from a device on a different network "
- "(e.g. a phone on mobile data) or check your router's port forwarding page."
- ""
- )
- note.set_wrap(True)
- note.set_justify(Gtk.Justification.FILL)
- note.set_margin_top(8)
- ports_box.append(note)
-
- sw.set_child(ports_box)
- outer.append(sw)
-
- outer.append(self.nav_row(
- back_label="← Back",
- back_cb=lambda b: self.nav.pop(),
- next_label="I Understand →",
- next_cb=lambda b: self.push_disk_detect(),
- ))
-
- self.push_page("Network Port Requirements", outer, show_back=True)
+ self.push_disk_detect()
# ── Step 2a: Disk Detect ──────────────────────────────────────────────
@@ -773,7 +676,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)
@@ -830,10 +732,10 @@ class InstallerWindow(Adw.ApplicationWindow):
cmd = [
"sudo", "disko", "--mode", "destroy,format,mount",
f"{FLAKE}/iso/disko.nix",
- "--arg", "device", f'"{boot_path}"'
+ "--arg", "device", boot_path
]
if data_path:
- cmd += ["--arg", "dataDevice", f'"{data_path}"']
+ cmd += ["--arg", "dataDevice", data_path]
run_stream(cmd, buf)
GLib.idle_add(append_text, buf, "\n=== Generating hardware config ===\n")
diff --git a/iso/plymouth-theme.nix b/iso/plymouth-theme.nix
index 9b6ae18..9075659 100644
--- a/iso/plymouth-theme.nix
+++ b/iso/plymouth-theme.nix
@@ -10,15 +10,15 @@ pkgs.stdenv.mkDerivation {
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'
+ cat > $out/share/plymouth/themes/sovran/sovran.plymouth < $out/share/plymouth/themes/sovran/sovran.script <<'EOF'