Improve background check handling, and hide services until connection is proved.

This commit is contained in:
2026-08-26 21:32:18 +01:00
parent 80c2fc086f
commit d2cb0b0645
7 changed files with 223 additions and 58 deletions

View File

@@ -2,7 +2,8 @@ import subprocess
import shutil
class ActionDispatcher:
def dispatch(self, service_type, config):
def dispatch(self, config):
service_type = config.get("type", "command")
print(f"Executing action -> Type: {service_type}")
if service_type == "browser":

View File

@@ -1,24 +1,74 @@
# checks.py
import socket
import threading
import urllib.request
import urllib.error
class SystemChecks:
def __init__(self, network_targets, ziti_target):
self.network_targets = network_targets
self.ziti_target = ziti_target
self.network_online = None
self.ziti_online = None
self._stop_event = threading.Event()
self._thread = None
@property
def ready(self):
"""Return True when network & ziti are both available"""
return (
self.network_online is True
and self.ziti_online is True
)
def start(self):
"""Start background status polling"""
if self._thread is not None:
return
self._thread = threading.Thread(
target=self._poll,
daemon=True
)
self._thread.start()
def stop(self):
"""Stop background status polling"""
self._stop_event.set()
def _poll(self):
"""Continuously check system status"""
while not self._stop_event.is_set():
self.network_online = self.check_network_status()
self.ziti_online = self.check_ziti_status()
print(
f"System status => "
f"Network: {self.network_online}, "
f"Ziti: {self.ziti_online}"
)
if self.ready:
interval = 30
else:
interval = 3
self._stop_event.wait(interval)
def check_network_status(self):
"""Loops through configured network targets and returns True if any succeed."""
"""Loops through configured network targets and return True if any respond"""
for host, port in self.network_targets:
if self._tcp_ping(host, int(port), timeout=1.5):
return True
return False
def check_ziti_status(self):
"""Makes an HTTP request to the configured address."""
"""Makes HTTP request to Ziti endpoint"""
if not self.ziti_target:
return False

View File

@@ -6,7 +6,7 @@ DEFAULT_CONFIG = {
"app_title": "FJLA Gateway",
"checks": {
"network_targets": [["1.1.1.1", 53], ["8.8.8.8", 53]],
"ziti_target": ["127.0.0.1", 3020],
"ziti_target": "http://test.ziti",
},
"services": [
{

View File

@@ -15,7 +15,9 @@ class LauncherApp(Gtk.Application):
super().__init__(application_id="com.local.Launcher")
self.config = load_launcher_config()
check_cfg = self.config.get("checks", {})
self.checks = SystemChecks(
network_targets=check_cfg.get("network_targets"),
ziti_target=check_cfg.get("ziti_target"),
@@ -23,35 +25,24 @@ class LauncherApp(Gtk.Application):
self.dispatcher = ActionDispatcher()
self.ui = UIController(
status_provider_callback=lambda: {
"net": self.checks.check_network_status(),
"ziti": self.checks.check_ziti_status(),
},
system_checks=self.checks,
power_action_callback=self.dispatcher.dispatch,
service_action_callback=self.dispatcher.dispatch,
services=self.config.get("services", []),
)
def do_activate(self):
self.checks.start()
self.ui.set_app_title(self.config.get("app_title", "Launcher"))
for service in self.config.get("services", []):
title = service.get("title", "Service")
desc = service.get("desc", "")
bg_img = service.get("bg_image", "")
self.ui.add_card(
title,
desc,
bg_img,
lambda svc=service: self.dispatcher.dispatch(
svc.get("type", "command"),
svc,
),
)
self.ui.setup_exit_shortcut(self.quit)
self.ui.present(self)
def do_shutdown(self):
self.checks.stop()
Gtk.Application.do_shutdown(self)
if __name__ == "__main__":
app = LauncherApp()

View File

@@ -5,6 +5,21 @@ window {
font-family: sans-serif;
}
/* Startup Messages */
.startup-message {
padding: 32px;
}
.startup-message-title {
font-size: 55px;
font-weight: 700;
}
.startup-message-description {
font-size: 28px;
opacity: 0.7;
}
/* Large, prominent clock */
label.date-display {
font-size: 28px;

View File

@@ -12,47 +12,78 @@ class UIController:
self,
ui_file="window.ui",
css_file="style.css",
status_provider_callback=None,
power_action_callback=None
system_checks=None,
power_action_callback=None,
service_action_callback=None,
services = None
):
self.builder = Gtk.Builder.new_from_file(ui_file)
self.window = self.builder.get_object("main_window")
self._load_styles(css_file)
self._setup_clock()
self._initialist_power_options()
self._initialise_power_options()
self.power_action_callback = power_action_callback
self.system_checks = system_checks
self.service_action_callback = service_action_callback
self.services = services or []
if status_provider_callback:
self.setup_status_poller(status_provider_callback)
self._cards_created = False
self._services_visible = False
def setup_status_poller(self, status_provider_callback, interval_seconds=20):
""" Accepts any function that returns a dictionary of statuses,
currently: {'net':True, 'ziti':True}
"""
def poll():
try:
statuses = status_provider_callback()
print(f"Status result {statuses}")
if self.system_checks:
self._setup_system_status()
# UPDATE UI
if not self._cards_created:
self._create_cards()
self._cards_created = True
def _setup_system_status(self):
"""Monitor SystemChecks state from GTK Main thread"""
self._update_system_status()
GLib.timeout_add_seconds(
1,
self._update_system_status
)
def _update_system_status(self):
"""Update Status display and create cards when system is ready"""
network = self.system_checks.network_online
ziti = self.system_checks.ziti_online
if network is None:
self._update_label(
"net_status",
"Net: Connected" if statuses.get("net") else "Net: Disconnected",
statuses.get("net"),
"Net: Checking",
None,
)
else:
self._update_label(
"net_status",
"Net: Connected" if network else "Net: Disconnected",
network,
)
if ziti is None:
self._update_label(
"ziti_status",
"Ziti: Connected" if statuses.get("ziti") else "Ziti: Disconnected",
statuses.get("ziti")
"Ziti: Checking",
None,
)
except Exception as e:
print(f"Error polling status: {e}")
import traceback
traceback.print_exc()
else :
self._update_label(
"ziti_status",
"Ziti: Connected" if ziti else "Ziti: Disconnected",
ziti,
)
if self.system_checks.ready and not self._services_visible:
self.builder.get_object("startup_revealer").set_reveal_child(False)
self.builder.get_object("cards_revealer").set_reveal_child(True)
self._services_visible = True
return True
poll()
GLib.timeout_add_seconds(interval_seconds, poll)
def _update_label(self, widget_id, text, is_healthy):
label = self.builder.get_object(widget_id)
@@ -65,6 +96,21 @@ class UIController:
label.remove_css_class("ok-label")
label.add_css_class("error-label")
def _create_cards(self):
"""Create Service Cards"""
for service in self.services:
title = service.get("title", "Service")
desc = service.get("desc", "")
bg_img = service.get("bg_image", "")
self.add_card(
title,
desc,
bg_img,
lambda svc=service: self.service_action_callback(svc),
)
def _load_styles(self, css_file):
css_provider = Gtk.CssProvider()
css_provider.load_from_path(css_file)
@@ -87,7 +133,7 @@ class UIController:
update()
GLib.timeout_add_seconds(10, update)
def _initialist_power_options(self):
def _initialise_power_options(self):
btn_reboot = self.builder.get_object("btn_reboot")
btn_shutdown = self.builder.get_object("btn_shutdown")

View File

@@ -49,11 +49,24 @@
</object>
</child>
<!-- Middle: Cards Grid/Scroller -->
<!-- Middle: Status / Cards -->
<child>
<object class="GtkScrolledWindow">
<object class="GtkOverlay" id="content_area">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<!-- Cards -->
<child>
<object class="GtkRevealer" id="cards_revealer">
<property name="transition-type">crossfade</property>
<property name="transition-duration">500</property>
<property name="reveal-child">false</property>
<child>
<object class="GtkScrolledWindow" id="cards_scroller">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkFlowBox" id="cards_flow">
<property name="halign">3</property>
@@ -68,6 +81,55 @@
</object>
</child>
</object>
</child>
<!-- Startup message -->
<child type="overlay">
<object class="GtkRevealer" id="startup_revealer">
<property name="transition-type">crossfade</property>
<property name="transition-duration">500</property>
<property name="reveal-child">true</property>
<property name="halign">3</property>
<property name="valign">3</property>
<child>
<object class="GtkBox" id="startup_message">
<style>
<class name="startup-message"/>
</style>
<property name="orientation">1</property>
<property name="spacing">8</property>
<property name="halign">3</property>
<property name="valign">3</property>
<child>
<object class="GtkLabel">
<property name="label">Connecting...</property>
<style>
<class name="startup-message-title"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Waiting for network connectivity before enabling services.</property>
<property name="justify">2</property>
<style>
<class name="startup-message-description"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<!-- Bottom: Status Bar -->
<child>
<object class="GtkBox" id="status_bar">