From 7a8c4ded602bcd6b187d70cb59af153fee5c482d Mon Sep 17 00:00:00 2001 From: JOhn Date: Wed, 1 Jul 2026 11:01:26 -0400 Subject: [PATCH] update: navigator and main updates --- gui/__pycache__/main_ui.cpython-312.pyc | Bin 51946 -> 43473 bytes gui/main_ui.py | 210 +++-------------- gui/v2/actions/flags.py | 7 + gui/v2/actions/settings_data.py | 138 +++++++++++ gui/v2/infrastructure/navigator.py | 170 ++++++++++++-- gui/v2/infrastructure/orm.py | 0 gui/v2/ui/pages/Page.py | 6 + gui/v2/ui/pages/settings_page.py | 288 +++-------------------- gui/v2/ui/popups/Database_version.py | 290 ------------------------ gui/v2/ui/popups/pick_folder_to_move.py | 50 ---- gui/v2/ui/styles/styles.py | 36 +++ 11 files changed, 411 insertions(+), 784 deletions(-) create mode 100755 gui/v2/actions/settings_data.py mode change 100644 => 100755 gui/v2/infrastructure/orm.py delete mode 100644 gui/v2/ui/popups/Database_version.py delete mode 100644 gui/v2/ui/popups/pick_folder_to_move.py diff --git a/gui/__pycache__/main_ui.cpython-312.pyc b/gui/__pycache__/main_ui.cpython-312.pyc index 273b5de27062afeb469b337aedfd0f918188778f..39261e116c8cfa3bc6ba2fba3af24bf027979858 100644 GIT binary patch delta 10225 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 delta 17209 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 U){QUyh{5j skipping regular-flow pages: " - f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True) - print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True) - print(f"[preload] ============================================================", flush=True) - self._preload_timer.start(700) - - def _preload_next(self): - while self._preload_queue and self._preload_queue[0] in self.navigator._instances: - skipped = self._preload_queue.pop(0) - self._preload_skipped_runtime += 1 - print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True) - if not self._preload_queue: - self._maybe_emit_preload_summary() - return - self._preload_dispatched += 1 - idx = self._preload_dispatched - name = self._preload_queue.pop(0) - print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True) - if name in ASYNC_PREPARE: - self._dispatch_async_preload(name) - else: - ok = self.navigator.preload(name) - if ok: - self._preload_done += 1 - else: - self._preload_failed += 1 - if self._preload_queue: - self._preload_timer.start(250) - else: - self._maybe_emit_preload_summary() - - def _dispatch_async_preload(self, name): - module_path, class_name = PAGE_REGISTRY[name] - print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True) - worker = PageDataWorker(name, module_path, class_name, self) - worker.data_ready.connect(self._on_preload_data_ready) - worker.failed.connect(self._on_preload_failed) - worker.finished.connect(lambda n=name: self._cleanup_preload_worker(n)) - self._preload_workers[name] = worker - self._preload_inflight += 1 - worker.start() - - def _on_preload_data_ready(self, name, payload): - self._preload_inflight -= 1 - if name in self.navigator._instances: - print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True) - else: - try: - page = self.navigator._instantiate(name, prepared=payload) - self.navigator.register_instance(name, page) - self._preload_done += 1 - print(f"[preload] {name:30s} OK (async, cached={len(self.navigator._instances)}, " - f"stack_size={self.page_stack.count()})", flush=True) - except Exception as e: - self._preload_failed += 1 - print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True) - self._maybe_emit_preload_summary() - - def _on_preload_failed(self, name, error): - self._preload_inflight -= 1 - print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True) - if name not in self.navigator._instances: - ok = self.navigator.preload(name) - if ok: - self._preload_done += 1 - else: - self._preload_failed += 1 - self._maybe_emit_preload_summary() - - def _cleanup_preload_worker(self, name): - worker = self._preload_workers.pop(name, None) - if worker is not None: - worker.deleteLater() - - def _maybe_emit_preload_summary(self): - if self._preload_summary_emitted: - return - if not self._preload_queue and self._preload_inflight == 0: - self._preload_summary_emitted = True - self._emit_preload_summary() - - def _emit_preload_summary(self): - import time - total_s = time.perf_counter() - self._preload_t_start - print(f"[preload] ============================================================", flush=True) - print(f"[preload] complete: {self._preload_done} loaded, " - f"{self._preload_failed} failed, " - f"{self._preload_skipped_runtime} skipped (user beat us to it) " - f"in {total_s:.2f}s", flush=True) - print(f"[preload] cached pages now: {sorted(self.navigator._instances.keys())}", flush=True) - print(f"[preload] ============================================================", flush=True) + self.navigator.start_preload() def perform_update_check(self): from core.controllers.ClientController import ClientController update_available = ClientController.can_be_updated() if update_available: - menu_page = self.navigator.get_cached("menu") - if menu_page is not None: - menu_page.on_update_check_finished() + self.navigator.update_if_cached( + "menu", lambda p: p.on_update_check_finished()) def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers): if not status: @@ -363,34 +224,27 @@ class CustomWindow(QMainWindow): for i, brw in enumerate(available_browsers) ] - browser_page = self.navigator.get_cached("browser") - if browser_page is not None: - browser_page.create_interface_elements(available_browsers_list) + self.navigator.update_if_cached( + "browser", lambda p: p.create_interface_elements(available_browsers_list)) + self.navigator.update_if_cached( + "location", lambda p: p.create_interface_elements(available_locations_list)) + self.navigator.update_if_cached( + "hidetor", lambda p: p.create_interface_elements(available_locations_list)) + self.navigator.update_if_cached( + "protocol", lambda p: p.enable_protocol_buttons()) + self.navigator.update_if_cached("menu", self._enable_menu_verification_icons) - location_page = self.navigator.get_cached("location") - if location_page is not None: - location_page.create_interface_elements(available_locations_list) - - hidetor_page = self.navigator.get_cached("hidetor") - if hidetor_page is not None: - hidetor_page.create_interface_elements(available_locations_list) - - protocol_page = self.navigator.get_cached("protocol") - if protocol_page is not None: - protocol_page.enable_protocol_buttons() - - menu_page = self.navigator.get_cached("menu") - if menu_page is not None: - if hasattr(menu_page, 'refresh_profiles_data'): - menu_page.refresh_profiles_data() - if hasattr(menu_page, 'buttons'): - for button in menu_page.buttons: - parent = button.parent() - if parent: - verification_icons = parent.findChildren(QPushButton) - for icon in verification_icons: - if icon.geometry().width() == 20 and icon.geometry().height() == 20: - icon.setEnabled(True) + def _enable_menu_verification_icons(self, menu_page): + if hasattr(menu_page, 'refresh_profiles_data'): + menu_page.refresh_profiles_data() + if hasattr(menu_page, 'buttons'): + for button in menu_page.buttons: + parent = button.parent() + if parent: + verification_icons = parent.findChildren(QPushButton) + for icon in verification_icons: + if icon.geometry().width() == 20 and icon.geometry().height() == 20: + icon.setEnabled(True) def sync(self): core_logger.info("User clicked sync button") @@ -803,9 +657,9 @@ class CustomWindow(QMainWindow): if not self.has_shown_fast_mode_prompt(): self.navigator.navigate("fast_mode_prompt") else: - menu_page = self.navigator.get_cached("menu") - if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'): - menu_page.refresh_menu_buttons() + self.navigator.update_if_cached( + "menu", + lambda p: p.refresh_menu_buttons() if hasattr(p, 'refresh_menu_buttons') else None) self.navigator.navigate("menu") def enable_marquee(self, text): diff --git a/gui/v2/actions/flags.py b/gui/v2/actions/flags.py index af16022..317728e 100755 --- a/gui/v2/actions/flags.py +++ b/gui/v2/actions/flags.py @@ -42,6 +42,13 @@ def is_fast_registration_enabled(gui_config_file): return config.get("registrations", {}).get("fast_registration_enabled", False) +def is_auto_sync_enabled(gui_config_file): + config = load_gui_config(gui_config_file) + if not config: + return False + return config.get("registrations", {}).get("auto_sync_enabled", False) + + def set_fast_mode_enabled(gui_config_file, enabled): config = load_gui_config(gui_config_file) if config is None: diff --git a/gui/v2/actions/settings_data.py b/gui/v2/actions/settings_data.py new file mode 100755 index 0000000..e190e2a --- /dev/null +++ b/gui/v2/actions/settings_data.py @@ -0,0 +1,138 @@ +import json +import os + +from core.Constants import Constants +from core.controllers.ConfigurationController import ConfigurationController +from core.controllers.PolicyController import PolicyController +from core.controllers.ProfileController import ProfileController + +from gui.v2.actions.config import load_gui_config +from gui.v2.actions.profile_order import normalize_profile_order +from gui.v2.actions.ticket_failure import get_ticket_verification_failure + + +def read_systemwide_enabled(): + try: + privilege_policy = PolicyController.get('privilege') + if privilege_policy is not None: + return PolicyController.is_instated(privilege_policy) + except Exception: + pass + return False + + +def read_bwrap_enabled(): + try: + capability_policy = PolicyController.get('capability') + if capability_policy is not None: + return PolicyController.is_instated(capability_policy) + except Exception: + pass + return False + + +def prepare(gui_config_file): + profiles = ProfileController.get_all() + profile_order = normalize_profile_order(gui_config_file, profiles.keys()) + try: + endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled() + except Exception: + endpoint_verification_enabled = False + return { + "profiles": profiles, + "profile_order": profile_order, + "current_connection": ConfigurationController.get_connection(), + "endpoint_verification_enabled": endpoint_verification_enabled, + "systemwide_enabled": read_systemwide_enabled(), + "bwrap_enabled": read_bwrap_enabled(), + "gui_config": load_gui_config(gui_config_file), + "ticket_failure": get_ticket_verification_failure(gui_config_file), + } + + +def empty_payload(): + return { + "profiles": {}, + "profile_order": [], + "current_connection": None, + "endpoint_verification_enabled": False, + "systemwide_enabled": False, + "bwrap_enabled": False, + "gui_config": None, + "ticket_failure": None, + } + + +def extract_endpoint_ip(profile): + try: + profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile.id}' + wg_conf_path = f'{profile_path}/wg.conf.bak' + if not os.path.exists(wg_conf_path): + return None + with open(wg_conf_path, 'r') as f: + content = f.read() + for line in content.split('\n'): + if line.strip().startswith('Endpoint = '): + endpoint = line.strip().split(' = ')[1] + return endpoint.split(':')[0] + return None + except Exception: + return None + + +def truncate_key(text, max_length=50): + if not text or text == "N/A" or len(text) <= max_length: + return text + start_len = max_length // 2 - 2 + end_len = max_length // 2 - 2 + return text[:start_len] + "....." + text[-end_len:] + + +def build_verification_view(profile): + operator = None + if profile and getattr(profile, 'location', None) and profile.location.operator: + operator = profile.location.operator + if not operator: + return { + "operator_name": "N/A", + "nostr_public_key": "N/A", + "hydraveil_public_key": "N/A", + "nostr_attestation_event_reference": "N/A", + } + return { + "operator_name": operator.name or "N/A", + "nostr_public_key": operator.nostr_public_key or "N/A", + "hydraveil_public_key": operator.public_key or "N/A", + "nostr_attestation_event_reference": operator.nostr_attestation_event_reference or "N/A", + } + + +def build_subscription_view(profile): + subscription = profile.subscription + billing_code = str(subscription.billing_code) + if hasattr(subscription, 'expires_at') and subscription.expires_at: + expires_at = subscription.expires_at.strftime("%Y-%m-%d %H:%M:%S UTC") + else: + expires_at = "Not available" + return {"billing_code": billing_code, "expires_at": expires_at} + + +def format_ticket_failure_status(failure): + if not failure: + return "No saved verification failure. If ticket preparation fails validation, recovery data will appear here." + failed_validations = failure.get("failed_validations", []) + how_many_failed = failure.get("how_many_failed", len(failed_validations)) + updated_at = failure.get("updated_at", "unknown time") + failed_text = ", ".join(str(item) for item in failed_validations) + return ( + f"Saved verification failure: {how_many_failed} failed. " + f"Failed validation indices: {failed_text}. Saved: {updated_at}" + ) + + +def format_ticket_recovery_result(label, result): + try: + payload = json.dumps(result, indent=2, default=str) + except TypeError: + payload = str(result) + return f"{label}:\n{payload}" diff --git a/gui/v2/infrastructure/navigator.py b/gui/v2/infrastructure/navigator.py index 4c54a8a..1499649 100755 --- a/gui/v2/infrastructure/navigator.py +++ b/gui/v2/infrastructure/navigator.py @@ -2,15 +2,40 @@ import importlib import time import traceback -from gui.v2.infrastructure.page_registry import PAGE_REGISTRY, ASYNC_PREPARE +from PyQt6.QtCore import QObject, QTimer + +from gui.v2.infrastructure.page_registry import ( + PAGE_REGISTRY, + ASYNC_PREPARE, + REGULAR_FLOW_ONLY, + PRELOAD_ORDER, + PRELOAD_SKIP, +) +from gui.v2.workers.page_data_worker import PageDataWorker +from gui.v2.actions.flags import is_fast_registration_enabled -class Navigator: - def __init__(self, page_stack, custom_window): - self.page_stack = page_stack +class Navigator(QObject): + def __init__(self, custom_window): + super().__init__() self.custom_window = custom_window + self.page_stack = custom_window.page_stack self._instances = {} + self._preload_queue = [] + self._preload_total = 0 + self._preload_done = 0 + self._preload_failed = 0 + self._preload_skipped_runtime = 0 + self._preload_dispatched = 0 + self._preload_inflight = 0 + self._preload_workers = {} + self._preload_summary_emitted = False + self._preload_t_start = 0.0 + self._preload_timer = QTimer(self) + self._preload_timer.setSingleShot(True) + self._preload_timer.timeout.connect(self._preload_next) + def get_class(self, name): module_path, class_name = PAGE_REGISTRY[name] module = importlib.import_module(module_path) @@ -31,12 +56,28 @@ class Navigator: if page is None: if name not in PAGE_REGISTRY: self._warn_not_migrated(name) - return + return None page = self._instantiate(name) self.register_instance(name, page) self.page_stack.setCurrentIndex(self.page_stack.indexOf(page)) + return page - def preload(self, name): + def get_cached(self, name): + return self._instances.get(name) + + def update_if_cached(self, name, fn): + page = self._instances.get(name) + if page is not None: + fn(page) + return page + + def _warn_not_migrated(self, name): + try: + self.custom_window.update_status(f"Page '{name}' not migrated yet.") + except Exception: + pass + + def _preload_one(self, name): if name in self._instances: print(f"[preload] {name:30s} SKIP (already cached)", flush=True) return False @@ -64,11 +105,114 @@ class Navigator: f"{type(e).__name__}: {e}") return False - def get_cached(self, name): - return self._instances.get(name) + def start_preload(self, fast_mode=None): + if fast_mode is None: + fast_mode = is_fast_registration_enabled(self.custom_window.gui_config_file) + self._preload_queue = [n for n in PRELOAD_ORDER if n not in PRELOAD_SKIP] + skipped_regular = [] + if fast_mode: + skipped_regular = [n for n in self._preload_queue if n in REGULAR_FLOW_ONLY] + self._preload_queue = [n for n in self._preload_queue if n not in REGULAR_FLOW_ONLY] + self._preload_total = len(self._preload_queue) + self._preload_done = 0 + self._preload_failed = 0 + self._preload_skipped_runtime = 0 + self._preload_dispatched = 0 + self._preload_inflight = 0 + self._preload_summary_emitted = False + self._preload_t_start = time.perf_counter() + print(f"[preload] ============================================================", flush=True) + print(f"[preload] starting background preload: {self._preload_total} pages queued", flush=True) + print(f"[preload] order : {', '.join(self._preload_queue)}", flush=True) + print(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)", flush=True) + if fast_mode: + print(f"[preload] fastmode: ON -> skipping regular-flow pages: " + f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True) + print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True) + print(f"[preload] ============================================================", flush=True) + self._preload_timer.start(700) - def _warn_not_migrated(self, name): - try: - self.custom_window.update_status(f"Page '{name}' not migrated yet.") - except Exception: - pass + def _preload_next(self): + while self._preload_queue and self._preload_queue[0] in self._instances: + skipped = self._preload_queue.pop(0) + self._preload_skipped_runtime += 1 + print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True) + if not self._preload_queue: + self._maybe_emit_preload_summary() + return + self._preload_dispatched += 1 + idx = self._preload_dispatched + name = self._preload_queue.pop(0) + print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True) + if name in ASYNC_PREPARE: + self._dispatch_async_preload(name) + else: + ok = self._preload_one(name) + if ok: + self._preload_done += 1 + else: + self._preload_failed += 1 + if self._preload_queue: + self._preload_timer.start(250) + else: + self._maybe_emit_preload_summary() + + def _dispatch_async_preload(self, name): + module_path, class_name = PAGE_REGISTRY[name] + print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True) + worker = PageDataWorker(name, module_path, class_name, self.custom_window) + worker.data_ready.connect(self._on_preload_data_ready) + worker.failed.connect(self._on_preload_failed) + worker.finished.connect(lambda n=name: self._cleanup_preload_worker(n)) + self._preload_workers[name] = worker + self._preload_inflight += 1 + worker.start() + + def _on_preload_data_ready(self, name, payload): + self._preload_inflight -= 1 + if name in self._instances: + print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True) + else: + try: + page = self._instantiate(name, prepared=payload) + self.register_instance(name, page) + self._preload_done += 1 + print(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, " + f"stack_size={self.page_stack.count()})", flush=True) + except Exception as e: + self._preload_failed += 1 + print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True) + self._maybe_emit_preload_summary() + + def _on_preload_failed(self, name, error): + self._preload_inflight -= 1 + print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True) + if name not in self._instances: + ok = self._preload_one(name) + if ok: + self._preload_done += 1 + else: + self._preload_failed += 1 + self._maybe_emit_preload_summary() + + def _cleanup_preload_worker(self, name): + worker = self._preload_workers.pop(name, None) + if worker is not None: + worker.deleteLater() + + def _maybe_emit_preload_summary(self): + if self._preload_summary_emitted: + return + if not self._preload_queue and self._preload_inflight == 0: + self._preload_summary_emitted = True + self._emit_preload_summary() + + def _emit_preload_summary(self): + total_s = time.perf_counter() - self._preload_t_start + print(f"[preload] ============================================================", flush=True) + print(f"[preload] complete: {self._preload_done} loaded, " + f"{self._preload_failed} failed, " + f"{self._preload_skipped_runtime} skipped (user beat us to it) " + f"in {total_s:.2f}s", flush=True) + print(f"[preload] cached pages now: {sorted(self._instances.keys())}", flush=True) + print(f"[preload] ============================================================", flush=True) diff --git a/gui/v2/infrastructure/orm.py b/gui/v2/infrastructure/orm.py old mode 100644 new mode 100755 diff --git a/gui/v2/ui/pages/Page.py b/gui/v2/ui/pages/Page.py index d6b673c..daee6a0 100755 --- a/gui/v2/ui/pages/Page.py +++ b/gui/v2/ui/pages/Page.py @@ -13,6 +13,7 @@ class Page(QWidget): self.btn_path = custom_window.btn_path self.name = name self.page_stack = page_stack + self._prepared = None self.init_ui() self.selected_profiles = [] self.selected_wireguard = [] @@ -23,6 +24,11 @@ class Page(QWidget): def prepare_data(custom_window): return None + def _prepared_value(self, key, loader): + if self._prepared is not None and key in self._prepared: + return self._prepared[key] + return loader() + def add_selected_profile(self, profile): self.selected_profiles.clear() self.selected_wireguard.clear() diff --git a/gui/v2/ui/pages/settings_page.py b/gui/v2/ui/pages/settings_page.py index e5ab437..a4af5e8 100755 --- a/gui/v2/ui/pages/settings_page.py +++ b/gui/v2/ui/pages/settings_page.py @@ -1,11 +1,9 @@ -import json import logging import os import subprocess import sys import threading from datetime import datetime, timezone -from typing import Union from PyQt6.QtWidgets import ( QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame, QGridLayout, @@ -34,6 +32,7 @@ from core.Errors import ( ) from core.errors.logger import logger as core_logger +from gui.v2.actions import settings_data from gui.v2.actions.key_interpretation import interpret_key_results from gui.v2.actions.profile_order import normalize_profile_order from gui.v2.infrastructure.page_registry import PAGE_REGISTRY @@ -44,6 +43,7 @@ from gui.v2.ui.styles.styles import ( SCROLLBAR_CYAN_QSS, TERMINAL_LIST_QSS, checkbox_style, + combobox_style, ) from gui.v2.ui.widgets.clickable_label import ClickableValueLabel from gui.v2.ui.widgets.terminal_widget import TerminalWidget @@ -59,7 +59,7 @@ class Settings(Page): self.update_status = main_window self.update_logging = main_window - self._prepared = prepared if prepared is not None else self.prepare_data(main_window) + self._prepared = prepared if prepared is not None else settings_data.empty_payload() self._settings_refresh_seq = 0 self._settings_workers = set() self.button_reverse.setVisible(True) @@ -71,49 +71,7 @@ class Settings(Page): @staticmethod def prepare_data(custom_window): - profiles = ProfileController.get_all() - profile_order = normalize_profile_order( - getattr(custom_window, 'gui_config_file', None), profiles.keys()) - current_connection = custom_window.get_current_connection() - try: - endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled() - except Exception: - endpoint_verification_enabled = False - return { - "profiles": profiles, - "profile_order": profile_order, - "current_connection": current_connection, - "endpoint_verification_enabled": endpoint_verification_enabled, - "systemwide_enabled": Settings._read_systemwide_enabled(), - "bwrap_enabled": Settings._read_bwrap_enabled(), - "gui_config": custom_window._load_gui_config(), - "ticket_failure": custom_window.get_ticket_verification_failure(), - } - - def _prepared_value(self, key, loader): - if self._prepared is not None and key in self._prepared: - return self._prepared[key] - return loader() - - @staticmethod - def _read_systemwide_enabled(): - try: - privilege_policy = PolicyController.get('privilege') - if privilege_policy is not None: - return PolicyController.is_instated(privilege_policy) - except Exception: - pass - return False - - @staticmethod - def _read_bwrap_enabled(): - try: - capability_policy = PolicyController.get('capability') - if capability_policy is not None: - return PolicyController.is_instated(capability_policy) - except Exception: - pass - return False + return settings_data.prepare(custom_window.gui_config_file) def setup_ui(self): main_container = QWidget(self) @@ -344,7 +302,7 @@ class Settings(Page): profile_selection_layout.addWidget(profile_label) self.debug_profile_selector = QComboBox() - self.debug_profile_selector.setStyleSheet(self.get_combobox_style()) + self.debug_profile_selector.setStyleSheet(combobox_style(self.font_style)) self.debug_profile_selector.currentTextChanged.connect( self.on_debug_profile_selected) profile_selection_layout.addWidget(self.debug_profile_selector) @@ -690,7 +648,7 @@ class Settings(Page): self.cli_command.setText( f"'{app_path}' --cli profile enable -i {profile_id}") self.cli_copy_button.setEnabled(True) - ip_address = self.extract_endpoint_ip(profile) + ip_address = settings_data.extract_endpoint_ip(profile) if ip_address: self.ping_instruction_label.setText( f"Step 1, Can you Ping it? Copy-paste the command below into your terminal. This VPN Node's IP address is {ip_address}") @@ -722,25 +680,6 @@ class Settings(Page): self.wg_quick_up_button.setEnabled(False) self.wg_quick_down_button.setEnabled(False) - def extract_endpoint_ip(self, profile): - try: - profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile.id}' - wg_conf_path = f'{profile_path}/wg.conf.bak' - if not os.path.exists(wg_conf_path): - return None - - with open(wg_conf_path, 'r') as f: - content = f.read() - - for line in content.split('\n'): - if line.strip().startswith('Endpoint = '): - endpoint = line.strip().split(' = ')[1] - ip_address = endpoint.split(':')[0] - return ip_address - return None - except Exception: - return None - def copy_ping_command(self): profile_id = self.debug_profile_selector.currentData() if profile_id is None: @@ -748,7 +687,7 @@ class Settings(Page): profile = ProfileController.get(profile_id) if profile and isinstance(profile, SystemProfile): - ip_address = self.extract_endpoint_ip(profile) + ip_address = settings_data.extract_endpoint_ip(profile) if ip_address: ping_command = f"ping {ip_address}" clipboard = QApplication.clipboard() @@ -763,7 +702,7 @@ class Settings(Page): profile = ProfileController.get(profile_id) if profile and isinstance(profile, SystemProfile): - ip_address = self.extract_endpoint_ip(profile) + ip_address = settings_data.extract_endpoint_ip(profile) if ip_address: self.test_ping_button.setEnabled(False) self.ping_result_label.setText("Testing ping...") @@ -853,75 +792,9 @@ class Settings(Page): self.content_layout.addWidget(self.delete_page) self.content_layout.setCurrentWidget(self.delete_page) - def get_combobox_style(self) -> str: - return f""" - QComboBox {{ - color: black; - background: #f0f0f0; - padding: 5px 30px 5px 10px; - border: 1px solid #ccc; - border-radius: 4px; - min-width: 120px; - margin-bottom: 10px; - {self.font_style} - }} - QComboBox:disabled {{ - color: #666; - background: #e0e0e0; - }} - QComboBox::drop-down {{ - border: none; - width: 30px; - }} - QComboBox::down-arrow {{ - image: url(assets/down_arrow.png); - width: 12px; - height: 12px; - }} - QComboBox QAbstractItemView {{ - color: black; - background: white; - selection-background-color: #007bff; - selection-color: white; - border: 1px solid #ccc; - {self.font_style} - }} - """ - def get_checkbox_style(self) -> str: return checkbox_style(self.font_style) - def populate_wireguard_profiles(self) -> None: - self.wireguard_profile_selector.clear() - profiles = ProfileController.get_all() - for profile_id, profile in profiles.items(): - if profile.connection.code == 'wireguard': - - self.wireguard_profile_selector.addItem( - f"Profile {profile_id}: {profile.name}", profile_id) - - def convert_duration(self, value: Union[str, int], to_hours: bool = True) -> Union[str, int]: - if to_hours: - number, unit = value.split(' ') - number = int(number) - if unit in ['day', 'days']: - return number * 24 - elif unit in ['week', 'weeks']: - return number * 7 * 24 - else: - raise ValueError(f"Unsupported duration unit: {unit}") - else: - hours = int(value) - if hours % (7 * 24) == 0: - weeks = hours // (7 * 24) - return f"{weeks} {'week' if weeks == 1 else 'weeks'}" - elif hours % 24 == 0: - days = hours // 24 - return f"{days} {'day' if days == 1 else 'days'}" - else: - raise ValueError( - f"Hours value {hours} cannot be converted to days or weeks cleanly") - def reverse(self): self.custom_window.navigator.navigate("menu") @@ -956,7 +829,7 @@ class Settings(Page): profile_layout = QVBoxLayout(profile_group) self.profile_selector = QComboBox() - self.profile_selector.setStyleSheet(self.get_combobox_style()) + self.profile_selector.setStyleSheet(combobox_style(self.font_style)) profiles = self._prepared_value("profiles", ProfileController.get_all) if profiles: for profile_id, profile in profiles.items(): @@ -1091,7 +964,7 @@ class Settings(Page): self.verification_profile_selector = QComboBox() self.verification_profile_selector.setStyleSheet( - self.get_combobox_style()) + combobox_style(self.font_style)) profiles = self._prepared_value("profiles", ProfileController.get_all) if profiles: for profile_id, profile in profiles.items(): @@ -1210,72 +1083,30 @@ class Settings(Page): return page - def truncate_key(self, text, max_length=50): - if not text or text == "N/A" or len(text) <= max_length: - return text - start_len = max_length // 2 - 2 - end_len = max_length // 2 - 2 - return text[:start_len] + "....." + text[-end_len:] - def update_verification_info(self, index): if index < 0: return profile_id = self.verification_profile_selector.itemData(index) - if profile_id is None: - for key, widget in self.verification_info.items(): - widget.setText("N/A") - self.verification_full_values[key] = "N/A" - if key in self.verification_checkmarks: - self.verification_checkmarks[key].hide() - return + profile = None + if profile_id is not None: + profile = ProfileController.get(profile_id) - profile = ProfileController.get(profile_id) - if not profile: - for key, widget in self.verification_info.items(): - widget.setText("N/A") - self.verification_full_values[key] = "N/A" - if key in self.verification_checkmarks: - self.verification_checkmarks[key].hide() - return - - operator = None - if profile.location and profile.location.operator: - operator = profile.location.operator - - if operator: - operator_name = operator.name or "N/A" - self.verification_full_values["operator_name"] = operator_name - self.verification_info["operator_name"].setText(operator_name) - - nostr_key = operator.nostr_public_key or "N/A" - self.verification_full_values["nostr_public_key"] = nostr_key - self.verification_info["nostr_public_key"].setText( - self.truncate_key(nostr_key) if nostr_key != "N/A" else nostr_key) - - hydraveil_key = operator.public_key or "N/A" - self.verification_full_values["hydraveil_public_key"] = hydraveil_key - self.verification_info["hydraveil_public_key"].setText( - self.truncate_key(hydraveil_key) if hydraveil_key != "N/A" else hydraveil_key) - - nostr_verification = operator.nostr_attestation_event_reference or "N/A" - self.verification_full_values["nostr_attestation_event_reference"] = nostr_verification - self.verification_info["nostr_attestation_event_reference"].setText(self.truncate_key( - nostr_verification) if nostr_verification != "N/A" else nostr_verification) - else: - self.verification_info["operator_name"].setText("N/A") - self.verification_full_values["operator_name"] = "N/A" - self.verification_info["nostr_public_key"].setText("N/A") - self.verification_full_values["nostr_public_key"] = "N/A" - self.verification_info["hydraveil_public_key"].setText("N/A") - self.verification_full_values["hydraveil_public_key"] = "N/A" - self.verification_info["nostr_attestation_event_reference"].setText( - "N/A") - self.verification_full_values["nostr_attestation_event_reference"] = "N/A" + view = settings_data.build_verification_view(profile) + truncated_keys = ( + "nostr_public_key", + "hydraveil_public_key", + "nostr_attestation_event_reference", + ) for key, widget in self.verification_info.items(): + full_value = view.get(key, "N/A") + self.verification_full_values[key] = full_value + if key in truncated_keys and full_value != "N/A": + widget.setText(settings_data.truncate_key(full_value)) + else: + widget.setText(full_value) if key in self.verification_checkmarks: - full_value = self.verification_full_values.get(key, "") if full_value and full_value != "N/A": self.verification_checkmarks[key].show() else: @@ -1320,16 +1151,9 @@ class Settings(Page): if profile and hasattr(profile, 'subscription') and profile.subscription: try: - self.subscription_info["billing_code"].setText( - str(profile.subscription.billing_code)) - - if hasattr(profile.subscription, 'expires_at') and profile.subscription.expires_at: - expires_at = profile.subscription.expires_at.strftime( - "%Y-%m-%d %H:%M:%S UTC") - self.subscription_info["expires_at"].setText(expires_at) - else: - self.subscription_info["expires_at"].setText( - "Not available") + view = settings_data.build_subscription_view(profile) + self.subscription_info["billing_code"].setText(view["billing_code"]) + self.subscription_info["expires_at"].setText(view["expires_at"]) except Exception as e: print(f"Error updating subscription info: {e}") else: @@ -1787,7 +1611,7 @@ class Settings(Page): f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") recovery_layout = QVBoxLayout(recovery_group) - self.ticket_recovery_status_label = QLabel(self._format_ticket_failure_status(saved_failure)) + self.ticket_recovery_status_label = QLabel(settings_data.format_ticket_failure_status(saved_failure)) self.ticket_recovery_status_label.setStyleSheet( f"color: white; font-size: 12px; {self.font_style}") self.ticket_recovery_status_label.setWordWrap(True) @@ -1841,23 +1665,11 @@ class Settings(Page): page_layout.addWidget(scroll_area) return page - def _format_ticket_failure_status(self, failure): - if not failure: - return "No saved verification failure. If ticket preparation fails validation, recovery data will appear here." - failed_validations = failure.get("failed_validations", []) - how_many_failed = failure.get("how_many_failed", len(failed_validations)) - updated_at = failure.get("updated_at", "unknown time") - failed_text = ", ".join(str(item) for item in failed_validations) - return ( - f"Saved verification failure: {how_many_failed} failed. " - f"Failed validation indices: {failed_text}. Saved: {updated_at}" - ) - def _refresh_ticket_recovery_controls(self): failure = self.update_status.get_ticket_verification_failure() has_failure = failure is not None if hasattr(self, 'ticket_recovery_status_label'): - self.ticket_recovery_status_label.setText(self._format_ticket_failure_status(failure)) + self.ticket_recovery_status_label.setText(settings_data.format_ticket_failure_status(failure)) if hasattr(self, 'evaluate_public_key_button'): self.evaluate_public_key_button.setEnabled(has_failure) if hasattr(self, 'prepare_saved_blind_sigs_button'): @@ -1873,13 +1685,6 @@ class Settings(Page): if hasattr(self, 'ticket_recovery_output'): self.ticket_recovery_output.setPlainText(text) - def _format_ticket_recovery_result(self, label, result): - try: - payload = json.dumps(result, indent=2, default=str) - except TypeError: - payload = str(result) - return f"{label}:\n{payload}" - def _get_ticket_failure_for_action(self): failure = self.update_status.get_ticket_verification_failure() if failure is None: @@ -1941,7 +1746,7 @@ class Settings(Page): self.ticket_recovery_worker.start() def on_saved_blind_prep_done(self, result): - self._write_ticket_recovery_output(self._format_ticket_recovery_result("Results of preparation", result)) + self._write_ticket_recovery_output(settings_data.format_ticket_recovery_result("Results of preparation", result)) if isinstance(result, dict) and result.get('valid') is True: self.update_status.clear_ticket_verification_failure() self.update_status.update_status("Tickets prepared from saved blind signatures.") @@ -2187,7 +1992,7 @@ class Settings(Page): layout.addStretch() self.load_systemwide_settings( - self._prepared_value("systemwide_enabled", self._read_systemwide_enabled)) + self._prepared_value("systemwide_enabled", settings_data.read_systemwide_enabled)) return page def create_bwrap_page(self): @@ -2240,12 +2045,12 @@ class Settings(Page): layout.addStretch() self.load_bwrap_settings( - self._prepared_value("bwrap_enabled", self._read_bwrap_enabled)) + self._prepared_value("bwrap_enabled", settings_data.read_bwrap_enabled)) return page def load_systemwide_settings(self, enabled=None): if enabled is None: - enabled = self._read_systemwide_enabled() + enabled = settings_data.read_systemwide_enabled() self.systemwide_toggle.setChecked(enabled) if enabled: self.systemwide_status_value.setText("Enabled") @@ -2284,7 +2089,7 @@ class Settings(Page): def load_bwrap_settings(self, enabled=None): if enabled is None: - enabled = self._read_bwrap_enabled() + enabled = settings_data.read_bwrap_enabled() self.bwrap_toggle.setChecked(enabled) if enabled: self.bwrap_status_value.setText("Enabled") @@ -2382,26 +2187,3 @@ class Settings(Page): logging.error(f"Error saving registration settings: {str(e)}") self.update_status.update_status( "Error saving registration settings") - - def is_auto_sync_enabled(self) -> bool: - try: - config = self.update_status._load_gui_config() - if config and "registrations" in config: - registrations = config["registrations"] - return registrations.get("auto_sync_enabled", False) - return False - except Exception as e: - logging.error(f"Error checking auto-sync setting: {str(e)}") - return False - - def is_fast_registration_enabled(self) -> bool: - try: - config = self.update_status._load_gui_config() - if config and "registrations" in config: - registrations = config["registrations"] - return registrations.get("fast_registration_enabled", False) - return False - except Exception as e: - logging.error( - f"Error checking fast registration setting: {str(e)}") - return False diff --git a/gui/v2/ui/popups/Database_version.py b/gui/v2/ui/popups/Database_version.py deleted file mode 100644 index d42c2d6..0000000 --- a/gui/v2/ui/popups/Database_version.py +++ /dev/null @@ -1,290 +0,0 @@ -from core.Constants import Constants - -from PyQt6.QtWidgets import QApplication, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QScrollArea -from PyQt6.QtCore import Qt, QSize -from PyQt6.QtGui import QIcon, QFont, QColor, QPalette, QPixmap, QPainter -import sys - -APP_VERSION = Constants.DB_VERSION_THIS_APP_WANTS - -class DatabaseConflictDialog(QDialog): - WIPE = 1 - MOVE_DB = 2 - EXIT = 3 - - def __init__(self, check: dict, parent=None): - super().__init__(parent) - self.setWindowTitle("Database Recovery") - self.setModal(True) - self.user_choice = self.EXIT - self.setMinimumWidth(550) - self.setMinimumHeight(400) - self.setMaximumWidth(750) - self.setMaximumHeight(600) - - # Modern dark theme - self.setStyleSheet(""" - DatabaseConflictDialog { - background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #0f172a, stop:1 #1e293b); - } - """) - - main_layout = QVBoxLayout() - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - # Header with gradient and icon - header = QFrame() - header.setStyleSheet(""" - QFrame { - background: qlineargradient(x1:0, y1:0, x2:1, y2:0, - stop:0 #3b82f6, stop:1 #1e40af); - border: none; - } - """) - header.setFixedHeight(100) - header_layout = QHBoxLayout() - header_layout.setContentsMargins(30, 20, 30, 20) - - # Warning icon (using unicode) - icon_label = QLabel("⚠️") - icon_label.setFont(QFont("Segoe UI", 48)) - header_layout.addWidget(icon_label, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) - - ######## Title Section. ############## - - # First which messages to display, - if check["status"] == "version_mismatch": - error_title = "Database Compatibility" - error_subtitle = "Your App version and Database don't match" - else: - error_title = "Database Error" - error_subtitle = "The Database can't properly start" - - # then display them: - title_layout = QVBoxLayout() - title_layout.setSpacing(5) - - title = QLabel(error_title) - title.setFont(QFont("Segoe UI", 18, QFont.Weight.Bold)) - title.setStyleSheet("color: white;") - title_layout.addWidget(title) - - subtitle = QLabel(error_subtitle) - subtitle.setFont(QFont("Segoe UI", 11)) - subtitle.setStyleSheet("color: rgba(255, 255, 255, 0.8);") - title_layout.addWidget(subtitle) - - header_layout.addLayout(title_layout, 1) - header.setLayout(header_layout) - main_layout.addWidget(header) - - # Content area with scroll support - content = QFrame() - content.setStyleSheet(""" - QFrame { - background: #1e293b; - border: none; - } - """) - content_layout = QVBoxLayout() - content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.setSpacing(0) - - # Scrollable area for dynamic content - scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) - scroll_area.setStyleSheet(""" - QScrollArea { - background: #1e293b; - border: none; - } - QScrollBar:vertical { - background: #1e293b; - width: 12px; - } - QScrollBar::handle:vertical { - background: #475569; - border-radius: 6px; - min-height: 20px; - } - QScrollBar::handle:vertical:hover { - background: #64748b; - } - QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { - border: none; - } - """) - - # Inner scrollable widget - scroll_widget = QFrame() - scroll_widget.setStyleSheet("background: #1e293b; border: none;") - scroll_layout = QVBoxLayout() - scroll_layout.setContentsMargins(40, 30, 40, 30) - scroll_layout.setSpacing(15) - - # Main message - message = QLabel(check["message"]) - message.setFont(QFont("Segoe UI", 12)) - message.setStyleSheet("color: #e2e8f0; line-height: 1.6;") - message.setWordWrap(True) - scroll_layout.addWidget(message) - - # Version info - if check.get("db_version"): - info_frame = QFrame() - info_frame.setStyleSheet(""" - QFrame { - background: rgba(59, 130, 246, 0.1); - border: 1px solid rgba(59, 130, 246, 0.3); - border-radius: 6px; - } - """) - info_layout = QVBoxLayout() - info_layout.setContentsMargins(15, 12, 15, 12) - info_layout.setSpacing(5) - - app_ver = QLabel(f"App is looking for database version: {APP_VERSION}") - app_ver.setFont(QFont("Segoe UI", 10)) - app_ver.setStyleSheet("color: #94a3b8;") - info_layout.addWidget(app_ver) - - db_ver = QLabel(f"Your Actual Database version: {check['db_version']}") - db_ver.setFont(QFont("Segoe UI", 10)) - db_ver.setStyleSheet("color: #94a3b8;") - info_layout.addWidget(db_ver) - - info_frame.setLayout(info_layout) - scroll_layout.addWidget(info_frame) - - scroll_layout.addStretch() - scroll_widget.setLayout(scroll_layout) - scroll_area.setWidget(scroll_widget) - content_layout.addWidget(scroll_area, 1) - - content.setLayout(content_layout) - main_layout.addWidget(content, 1) - - # Button area - button_area = QFrame() - button_area.setStyleSheet(""" - QFrame { - background: #0f172a; - border-top: 1px solid #334155; - } - """) - button_layout = QVBoxLayout() - button_layout.setContentsMargins(40, 20, 40, 20) - button_layout.setSpacing(12) - - if check["status"] == "version_mismatch": - btn_backup = self._create_button( - "Create a Fresh Database. Then restart (recommended)", - "primary", - "✅" - ) - btn_backup.clicked.connect(self.on_wipe_create) - button_layout.addWidget(btn_backup) - - btn_different = self._create_button( - "Move the Stale Database for Debug Purposes", - "secondary", - "📁" - ) - btn_different.clicked.connect(self.on_move_db) - button_layout.addWidget(btn_different) - - btn_exit = self._create_button( - "Exit App without action.", - "danger", - "❌" - ) - btn_exit.clicked.connect(self.on_exit) - button_layout.addWidget(btn_exit) - - button_area.setLayout(button_layout) - main_layout.addWidget(button_area) - - self.setLayout(main_layout) - - def _create_button(self, text, style_type, icon): - """Create a styled button""" - btn = QPushButton(f" {icon} {text}") - btn.setFont(QFont("Segoe UI", 11, QFont.Weight.Medium)) - btn.setFixedHeight(45) - btn.setCursor(Qt.CursorShape.PointingHandCursor) - - if style_type == "primary": - btn.setStyleSheet(""" - QPushButton { - background: qlineargradient(x1:0, y1:0, x2:1, y2:0, - stop:0 #3b82f6, stop:1 #2563eb); - color: white; - border: none; - border-radius: 6px; - font-weight: 600; - } - QPushButton:hover { - background: qlineargradient(x1:0, y1:0, x2:1, y2:0, - stop:0 #2563eb, stop:1 #1d4ed8); - } - QPushButton:pressed { - background: #1d4ed8; - } - """) - elif style_type == "secondary": - btn.setStyleSheet(""" - QPushButton { - background: #334155; - color: #e2e8f0; - border: 1px solid #475569; - border-radius: 6px; - font-weight: 600; - } - QPushButton:hover { - background: #475569; - border: 1px solid #64748b; - } - QPushButton:pressed { - background: #1e293b; - } - """) - elif style_type == "danger": - btn.setStyleSheet(""" - QPushButton { - background: #64748b; - color: #e2e8f0; - border: 1px solid #475569; - border-radius: 6px; - font-weight: 600; - } - QPushButton:hover { - background: #ef4444; - border: 1px solid #dc2626; - } - QPushButton:pressed { - background: #dc2626; - } - """) - - return btn - - def on_wipe_create(self): - self.user_choice = self.WIPE - self.accept() - - def on_move_db(self): - self.user_choice = self.MOVE_DB - self.accept() - - def on_exit(self): - self.user_choice = self.EXIT - self.reject() - -def show_recovery_dialog(check: dict): - """Show recovery dialog as standalone app and return user choice""" - app = QApplication.instance() or QApplication(sys.argv) - dialog = DatabaseConflictDialog(check) - dialog.exec() - return dialog.user_choice diff --git a/gui/v2/ui/popups/pick_folder_to_move.py b/gui/v2/ui/popups/pick_folder_to_move.py deleted file mode 100644 index daff390..0000000 --- a/gui/v2/ui/popups/pick_folder_to_move.py +++ /dev/null @@ -1,50 +0,0 @@ -import shutil -import sys -from pathlib import Path -from PyQt6.QtWidgets import QApplication, QFileDialog - -def move_database_file(original_filepath: str) -> bool: - """ - Prompts the user to select a destination for a .db file and moves it there. - - Args: - original_filepath: The current path to the .db file - - Returns: - True if the move was successful, False otherwise - """ - # Create a QApplication if one doesn't already exist - app = QApplication.instance() - if app is None: - app = QApplication(sys.argv) - - # Validate that the original file exists - if not Path(original_filepath).exists(): - print(f"Error: File not found at {original_filepath}") - return False - - # Open the file save dialog - destination, _ = QFileDialog.getSaveFileName( - None, - "Select destination for database file", - str(Path(original_filepath).parent), # Start in the original file's directory - "Database Files (*.db);;All Files (*)" - ) - - # If the user cancelled the dialog - if not destination: - print("Operation cancelled by user") - return False - - try: - # Move the file - shutil.move(original_filepath, destination) - print(f"File successfully moved to: {destination}") - return True - except Exception as e: - print(f"Error moving file: {e}") - return False - - -def launch_file_picker(original_path): - move_database_file(original_path) diff --git a/gui/v2/ui/styles/styles.py b/gui/v2/ui/styles/styles.py index 3b8137d..ac75a4b 100755 --- a/gui/v2/ui/styles/styles.py +++ b/gui/v2/ui/styles/styles.py @@ -116,6 +116,42 @@ POPUP_ACTION_BUTTON_RED_QSS = """ """ +def combobox_style(font_style=""): + return f""" + QComboBox {{ + color: black; + background: #f0f0f0; + padding: 5px 30px 5px 10px; + border: 1px solid #ccc; + border-radius: 4px; + min-width: 120px; + margin-bottom: 10px; + {font_style} + }} + QComboBox:disabled {{ + color: #666; + background: #e0e0e0; + }} + QComboBox::drop-down {{ + border: none; + width: 30px; + }} + QComboBox::down-arrow {{ + image: url(assets/down_arrow.png); + width: 12px; + height: 12px; + }} + QComboBox QAbstractItemView {{ + color: black; + background: white; + selection-background-color: #007bff; + selection-color: white; + border: 1px solid #ccc; + {font_style} + }} + """ + + def checkbox_style(font_style=""): return f""" QCheckBox {{