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 import shutil
class ActionDispatcher: 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}") print(f"Executing action -> Type: {service_type}")
if service_type == "browser": if service_type == "browser":

View File

@@ -1,24 +1,74 @@
# checks.py # checks.py
import socket import socket
import threading
import urllib.request import urllib.request
import urllib.error import urllib.error
class SystemChecks: class SystemChecks:
def __init__(self, network_targets, ziti_target): def __init__(self, network_targets, ziti_target):
self.network_targets = network_targets self.network_targets = network_targets
self.ziti_target = ziti_target 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): 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: for host, port in self.network_targets:
if self._tcp_ping(host, int(port), timeout=1.5): if self._tcp_ping(host, int(port), timeout=1.5):
return True return True
return False return False
def check_ziti_status(self): def check_ziti_status(self):
"""Makes an HTTP request to the configured address.""" """Makes HTTP request to Ziti endpoint"""
if not self.ziti_target: if not self.ziti_target:
return False return False

View File

@@ -6,7 +6,7 @@ DEFAULT_CONFIG = {
"app_title": "FJLA Gateway", "app_title": "FJLA Gateway",
"checks": { "checks": {
"network_targets": [["1.1.1.1", 53], ["8.8.8.8", 53]], "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": [ "services": [
{ {

View File

@@ -15,7 +15,9 @@ class LauncherApp(Gtk.Application):
super().__init__(application_id="com.local.Launcher") super().__init__(application_id="com.local.Launcher")
self.config = load_launcher_config() self.config = load_launcher_config()
check_cfg = self.config.get("checks", {}) check_cfg = self.config.get("checks", {})
self.checks = SystemChecks( self.checks = SystemChecks(
network_targets=check_cfg.get("network_targets"), network_targets=check_cfg.get("network_targets"),
ziti_target=check_cfg.get("ziti_target"), ziti_target=check_cfg.get("ziti_target"),
@@ -23,35 +25,24 @@ class LauncherApp(Gtk.Application):
self.dispatcher = ActionDispatcher() self.dispatcher = ActionDispatcher()
self.ui = UIController( self.ui = UIController(
status_provider_callback=lambda: { system_checks=self.checks,
"net": self.checks.check_network_status(),
"ziti": self.checks.check_ziti_status(),
},
power_action_callback=self.dispatcher.dispatch, power_action_callback=self.dispatcher.dispatch,
service_action_callback=self.dispatcher.dispatch,
services=self.config.get("services", []),
) )
def do_activate(self): def do_activate(self):
self.checks.start()
self.ui.set_app_title(self.config.get("app_title", "Launcher")) 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.setup_exit_shortcut(self.quit)
self.ui.present(self) self.ui.present(self)
def do_shutdown(self):
self.checks.stop()
Gtk.Application.do_shutdown(self)
if __name__ == "__main__": if __name__ == "__main__":
app = LauncherApp() app = LauncherApp()

View File

@@ -5,6 +5,21 @@ window {
font-family: sans-serif; 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 */ /* Large, prominent clock */
label.date-display { label.date-display {
font-size: 28px; font-size: 28px;

View File

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

View File

@@ -49,11 +49,24 @@
</object> </object>
</child> </child>
<!-- Middle: Cards Grid/Scroller --> <!-- Middle: Status / Cards -->
<child>
<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> <child>
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow" id="cards_scroller">
<property name="vexpand">true</property> <property name="vexpand">true</property>
<property name="hexpand">true</property> <property name="hexpand">true</property>
<child> <child>
<object class="GtkFlowBox" id="cards_flow"> <object class="GtkFlowBox" id="cards_flow">
<property name="halign">3</property> <property name="halign">3</property>
@@ -68,6 +81,55 @@
</object> </object>
</child> </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 --> <!-- Bottom: Status Bar -->
<child> <child>
<object class="GtkBox" id="status_bar"> <object class="GtkBox" id="status_bar">