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, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton, QScrollArea, QStackedLayout, QVBoxLayout, QWidget, ) from PyQt6.QtGui import QIcon from PyQt6.QtCore import Qt, QSize from core.Constants import Constants from core.controllers.ConfigurationController import ConfigurationController from core.controllers.PolicyController import PolicyController from core.controllers.ProfileController import ProfileController from core.controllers.tickets.UseTicketController import ( do_we_use_a_random_ticket, get_unused_tickets, modify_random_tickets_setting, ) from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemProfile import SystemProfile from core.Errors import ( CommandNotFoundError, PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError, ) from core.errors.logger import logger as core_logger from gui.v2.actions.key_interpretation import interpret_key_results from gui.v2.actions.profile_order import normalize_profile_order from gui.v2.infrastructure.setup_observers import ticket_observer from gui.v2.ui.pages.Page import Page from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup from gui.v2.ui.styles.styles import ( SCROLLBAR_CYAN_QSS, TERMINAL_LIST_QSS, checkbox_style, ) from gui.v2.ui.widgets.clickable_label import ClickableValueLabel from gui.v2.ui.widgets.terminal_widget import TerminalWidget from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread from gui.v2.workers.worker_thread import WorkerThread class Settings(Page): def __init__(self, page_stack, main_window, parent=None): super().__init__("Settings", page_stack, main_window, parent) self.font_style = f"font-family: '{self.custom_window.open_sans_family}';" self.update_status = main_window self.update_logging = main_window self.button_reverse.setVisible(True) self.button_reverse.setEnabled(True) self.button_reverse.clicked.connect(self.reverse) self.title.setGeometry(585, 40, 185, 40) self.title.setText("Settings") self.setup_ui() def setup_ui(self): main_container = QWidget(self) main_container.setGeometry(0, 0, 800, 520) self.layout = QHBoxLayout(main_container) self.layout.setContentsMargins(0, 60, 0, 0) self.left_panel = QWidget() self.left_panel.setFixedWidth(200) self.left_layout = QVBoxLayout(self.left_panel) self.left_layout.setContentsMargins(0, 0, 0, 0) self.left_layout.setSpacing(0) self.content_widget = QWidget() self.content_layout = QStackedLayout(self.content_widget) self.layout.addWidget(self.left_panel) self.layout.addWidget(self.content_widget) self.setup_menu_buttons() self.setup_pages() def setup_menu_buttons(self): menu_items = [ ("Overview", self.show_account_page), ("Subscriptions", self.show_subscription_page), ("Tickets", self.show_tickets_page), ("Create/Edit", self.show_registrations_page), ("Verification", self.show_verification_page), ("Legacy-Version", self.show_systemwide_page), ("Bwrap Permission", self.show_bwrap_page), ("Delete Profile", self.show_delete_page), ("Error Logs", self.show_logs_page), ("Debug Help", self.show_debug_page) ] self.menu_buttons = [] for text, callback in menu_items: btn = QPushButton(text) btn.setFixedHeight(40) btn.setCursor(Qt.CursorShape.PointingHandCursor) btn.setCheckable(True) btn.clicked.connect(callback) btn.setStyleSheet(f""" QPushButton {{ text-align: left; padding-left: 20px; border: none; background: transparent; color: #808080; font-size: 14px; {self.font_style} }} QPushButton:checked {{ background: rgba(255, 255, 255, 0.1); color: white; border-left: 3px solid #007AFF; }} QPushButton:hover:!checked {{ background: rgba(255, 255, 255, 0.05); }} """) self.left_layout.addWidget(btn) self.menu_buttons.append(btn) self.left_layout.addStretch() def create_delete_page(self): page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(15) title = QLabel("DELETE PROFILE") title.setStyleSheet( f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") layout.addWidget(title) self.delete_scroll_area = QScrollArea() self.delete_scroll_area.setFixedSize(510, 300) self.delete_scroll_area.setWidgetResizable(True) self.delete_scroll_area.setHorizontalScrollBarPolicy( Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.delete_scroll_area.setVerticalScrollBarPolicy( Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.delete_scroll_area.setStyleSheet(SCROLLBAR_CYAN_QSS) self.delete_scroll_widget = QWidget() self.delete_scroll_widget.setStyleSheet( "background-color: transparent;") self.delete_scroll_area.setWidget(self.delete_scroll_widget) self.profile_buttons = QButtonGroup() self.profile_buttons.setExclusive(True) self.create_delete_profile_buttons() layout.addWidget(self.delete_scroll_area) self.delete_button = QPushButton("Delete") self.delete_button.setEnabled(False) self.delete_button.setFixedSize(120, 40) self.delete_button.setStyleSheet(f""" QPushButton {{ background: #ff4d4d; color: white; border: none; border-radius: 5px; font-weight: bold; {self.font_style} }} QPushButton:disabled {{ background: #666666; }} QPushButton:hover:!disabled {{ background: #ff3333; }} """) self.delete_button.clicked.connect(self.delete_selected_profile) self.profile_buttons.buttonClicked.connect(self.on_profile_selected) button_layout = QHBoxLayout() button_layout.addStretch() button_layout.addWidget(self.delete_button) button_layout.addStretch() layout.addStretch() layout.addLayout(button_layout) return page def create_delete_profile_buttons(self): profiles = ProfileController.get_all() profile_ids = normalize_profile_order( getattr(self.update_status, 'gui_config_file', None), profiles.keys()) for index, profile_id in enumerate(profile_ids): profile = profiles[profile_id] row = index // 2 col = index % 2 btn = QPushButton(f"Profile {profile_id}\n{profile.name}") btn.setCheckable(True) btn.setFixedSize(180, 60) btn.setParent(self.delete_scroll_widget) btn.setGeometry(col * 220 + 50, row * 100, 180, 60) btn.setStyleSheet(f""" QPushButton {{ background: rgba(255, 255, 255, 0.1); border: none; color: white; border-radius: 5px; text-align: center; {self.font_style} }} QPushButton:checked {{ background: rgba(255, 255, 255, 0.3); border: 2px solid #007AFF; }} QPushButton:hover:!checked {{ background: rgba(255, 255, 255, 0.2); }} """) self.profile_buttons.addButton(btn, profile_id) if profiles: rows = (len(profiles) + 1) // 2 height = rows * 100 self.delete_scroll_widget.setFixedSize(510, height) def create_debug_page(self): page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(15) title = QLabel("DEBUG HELP") title.setStyleSheet( f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") layout.addWidget(title) scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setStyleSheet(f""" QScrollArea {{ border: none; background: transparent; }} QScrollArea > QWidget > QWidget {{ background: transparent; }} """) scroll_content = QWidget() scroll_layout = QVBoxLayout(scroll_content) scroll_layout.setSpacing(15) profile_selection_group = QGroupBox("Profile Selection") profile_selection_group.setStyleSheet(f""" QGroupBox {{ color: white; font-weight: bold; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 15px; font-size: 10px; margin-top: 15px; {self.font_style} }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 5px; }} """) profile_selection_layout = QVBoxLayout(profile_selection_group) profile_label = QLabel("Select System-wide Profile:") profile_label.setStyleSheet( f"color: white; font-size: 12px; {self.font_style}") profile_selection_layout.addWidget(profile_label) self.debug_profile_selector = QComboBox() self.debug_profile_selector.setStyleSheet(self.get_combobox_style()) self.debug_profile_selector.currentTextChanged.connect( self.on_debug_profile_selected) profile_selection_layout.addWidget(self.debug_profile_selector) scroll_layout.addWidget(profile_selection_group) ping_group = QGroupBox("Ping Test") ping_group.setStyleSheet(f""" QGroupBox {{ color: white; font-weight: bold; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 15px; font-size: 10px; margin-top: 15px; {self.font_style} }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 5px; }} """) ping_layout = QVBoxLayout(ping_group) self.ping_instruction_label = QLabel("") self.ping_instruction_label.setStyleSheet( f"color: white; font-size: 12px; {self.font_style}") self.ping_instruction_label.setWordWrap(True) self.ping_instruction_label.hide() ping_layout.addWidget(self.ping_instruction_label) ping_buttons_layout = QHBoxLayout() self.copy_ping_button = QPushButton("Copy Ping Command") self.copy_ping_button.setFixedSize(140, 40) self.copy_ping_button.setStyleSheet(f""" QPushButton {{ font-size: 12px; background: #007AFF; color: white; border: none; border-radius: 5px; font-weight: bold; {self.font_style} }} QPushButton:hover {{ background: #0056CC; }} QPushButton:disabled {{ background: #666666; }} """) self.copy_ping_button.clicked.connect(self.copy_ping_command) self.copy_ping_button.setEnabled(False) ping_buttons_layout.addWidget(self.copy_ping_button) self.test_ping_button = QPushButton("Test Ping") self.test_ping_button.setFixedSize(120, 40) self.test_ping_button.setStyleSheet(f""" QPushButton {{ font-size: 12px; background: #4CAF50; color: white; border: none; border-radius: 5px; font-weight: bold; {self.font_style} }} QPushButton:hover {{ background: #45a049; }} QPushButton:disabled {{ background: #666666; }} """) self.test_ping_button.clicked.connect(self.test_ping_connection) self.test_ping_button.setEnabled(False) ping_buttons_layout.addWidget(self.test_ping_button) ping_layout.addLayout(ping_buttons_layout) self.ping_result_label = QLabel("") self.ping_result_label.setStyleSheet( f"color: white; font-size: 12px; font-weight: bold; {self.font_style}") self.ping_result_label.setWordWrap(True) self.ping_result_label.hide() ping_layout.addWidget(self.ping_result_label) scroll_layout.addWidget(ping_group) command_group = QGroupBox("CLI Commands") command_group.setStyleSheet(f""" QGroupBox {{ color: white; font-weight: bold; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 15px; font-size: 10px; margin-top: 15px; {self.font_style} }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 5px; }} """) command_layout = QVBoxLayout(command_group) self.cli_command = QLineEdit() self.cli_command.setReadOnly(True) self.cli_command.setText("Select a profile above to see command") self.cli_command.setStyleSheet(f""" QLineEdit {{ color: white; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 4px; padding: 8px; font-family: 'Courier New', monospace; font-size: 12px; }} """) command_layout.addWidget(self.cli_command) cli_buttons_layout = QHBoxLayout() cli_copy_button = QPushButton("Copy CLI Command") cli_copy_button.setFixedSize(140, 40) cli_copy_button.setStyleSheet(f""" QPushButton {{ font-size: 12px; background: #007AFF; color: white; border: none; border-radius: 5px; font-weight: bold; {self.font_style} }} QPushButton:hover {{ background: #0056CC; }} QPushButton:disabled {{ background: #666666; }} """) cli_copy_button.clicked.connect(self.copy_cli_command) cli_copy_button.setEnabled(False) cli_buttons_layout.addWidget(cli_copy_button) command_layout.addLayout(cli_buttons_layout) find_buttons_layout = QHBoxLayout() command_layout.addLayout(find_buttons_layout) scroll_layout.addWidget(command_group) self.cli_copy_button = cli_copy_button wg_quick_group = QGroupBox("Direct Systemwide Wireguard") wg_quick_group.setStyleSheet(f""" QGroupBox {{ color: white; font-weight: bold; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 15px; font-size: 10px; margin-top: 15px; {self.font_style} }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 5px; }} """) wg_quick_layout = QVBoxLayout(wg_quick_group) wg_quick_subtitle = QLabel("This uses your Linux system's Wireguard") wg_quick_subtitle.setStyleSheet( f"font-size: 10px; color: #CCCCCC; {self.font_style}") wg_quick_subtitle.setWordWrap(True) wg_quick_layout.addWidget(wg_quick_subtitle) self.wg_quick_up_command = QLineEdit() self.wg_quick_up_command.setReadOnly(True) self.wg_quick_up_command.setText( "Select a profile above to see commands") self.wg_quick_up_command.setStyleSheet(f""" QLineEdit {{ color: black; background: white; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 4px; padding: 8px; font-family: 'Courier New', monospace; font-size: 12px; }} """) wg_quick_layout.addWidget(self.wg_quick_up_command) copy_wg_up_button = QPushButton("Copy UP command") copy_wg_up_button.setFixedSize(140, 40) copy_wg_up_button.setStyleSheet(f""" QPushButton {{ font-size: 10px; background: #4CAF50; color: white; border: none; border-radius: 5px; font-weight: bold; {self.font_style} }} QPushButton:hover {{ background: #45a049; }} """) copy_wg_up_button.clicked.connect(self.copy_wg_quick_up_command) copy_wg_up_button.setEnabled(False) wg_quick_layout.addWidget(copy_wg_up_button) self.wg_quick_down_command = QLineEdit() self.wg_quick_down_command.setReadOnly(True) self.wg_quick_down_command.setText( "Select a profile above to see commands") self.wg_quick_down_command.setStyleSheet(f""" QLineEdit {{ color: black; background: white; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 4px; padding: 8px; font-family: 'Courier New', monospace; font-size: 12px; }} """) wg_quick_layout.addWidget(self.wg_quick_down_command) copy_wg_down_button = QPushButton("Copy DOWN command") copy_wg_down_button.setFixedSize(140, 40) copy_wg_down_button.setStyleSheet(f""" QPushButton {{ font-size: 10px; background: #F44336; color: white; border: none; border-radius: 5px; font-weight: bold; {self.font_style} }} QPushButton:hover {{ background: #d32f2f; }} """) copy_wg_down_button.clicked.connect(self.copy_wg_quick_down_command) copy_wg_down_button.setEnabled(False) wg_quick_layout.addWidget(copy_wg_down_button) self.wg_quick_up_button = copy_wg_up_button self.wg_quick_down_button = copy_wg_down_button self.wg_quick_up_command_widget = self.wg_quick_up_command self.wg_quick_down_command_widget = self.wg_quick_down_command scroll_layout.addWidget(wg_quick_group) self.update_debug_profile_list() scroll_area.setWidget(scroll_content) layout.addWidget(scroll_area) return page def copy_cli_command(self): clipboard = QApplication.clipboard() clipboard.setText(self.cli_command.text()) self.update_status.update_status("CLI command copied to clipboard") def execute_cli_command(self): try: command = self.cli_command.text().split() subprocess.Popen(command) self.update_status.update_status("CLI command executed") except Exception as e: self.update_status.update_status( f"Error executing command: {str(e)}") def copy_find_command(self, command_text): clipboard = QApplication.clipboard() clipboard.setText(command_text) self.update_status.update_status("Find command copied to clipboard") def execute_find_command(self, command_text): try: command = command_text.split() subprocess.Popen(command) self.update_status.update_status("Find command executed") except Exception as e: self.update_status.update_status( f"Error executing find command: {str(e)}") def update_debug_profile_list(self): self.debug_profile_selector.clear() profiles = ProfileController.get_all() system_profiles = {pid: profile for pid, profile in profiles.items( ) if isinstance(profile, SystemProfile)} if system_profiles: for profile_id, profile in system_profiles.items(): self.debug_profile_selector.addItem( f"Profile {profile_id}: {profile.name}", profile_id) else: self.debug_profile_selector.addItem( "No system-wide profiles found", None) def on_debug_profile_selected(self, text): profile_id = self.debug_profile_selector.currentData() if profile_id is None: self.ping_instruction_label.hide() self.copy_ping_button.setEnabled(False) self.test_ping_button.setEnabled(False) self.ping_result_label.hide() self.cli_command.setText("Select a profile above to see command") self.cli_copy_button.setEnabled(False) if hasattr(self, 'wg_quick_up_command_widget'): self.wg_quick_up_command_widget.setText( "Select a profile above to see commands") self.wg_quick_down_command_widget.setText( "Select a profile above to see commands") self.wg_quick_up_button.setEnabled(False) self.wg_quick_down_button.setEnabled(False) return profile = ProfileController.get(profile_id) if profile and isinstance(profile, SystemProfile): app_path = os.environ.get('APPIMAGE') or "/path/to/hydra-veil" 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) 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}") self.ping_instruction_label.show() self.copy_ping_button.setEnabled(True) self.test_ping_button.setEnabled(True) self.ping_result_label.hide() if hasattr(self, 'wg_quick_up_command_widget'): self.wg_quick_up_command_widget.setText( f"sudo wg-quick up '/etc/hydra-veil/profiles/{profile_id}/wg.conf'") self.wg_quick_down_command_widget.setText( f"sudo wg-quick down '/etc/hydra-veil/profiles/{profile_id}/wg.conf'") self.wg_quick_up_button.setEnabled(True) self.wg_quick_down_button.setEnabled(True) else: self.ping_instruction_label.setText( "Could not extract IP address from WireGuard configuration") self.ping_instruction_label.show() self.copy_ping_button.setEnabled(False) self.test_ping_button.setEnabled(False) self.ping_result_label.hide() if hasattr(self, 'wg_quick_up_command_widget'): self.wg_quick_up_command_widget.setText( "Could not load profile configuration") self.wg_quick_down_command_widget.setText( "Could not load profile configuration") 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: return profile = ProfileController.get(profile_id) if profile and isinstance(profile, SystemProfile): ip_address = self.extract_endpoint_ip(profile) if ip_address: ping_command = f"ping {ip_address}" clipboard = QApplication.clipboard() clipboard.setText(ping_command) self.update_status.update_status( "Ping command copied to clipboard") def test_ping_connection(self): profile_id = self.debug_profile_selector.currentData() if profile_id is None: return profile = ProfileController.get(profile_id) if profile and isinstance(profile, SystemProfile): ip_address = self.extract_endpoint_ip(profile) if ip_address: self.test_ping_button.setEnabled(False) self.ping_result_label.setText("Testing ping...") self.ping_result_label.setStyleSheet( f"color: #FFA500; font-size: 12px; font-weight: bold; {self.font_style}") self.ping_result_label.show() def ping_test(): try: if sys.platform == "win32": result = subprocess.run(['ping', '-n', '4', ip_address], capture_output=True, text=True, timeout=10) else: result = subprocess.run(['ping', '-c', '4', ip_address], capture_output=True, text=True, timeout=10) if result.returncode == 0: self.ping_result_label.setText( "✅ Ping successful - Connection is working!") self.ping_result_label.setStyleSheet( f"color: #4CAF50; font-size: 12px; font-weight: bold; {self.font_style}") else: self.ping_result_label.setText( "❌ Ping failed - Connection issue detected") self.ping_result_label.setStyleSheet( f"color: #F44336; font-size: 12px; font-weight: bold; {self.font_style}") except subprocess.TimeoutExpired: self.ping_result_label.setText( "❌ Ping timeout - Connection issue detected") self.ping_result_label.setStyleSheet( f"color: #F44336; font-size: 12px; font-weight: bold; {self.font_style}") except Exception as e: self.ping_result_label.setText( f"❌ Ping error: {str(e)}") self.ping_result_label.setStyleSheet( f"color: #F44336; font-size: 12px; font-weight: bold; {self.font_style}") finally: self.test_ping_button.setEnabled(True) threading.Thread(target=ping_test, daemon=True).start() def copy_wg_quick_up_command(self): clipboard = QApplication.clipboard() clipboard.setText(self.wg_quick_up_command_widget.text()) self.update_status.update_status( "wg-quick UP command copied to clipboard") def copy_wg_quick_down_command(self): clipboard = QApplication.clipboard() clipboard.setText(self.wg_quick_down_command_widget.text()) self.update_status.update_status( "wg-quick DOWN command copied to clipboard") def on_profile_selected(self, button): self.delete_button.setEnabled(True) self.selected_profile_id = self.profile_buttons.id(button) def delete_selected_profile(self): if hasattr(self, 'selected_profile_id'): self.popup = ConfirmationPopup( self, message=f"Are you sure you want to delete Profile {self.selected_profile_id}?", action_button_text="Delete", cancel_button_text="Cancel" ) self.popup.setWindowModality(Qt.WindowModality.ApplicationModal) self.popup.finished.connect(self.handle_delete_confirmation) self.popup.show() def handle_delete_confirmation(self, confirmed): if confirmed: self.update_status.update_status(f'Deleting profile...') self.worker_thread = WorkerThread('DESTROY_PROFILE', profile_data={ 'id': self.selected_profile_id}) self.worker_thread.text_output.connect(self.delete_status_update) self.worker_thread.start() def delete_status_update(self, message): self.update_status.update_status(message) if not message.startswith('Could not delete'): menu_page = self.custom_window.navigator.get_cached("menu") if menu_page is not None and hasattr(menu_page, 'clear_profile_details'): menu_page.clear_profile_details() if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'): menu_page.refresh_menu_buttons() self.content_layout.removeWidget(self.delete_page) self.delete_page = self.create_delete_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") def create_subscription_page(self): page = QWidget() layout = QVBoxLayout(page) layout.setSpacing(20) layout.setContentsMargins(20, 20, 20, 20) title = QLabel("Subscription Info") title.setStyleSheet( f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") layout.addWidget(title) profile_group = QGroupBox("Profile Selection") profile_group.setStyleSheet(f""" QGroupBox {{ color: white; font-weight: bold; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 15px; margin-top: 15px; {self.font_style} }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 5px; }} """) profile_layout = QVBoxLayout(profile_group) self.profile_selector = QComboBox() self.profile_selector.setStyleSheet(self.get_combobox_style()) profiles = ProfileController.get_all() if profiles: for profile_id, profile in profiles.items(): self.profile_selector.addItem( f"Profile {profile_id}: {profile.name}", profile_id) profile_layout.addWidget(self.profile_selector) layout.addWidget(profile_group) subscription_group = QGroupBox("Subscription Details") subscription_group.setStyleSheet(f""" QGroupBox {{ color: white; font-weight: bold; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 15px; margin-top: 15px; {self.font_style} }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 5px; }} """) subscription_layout = QGridLayout(subscription_group) subscription_layout.setSpacing(10) self.subscription_info = {} info_items = [ ("Billing Code", "billing_code"), ("Expires At", "expires_at") ] for i, (label, key) in enumerate(info_items): if key == "billing_code": billing_container = QWidget() billing_layout = QVBoxLayout(billing_container) billing_layout.setContentsMargins(0, 0, 0, 0) billing_layout.setSpacing(5) stat_widget = self.create_stat_widget(label, "N/A") billing_layout.addWidget(stat_widget) copy_button = QPushButton("Copy") copy_button.setFixedSize(60, 30) copy_button.setStyleSheet(f""" QPushButton {{ background: #007AFF; color: white; border: none; border-radius: 4px; font-size: 11px; font-weight: bold; {self.font_style} }} QPushButton:hover {{ background: #0056CC; }} """) copy_button.clicked.connect(lambda: self.copy_billing_code()) billing_layout.addWidget(copy_button) row, col = divmod(i, 2) subscription_layout.addWidget(billing_container, row, col) old_label = stat_widget.findChild(QLabel, "value_label") if old_label: old_label.setParent(None) old_label.deleteLater() value_label = ClickableValueLabel("N/A", stat_widget) value_label.setObjectName("value_label") value_label.setStyleSheet( f"color: #00ffff; font-size: 12px; font-family: 'Retro Gaming', sans-serif;") value_label.setAlignment(Qt.AlignmentFlag.AlignCenter) stat_widget.layout().insertWidget(0, value_label) self.subscription_info[key] = value_label else: stat_widget = self.create_stat_widget(label, "N/A") row, col = divmod(i, 2) subscription_layout.addWidget(stat_widget, row, col) old_label = stat_widget.findChild(QLabel, "value_label") if old_label: old_label.setParent(None) old_label.deleteLater() value_label = ClickableValueLabel("N/A", stat_widget) value_label.setObjectName("value_label") value_label.setStyleSheet( f"color: #00ffff; font-size: 12px; font-family: 'Retro Gaming', sans-serif;") value_label.setAlignment(Qt.AlignmentFlag.AlignCenter) stat_widget.layout().insertWidget(0, value_label) self.subscription_info[key] = value_label layout.addWidget(subscription_group) clipboard_hint = QLabel( "💡 Click on the billing code or use the Copy button to copy it to clipboard") clipboard_hint.setStyleSheet( f"color: #666666; font-size: 11px; font-style: italic; {self.font_style}") clipboard_hint.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(clipboard_hint) layout.addStretch() self.profile_selector.currentIndexChanged.connect( self.update_subscription_info) if self.profile_selector.count() > 0: self.update_subscription_info(0) return page def create_verification_page(self): page = QWidget() layout = QVBoxLayout(page) layout.setSpacing(20) layout.setContentsMargins(20, 20, 20, 20) title = QLabel("Verification Information") title.setStyleSheet( f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") layout.addWidget(title) profile_label = QLabel("Profile Selection:") profile_label.setStyleSheet( f"color: white; font-size: 14px; {self.font_style}") layout.addWidget(profile_label) self.verification_profile_selector = QComboBox() self.verification_profile_selector.setStyleSheet( self.get_combobox_style()) profiles = ProfileController.get_all() if profiles: for profile_id, profile in profiles.items(): self.verification_profile_selector.addItem( f"Profile {profile_id}: {profile.name}", profile_id) layout.addWidget(self.verification_profile_selector) details_label = QLabel("Verification Details:") details_label.setStyleSheet( f"color: white; font-size: 14px; margin-top: 20px; {self.font_style}") layout.addWidget(details_label) verification_layout = QVBoxLayout() verification_layout.setSpacing(15) self.verification_info = {} self.verification_checkmarks = {} self.verification_full_values = {} info_items = [ ("Operator Name", "operator_name"), ("Nostr Key", "nostr_public_key"), ("HydraVeil Key", "hydraveil_public_key"), ("Nostr Verification", "nostr_attestation_event_reference"), ] for label, key in info_items: container = QWidget() container_layout = QHBoxLayout(container) container_layout.setContentsMargins(0, 0, 0, 0) container_layout.setSpacing(10) label_widget = QLabel(label + ":") label_widget.setStyleSheet( f"color: white; font-size: 12px; {self.font_style}") label_widget.setMinimumWidth(120) container_layout.addWidget(label_widget) value_widget = QLineEdit() value_widget.setReadOnly(True) value_widget.setText("N/A") value_widget.setStyleSheet(f""" QLineEdit {{ color: white; background: transparent; border: none; border-bottom: 1px solid rgba(255, 255, 255, 0.3); padding: 5px 0px; font-size: 12px; {self.font_style} }} """) container_layout.addWidget(value_widget, 1) checkmark_widget = QLabel("✓") checkmark_widget.setStyleSheet( f"color: #4CAF50; font-size: 20px; font-weight: bold; {self.font_style}") checkmark_widget.setFixedSize(20, 20) checkmark_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) checkmark_widget.hide() container_layout.addWidget(checkmark_widget) copy_button = QPushButton() copy_button.setIcon( QIcon(os.path.join(self.btn_path, "paste_button.png"))) copy_button.setIconSize(QSize(24, 24)) copy_button.setFixedSize(30, 30) copy_button.setStyleSheet(f""" QPushButton {{ background: transparent; border: none; }} QPushButton:hover {{ background: rgba(255, 255, 255, 0.1); border-radius: 4px; }} """) copy_button.clicked.connect( lambda checked=False, k=key: self.copy_verification_value(k)) container_layout.addWidget(copy_button) verification_layout.addWidget(container) self.verification_info[key] = value_widget self.verification_checkmarks[key] = checkmark_widget layout.addLayout(verification_layout) layout.addStretch() endpoint_verification_label = QLabel("Endpoint Verification:", page) endpoint_verification_label.setGeometry(20, 420, 150, 20) endpoint_verification_label.setStyleSheet( f"color: white; font-size: 15px; {self.font_style}") endpoint_verification_label.show() self.endpoint_verification_checkbox = QCheckBox(page) self.endpoint_verification_checkbox.setGeometry(180, 415, 30, 30) self.endpoint_verification_checkbox.setChecked( ConfigurationController.get_endpoint_verification_enabled()) self.endpoint_verification_checkbox.setStyleSheet( self.get_checkbox_style()) self.endpoint_verification_checkbox.show() save_button = QPushButton(page) save_button.setGeometry(350, 410, 60, 31) save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png"))) save_button.setIconSize(QSize(60, 31)) save_button.clicked.connect(self.save_verification_settings) save_button.show() self.verification_profile_selector.currentIndexChanged.connect( self.update_verification_info) self.verification_profile_selector.activated.connect( self.update_verification_info) if self.verification_profile_selector.count() > 0: self.update_verification_info(0) 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 = 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" for key, widget in self.verification_info.items(): 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: self.verification_checkmarks[key].hide() def save_verification_settings(self): try: ConfigurationController.set_endpoint_verification_enabled( self.endpoint_verification_checkbox.isChecked()) self.update_status.update_status( "Verification settings saved successfully") except Exception as e: logging.error(f"Error saving verification settings: {str(e)}") self.update_status.update_status( "Error saving verification settings") def copy_verification_value(self, key): full_value = self.verification_full_values.get(key, "") if full_value and full_value != "N/A": clipboard = QApplication.clipboard() clipboard.setText(full_value) verification_display_names = { "operator_name": "Operator Name", "nostr_public_key": "Nostr Key", "hydraveil_public_key": "HydraVeil Key", "nostr_attestation_event_reference": "Nostr Verification" } display_name = verification_display_names.get( key, "Verification value") self.update_status.update_status( f"{display_name} copied to clipboard") def update_subscription_info(self, index): if index < 0: return profile_id = self.profile_selector.itemData(index) if profile_id is None: return profile = ProfileController.get(profile_id) 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") except Exception as e: print(f"Error updating subscription info: {e}") else: for label in self.subscription_info.values(): label.setText("N/A") def copy_billing_code(self): if "billing_code" in self.subscription_info: billing_code = self.subscription_info["billing_code"].text() if billing_code and billing_code != "N/A": clipboard = QApplication.clipboard() clipboard.setText(billing_code) self.update_status.update_status( "Billing code copied to clipboard") else: self.update_status.update_status( "No billing code available to copy") def showEvent(self, event): super().showEvent(event) current_index = self.content_layout.currentIndex() self.content_layout.removeWidget(self.account_page) self.account_page = self.create_account_page() self.content_layout.addWidget(self.account_page) self.content_layout.removeWidget(self.subscription_page) self.subscription_page = self.create_subscription_page() self.content_layout.addWidget(self.subscription_page) self.content_layout.removeWidget(self.tickets_page) self.tickets_page = self.create_tickets_page() self.content_layout.addWidget(self.tickets_page) self.content_layout.removeWidget(self.registrations_page) self.registrations_page = self.create_registrations_page() self.content_layout.addWidget(self.registrations_page) self.content_layout.removeWidget(self.verification_page) self.verification_page = self.create_verification_page() self.content_layout.addWidget(self.verification_page) self.content_layout.removeWidget(self.systemwide_page) self.systemwide_page = self.create_systemwide_page() self.content_layout.addWidget(self.systemwide_page) self.content_layout.removeWidget(self.bwrap_page) self.bwrap_page = self.create_bwrap_page() self.content_layout.addWidget(self.bwrap_page) self.content_layout.removeWidget(self.delete_page) self.delete_page = self.create_delete_page() self.content_layout.addWidget(self.delete_page) self.content_layout.removeWidget(self.logs_page) self.logs_page = self.create_logs_page() self.content_layout.addWidget(self.logs_page) self.content_layout.removeWidget(self.debug_page) self.debug_page = self.create_debug_page() self.content_layout.addWidget(self.debug_page) self.content_layout.setCurrentIndex(current_index) if self.content_layout.currentWidget() == self.subscription_page: if self.profile_selector.count() > 0: self.update_subscription_info(0) def create_account_page(self): page = QWidget() layout = QVBoxLayout(page) layout.setSpacing(20) layout.setContentsMargins(20, 20, 20, 20) title = QLabel("Account Overview") title.setStyleSheet( f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") layout.addWidget(title) scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setStyleSheet(f""" QScrollArea {{ border: none; background: transparent; }} QScrollArea > QWidget > QWidget {{ background: transparent; }} """) scroll_content = QWidget() scroll_layout = QVBoxLayout(scroll_content) scroll_layout.setSpacing(15) info_sections = [ ("Profile Statistics", self.create_profile_stats()), ("System Resources", self.create_system_stats()) ] for section_title, content_widget in info_sections: group = QGroupBox(section_title) group.setStyleSheet(f""" QGroupBox {{ color: white; font-weight: bold; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 15px; margin-top: 15px; {self.font_style} }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 5px; }} """) group_layout = QVBoxLayout(group) group_layout.addWidget(content_widget) scroll_layout.addWidget(group) scroll_area.setWidget(scroll_content) layout.addWidget(scroll_area) return page def create_profile_stats(self): widget = QWidget() layout = QGridLayout(widget) layout.setSpacing(10) profiles = ProfileController.get_all() total_profiles = len(profiles) session_profiles = sum(1 for p in profiles.values() if isinstance(p, SessionProfile)) system_profiles = sum(1 for p in profiles.values() if isinstance(p, SystemProfile)) active_profiles = sum( 1 for p in profiles.values() if p.subscription and p.subscription.expires_at and p.subscription.expires_at > datetime.now(timezone.utc) ) stats = [ ("Total Profiles", total_profiles), ("Browser-only", session_profiles), ("System-wide", system_profiles), ("Active Profiles", active_profiles) ] for i, (label, value) in enumerate(stats): stat_widget = self.create_stat_widget(label, value) row, col = divmod(i, 2) layout.addWidget(stat_widget, row, col) return widget def create_system_stats(self): widget = QWidget() layout = QGridLayout(widget) layout.setSpacing(10) config_path = Constants.HV_CONFIG_HOME current_connection = self.update_status.get_current_connection() if current_connection is not None: current_connection = current_connection.capitalize() stats = [ ("Config Path", config_path), ("Client Version", Constants.HV_CLIENT_VERSION_NUMBER), ("Connection", current_connection), ] for i, (label, value) in enumerate(stats): info_widget = QLabel(f"{label}: {value}") info_widget.setStyleSheet( f"font-size: 14px; color: white; padding: 5px; {self.font_style}") info_widget.setWordWrap(True) layout.addWidget(info_widget, i, 0) return widget def create_stat_widget(self, label, value): widget = QFrame() widget.setStyleSheet(f""" QFrame {{ background: rgba(255, 255, 255, 0.05); border-radius: 8px; padding: 10px; {self.font_style} }} """) layout = QVBoxLayout(widget) value_label = QLabel(str(value)) value_label.setObjectName("value_label") value_label.setStyleSheet( "font-family: 'Retro Gaming', sans-serif; color: #00ffff; font-size: 22px; font-weight: bold") value_label.setAlignment(Qt.AlignmentFlag.AlignCenter) desc_label = QLabel(label) desc_label.setStyleSheet( f"color: white; font-size: 12px; {self.font_style}") desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(value_label) layout.addWidget(desc_label) return widget def create_horizontal_stat(self, label, value, total): widget = QWidget() layout = QHBoxLayout(widget) layout.setContentsMargins(0, 5, 0, 5) text_label = QLabel(f"{label}") text_label.setStyleSheet( f"color: white; font-size: 12px; {self.font_style}") text_label.setFixedWidth(100) progress = QFrame() progress.setStyleSheet(""" QFrame { background: rgba(0, 255, 255, 0.3); border-radius: 3px; } """) percentage = (value / total) * 100 if total > 0 else 0 progress.setFixedSize(int(200 * (percentage / 100)), 20) value_label = QLabel(f"{value} ({percentage:.1f}%)") value_label.setStyleSheet( f"color: white; font-size: 12px; {self.font_style}") value_label.setAlignment(Qt.AlignmentFlag.AlignRight) progress_container = QFrame() progress_container.setStyleSheet(""" QFrame { background: rgba(255, 255, 255, 0.1); border-radius: 3px; } """) progress_container.setFixedSize(200, 20) progress_layout = QHBoxLayout(progress_container) progress_layout.setContentsMargins(0, 0, 0, 0) progress_layout.addWidget(progress) progress.setFixedHeight(20) layout.addWidget(text_label) layout.addWidget(progress_container) layout.addWidget(value_label) layout.addStretch() return widget def setup_pages(self): self.account_page = self.create_account_page() self.subscription_page = self.create_subscription_page() self.tickets_page = self.create_tickets_page() self.registrations_page = self.create_registrations_page() self.verification_page = self.create_verification_page() self.systemwide_page = self.create_systemwide_page() self.bwrap_page = self.create_bwrap_page() self.logs_page = self.create_logs_page() self.delete_page = self.create_delete_page() self.debug_page = self.create_debug_page() self.content_layout.addWidget(self.account_page) self.content_layout.addWidget(self.subscription_page) self.content_layout.addWidget(self.tickets_page) self.content_layout.addWidget(self.registrations_page) self.content_layout.addWidget(self.verification_page) self.content_layout.addWidget(self.systemwide_page) self.content_layout.addWidget(self.bwrap_page) self.content_layout.addWidget(self.logs_page) self.content_layout.addWidget(self.delete_page) self.content_layout.addWidget(self.debug_page) self.content_layout.setCurrentIndex(0) self.show_account_page() def show_account_page(self): core_logger.info("User navigated to Settings -> Overview") self.content_layout.setCurrentWidget(self.account_page) self._select_menu_button("Overview") def show_subscription_page(self): core_logger.info("User navigated to Settings -> Subscriptions") self.content_layout.setCurrentWidget(self.subscription_page) self._select_menu_button("Subscriptions") def show_tickets_page(self): core_logger.info("User navigated to Settings -> Tickets") self.content_layout.setCurrentWidget(self.tickets_page) self._select_menu_button("Tickets") self._load_random_tickets_state() self._refresh_tickets_inventory() self._refresh_ticket_recovery_controls() def _load_random_tickets_state(self): if getattr(self, '_random_tickets_state_loaded', False): return try: which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer) checked = which_ticket is not None and which_ticket != 'error' except Exception: checked = False self.random_tickets_checkbox.blockSignals(True) self.random_tickets_checkbox.setChecked(checked) self.random_tickets_checkbox.blockSignals(False) self._random_tickets_state_loaded = True def show_registrations_page(self): core_logger.info("User navigated to Settings -> Create/Edit") self.content_layout.setCurrentWidget(self.registrations_page) self._select_menu_button("Create/Edit") def show_verification_page(self): core_logger.info("User navigated to Settings -> Verification") self.content_layout.setCurrentWidget(self.verification_page) self._select_menu_button("Verification") def show_systemwide_page(self): core_logger.info("User navigated to Settings -> Legacy-Version") self.content_layout.setCurrentWidget(self.systemwide_page) self._select_menu_button("Legacy-Version") def show_bwrap_page(self): core_logger.info("User navigated to Settings -> Bwrap Permission") self.content_layout.setCurrentWidget(self.bwrap_page) self._select_menu_button("Bwrap Permission") def show_logs_page(self): core_logger.info("User navigated to Settings -> Error Logs") self.content_layout.setCurrentWidget(self.logs_page) self._select_menu_button("Error Logs") def show_delete_page(self): core_logger.info("User navigated to Settings -> Delete Profile") self.content_layout.setCurrentWidget(self.delete_page) self._select_menu_button("Delete Profile") def show_debug_page(self): core_logger.info("User navigated to Settings -> Debug Help") self.content_layout.setCurrentWidget(self.debug_page) self._select_menu_button("Debug Help") def _select_menu_button(self, label): for btn in self.menu_buttons: if btn.text() == label: btn.setChecked(True) else: btn.setChecked(False) def create_tickets_page(self): page = QWidget() page_layout = QVBoxLayout(page) page_layout.setSpacing(15) page_layout.setContentsMargins(20, 20, 20, 20) title = QLabel("TICKETS") title.setStyleSheet( f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") page_layout.addWidget(title) scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) scroll_area.setStyleSheet(""" QScrollArea { background-color: transparent; border: none; } QScrollArea > QWidget > QWidget { background-color: transparent; } """) scroll_content = QWidget() layout = QVBoxLayout(scroll_content) layout.setSpacing(15) layout.setContentsMargins(0, 0, 8, 0) random_group = QGroupBox("Random Ticket Use") random_group.setStyleSheet( f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") random_layout = QVBoxLayout(random_group) self.random_tickets_checkbox = QCheckBox("Use a random ticket automatically when enabling a profile") self.random_tickets_checkbox.setStyleSheet(f"color: white; {self.font_style}") self.random_tickets_checkbox.setChecked(False) self.random_tickets_checkbox.toggled.connect(self._on_random_toggle) self._random_tickets_state_loaded = False random_layout.addWidget(self.random_tickets_checkbox) layout.addWidget(random_group) inventory_group = QGroupBox("Unused Tickets") inventory_group.setStyleSheet( f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") inventory_layout = QVBoxLayout(inventory_group) self.tickets_inventory_label = QLabel("Loading tickets...") self.tickets_inventory_label.setStyleSheet( f"color: white; font-size: 13px; {self.font_style}") self.tickets_inventory_label.setWordWrap(True) inventory_layout.addWidget(self.tickets_inventory_label) self.refresh_tickets_button = QPushButton("Refresh") self.refresh_tickets_button.setFixedSize(120, 32) self.refresh_tickets_button.setStyleSheet(f""" QPushButton {{ background: #00aaff; color: white; border: none; border-radius: 5px; font-weight: bold; {self.font_style} }} QPushButton:hover {{ background: #0088cc; }} """) self.refresh_tickets_button.clicked.connect(self._refresh_tickets_inventory) inventory_layout.addWidget(self.refresh_tickets_button) layout.addWidget(inventory_group) saved_failure = self.update_status.get_ticket_verification_failure() has_failure = saved_failure is not None recovery_group = QGroupBox("Verification Failure Recovery") recovery_group.setStyleSheet( 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.setStyleSheet( f"color: white; font-size: 12px; {self.font_style}") self.ticket_recovery_status_label.setWordWrap(True) recovery_layout.addWidget(self.ticket_recovery_status_label) self.evaluate_public_key_button = QPushButton("Evaluate Public Key") self.evaluate_public_key_button.setFixedSize(180, 36) self.evaluate_public_key_button.setEnabled(has_failure) self.evaluate_public_key_button.setStyleSheet(f""" QPushButton {{ background: #00aaff; color: white; border: none; border-radius: 5px; font-weight: bold; {self.font_style} }} QPushButton:hover:!disabled {{ background: #0088cc; }} QPushButton:disabled {{ background: #666666; color: #bbbbbb; }} """) self.evaluate_public_key_button.clicked.connect(self.evaluate_ticket_public_key) recovery_layout.addWidget(self.evaluate_public_key_button) self.ticket_recovery_output = TerminalWidget() self.ticket_recovery_output.setFixedHeight(130) if not has_failure: self.ticket_recovery_output.setPlainText("No saved verification failure.") recovery_layout.addWidget(self.ticket_recovery_output) layout.addWidget(recovery_group) debug_group = QGroupBox("Debug Recovery") debug_group.setStyleSheet( f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") debug_layout = QVBoxLayout(debug_group) self.prepare_saved_blind_sigs_button = QPushButton( "Prepare Tickets even if validation of the server's signature failed") self.prepare_saved_blind_sigs_button.setFixedSize(500, 40) self.prepare_saved_blind_sigs_button.setEnabled(has_failure) self.prepare_saved_blind_sigs_button.setStyleSheet(f""" QPushButton {{ background: #c0392b; color: white; border: none; border-radius: 5px; font-size: 10px; font-weight: bold; {self.font_style} }} QPushButton:hover:!disabled {{ background: #a93226; }} QPushButton:disabled {{ background: #666666; color: #bbbbbb; }} """) self.prepare_saved_blind_sigs_button.clicked.connect(self.prepare_tickets_with_saved_blind_signatures) debug_layout.addWidget(self.prepare_saved_blind_sigs_button) layout.addWidget(debug_group) layout.addStretch() scroll_area.setWidget(scroll_content) 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)) if hasattr(self, 'evaluate_public_key_button'): self.evaluate_public_key_button.setEnabled(has_failure) if hasattr(self, 'prepare_saved_blind_sigs_button'): self.prepare_saved_blind_sigs_button.setEnabled(has_failure) def _set_ticket_recovery_busy(self, busy): if hasattr(self, 'evaluate_public_key_button'): self.evaluate_public_key_button.setEnabled(not busy and self.update_status.get_ticket_verification_failure() is not None) if hasattr(self, 'prepare_saved_blind_sigs_button'): self.prepare_saved_blind_sigs_button.setEnabled(not busy and self.update_status.get_ticket_verification_failure() is not None) def _write_ticket_recovery_output(self, text): 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: self._write_ticket_recovery_output("No saved verification failure.") self.update_status.update_status("No ticket verification failure is saved.") self._refresh_ticket_recovery_controls() return None return failure def evaluate_ticket_public_key(self): core_logger.info("User clicked 'Evaluate Public Key' button") failure = self._get_ticket_failure_for_action() if failure is None: return failed_validations = list(failure.get("failed_validations", [])) self._write_ticket_recovery_output("Evaluating public key...") self.update_status.update_status("Evaluating ticket public key...") self._set_ticket_recovery_busy(True) self.ticket_recovery_worker = TicketingWorkerThread( 'EVALUATE_FAILED_VERIFICATION', params={'failed_validations': failed_validations}, ) self.ticket_recovery_worker.failed_verification_evaluated.connect(self.on_failed_verification_evaluated) self.ticket_recovery_worker.error.connect(self.on_ticket_recovery_error) self.ticket_recovery_worker.start() def on_failed_verification_evaluated(self, result): message = interpret_key_results(result) self._write_ticket_recovery_output(message) core_logger.info(f"Public key evaluation result: {message}") self.update_status.update_status("Ticket public key evaluation complete.") self._set_ticket_recovery_busy(False) self._refresh_ticket_recovery_controls() def prepare_tickets_with_saved_blind_signatures(self): core_logger.info("User clicked 'Prepare Tickets even if validation of the server's signature failed' button") failure = self._get_ticket_failure_for_action() if failure is None: return reply = QMessageBox.warning( self, "Prepare Tickets Anyway", "This will prepare tickets even though server signature validation failed. Continue only if you have evaluated the situation.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if reply != QMessageBox.StandardButton.Yes: return self._write_ticket_recovery_output("Preparing tickets with saved blind signatures...") self.update_status.update_status("Preparing tickets with saved blind signatures...") self._set_ticket_recovery_busy(True) self.ticket_recovery_worker = TicketingWorkerThread('PREPARE_SAVED_BLIND_SIGS') self.ticket_recovery_worker.saved_blind_prep_done.connect(self.on_saved_blind_prep_done) self.ticket_recovery_worker.error.connect(self.on_ticket_recovery_error) 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)) 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.") self._refresh_tickets_inventory() else: msg = result.get('message', 'failed') if isinstance(result, dict) else 'failed' self.update_status.update_status(f"Saved blind signature prep failed: {msg}") self._set_ticket_recovery_busy(False) self._refresh_ticket_recovery_controls() def on_ticket_recovery_error(self, msg): core_logger.error(f"Ticket recovery error: {msg}") self._write_ticket_recovery_output(f"EXCEPTION: {msg}") self.update_status.update_status(f"Ticket recovery error: {msg}") self._set_ticket_recovery_busy(False) self._refresh_ticket_recovery_controls() def _on_random_toggle(self, checked): core_logger.info(f"User toggled random ticket setting to {'on' if checked else 'off'}") try: result = modify_random_tickets_setting('on' if checked else 'off', ticket_observer) if not (isinstance(result, dict) and result.get('valid')): msg = result.get('message', 'failed') if isinstance(result, dict) else 'failed' core_logger.error(f"Could not change random ticket setting: {msg}") self.update_status.update_status(f'Could not change setting: {msg}') except Exception as e: core_logger.error(f"Exception changing random ticket setting: {e}") self.update_status.update_status(f'Could not change setting: {e}') def _refresh_tickets_inventory(self): if not hasattr(self, 'tickets_inventory_label'): return core_logger.info("Refreshing tickets inventory") try: unused = get_unused_tickets(ticket_observer) except Exception as e: core_logger.error(f"Error fetching unused tickets: {e}") self.tickets_inventory_label.setText(f"Error: {e}") return if isinstance(unused, dict) and unused.get('valid'): data = unused.get('data', []) if data: self.tickets_inventory_label.setText( f"You have {len(data)} unused tickets: " + ", ".join(f"#{t}" for t in data)) else: self.tickets_inventory_label.setText("No unused tickets.") else: msg = unused.get('message', 'No tickets found.') if isinstance(unused, dict) else 'No tickets found.' self.tickets_inventory_label.setText(msg) def create_logs_page(self): page = QWidget() layout = QVBoxLayout(page) layout.setSpacing(20) layout.setContentsMargins(20, 20, 20, 20) title = QLabel("LOGGING SETTINGS") title.setStyleSheet( f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") layout.addWidget(title) log_text = QLabel( f"Enabling this feature means error logs will be stored on your local\nmachine at: '{Constants.HV_CACHE_HOME}/gui'.") log_text.setStyleSheet( f"color: white; font-size: 14px; {self.font_style}") layout.addWidget(log_text) logs_group = QGroupBox("Log Configuration") logs_group.setStyleSheet( f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") logs_layout = QVBoxLayout(logs_group) self.enable_gui_logging = QCheckBox("Enable GUI logging") self.enable_gui_logging.setStyleSheet(self.get_checkbox_style()) logs_layout.addWidget(self.enable_gui_logging) layout.addWidget(logs_group) save_button = QPushButton() save_button.setFixedSize(75, 46) save_button.setIcon(QIcon(os.path.join(self.btn_path, f"save.png"))) save_button.setIconSize(QSize(75, 46)) save_button.clicked.connect(self.save_logs_settings) button_layout = QHBoxLayout() button_layout.addStretch() button_layout.addWidget(save_button) layout.addLayout(button_layout) layout.addStretch() self.load_logs_settings() return page def load_logs_settings(self) -> None: try: config = self.update_status._load_gui_config() if config and "logging" in config: self.enable_gui_logging.setChecked( config["logging"]["gui_logging_enabled"]) except Exception as e: logging.error(f"Error loading logging settings: {str(e)}") self.enable_gui_logging.setChecked(True) def save_logs_settings(self) -> None: try: config = self.update_status._load_gui_config() if config is None: config = { "logging": { "gui_logging_enabled": True, "log_level": "INFO" } } config["logging"]["gui_logging_enabled"] = self.enable_gui_logging.isChecked() self.update_status._save_gui_config(config) if self.enable_gui_logging.isChecked(): self.update_status._setup_gui_logging() core_logger.info("User enabled GUI logging") else: core_logger.info("User disabled GUI logging") self.update_status.stop_gui_logging() self.update_status.update_status( "Logging settings saved successfully") except Exception as e: logging.error(f"Error saving logging settings: {str(e)}") self.update_status.update_status("Error saving logging settings") def create_registrations_page(self): page = QWidget() layout = QVBoxLayout(page) layout.setSpacing(20) layout.setContentsMargins(20, 20, 20, 20) title = QLabel("Create/Edit SETTINGS") title.setStyleSheet( f"color: #808080; font-size: 15px; font-weight: bold; {self.font_style}") layout.addWidget(title) registrations_group = QGroupBox("Create/Edit Configuration") registrations_group.setStyleSheet( f"QGroupBox {{ color: white; font-size: 15px; padding: 15px; {self.font_style} }}") registrations_layout = QVBoxLayout(registrations_group) self.enable_auto_sync = QCheckBox("Enable auto-sync on Edit") self.enable_auto_sync.setStyleSheet(self.get_checkbox_style()) registrations_layout.addWidget(self.enable_auto_sync) self.enable_fast_registration = QCheckBox( "Enable Fast-mode for Profile Creation") self.enable_fast_registration.setStyleSheet(self.get_checkbox_style()) registrations_layout.addWidget(self.enable_fast_registration) self.dynamic_profiles_container = QWidget() dynamic_layout = QVBoxLayout(self.dynamic_profiles_container) dynamic_layout.setContentsMargins(20, 0, 0, 0) self.enable_dynamic_profiles = QCheckBox( "Enable Dynamic Larger Profiles") self.enable_dynamic_profiles.setStyleSheet(self.get_checkbox_style()) dynamic_layout.addWidget(self.enable_dynamic_profiles) dynamic_desc = QLabel("This is for fast mode profile creation. Some users may wish to have screen sizes that are nearly as large as their host, but slightly smaller by differing amounts. This fools fingerprinters but still gives larger sizes. So for example if your host is 1200 x 800, then fast mode would offer 1150 x 750, 1100 x 700, 1050 x 650, etc.") dynamic_desc.setWordWrap(True) dynamic_desc.setStyleSheet( f"color: #888888; font-size: 11px; font-style: italic; {self.font_style}") dynamic_layout.addWidget(dynamic_desc) registrations_layout.addWidget(self.dynamic_profiles_container) self.enable_fast_registration.toggled.connect( lambda checked: self.dynamic_profiles_container.setVisible(checked)) layout.addWidget(registrations_group) save_button = QPushButton() save_button.setFixedSize(75, 46) save_button.setIcon(QIcon(os.path.join(self.btn_path, f"save.png"))) save_button.setIconSize(QSize(75, 46)) save_button.clicked.connect(self.save_registrations_settings) button_layout = QHBoxLayout() button_layout.addWidget(save_button) button_layout.addStretch() layout.addLayout(button_layout) layout.addStretch() self.load_registrations_settings() return page def create_systemwide_page(self): page = QWidget() layout = QVBoxLayout(page) layout.setSpacing(20) layout.setContentsMargins(20, 20, 20, 20) title = QLabel("SYSTEM-WIDE PROFILES") title.setStyleSheet( f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") layout.addWidget(title) description = QLabel( "This is for the old version of the app! The new version prior to 2.1.0 does not need it. This allows you to enable or disable a legacy systemwide policy for sudo.") description.setWordWrap(True) description.setStyleSheet( f"color: white; font-size: 14px; {self.font_style}") layout.addWidget(description) status_layout = QHBoxLayout() status_label = QLabel("Current status:") status_label.setStyleSheet( f"color: white; font-size: 14px; {self.font_style}") self.systemwide_status_value = QLabel("") self.systemwide_status_value.setStyleSheet( f"color: #e67e22; font-size: 14px; {self.font_style}") status_layout.addWidget(status_label) status_layout.addWidget(self.systemwide_status_value) status_layout.addStretch() layout.addLayout(status_layout) toggle_layout = QHBoxLayout() self.systemwide_toggle = QCheckBox( "Enable \"No Sudo\" Systemwide Policy") self.systemwide_toggle.setStyleSheet(self.get_checkbox_style()) toggle_layout.addWidget(self.systemwide_toggle) toggle_layout.addStretch() layout.addLayout(toggle_layout) save_button = QPushButton() save_button.setFixedSize(75, 46) save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png"))) save_button.setIconSize(QSize(75, 46)) save_button.clicked.connect(self.save_systemwide_settings) button_layout = QHBoxLayout() button_layout.addWidget(save_button) button_layout.addStretch() layout.addLayout(button_layout) layout.addStretch() self.load_systemwide_settings() return page def create_bwrap_page(self): page = QWidget() layout = QVBoxLayout(page) layout.setSpacing(20) layout.setContentsMargins(20, 20, 20, 20) title = QLabel("BWRAP PERMISSION") title.setStyleSheet( f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") layout.addWidget(title) description = QLabel( "Control whether HydraVeil configures a capability policy so bwrap can be used without requiring additional permissions.") description.setWordWrap(True) description.setStyleSheet( f"color: white; font-size: 14px; {self.font_style}") layout.addWidget(description) status_layout = QHBoxLayout() status_label = QLabel("Current status:") status_label.setStyleSheet( f"color: white; font-size: 14px; {self.font_style}") self.bwrap_status_value = QLabel("") self.bwrap_status_value.setStyleSheet( f"color: #e67e22; font-size: 14px; {self.font_style}") status_layout.addWidget(status_label) status_layout.addWidget(self.bwrap_status_value) status_layout.addStretch() layout.addLayout(status_layout) toggle_layout = QHBoxLayout() self.bwrap_toggle = QCheckBox("Enable capability policy") self.bwrap_toggle.setStyleSheet(self.get_checkbox_style()) toggle_layout.addWidget(self.bwrap_toggle) toggle_layout.addStretch() layout.addLayout(toggle_layout) save_button = QPushButton() save_button.setFixedSize(75, 46) save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png"))) save_button.setIconSize(QSize(75, 46)) save_button.clicked.connect(self.save_bwrap_settings) button_layout = QHBoxLayout() button_layout.addWidget(save_button) button_layout.addStretch() layout.addLayout(button_layout) layout.addStretch() self.load_bwrap_settings() return page def load_systemwide_settings(self): enabled = False try: privilege_policy = PolicyController.get('privilege') if privilege_policy is not None: enabled = PolicyController.is_instated(privilege_policy) except Exception: enabled = False self.systemwide_toggle.setChecked(enabled) if enabled: self.systemwide_status_value.setText("Enabled") self.systemwide_status_value.setStyleSheet( f"color: #2ecc71; font-size: 14px; {self.font_style}") else: self.systemwide_status_value.setText("Disabled") self.systemwide_status_value.setStyleSheet( f"color: #e67e22; font-size: 14px; {self.font_style}") def save_systemwide_settings(self): enable = self.systemwide_toggle.isChecked() try: privilege_policy = PolicyController.get('privilege') if privilege_policy is not None: if enable: PolicyController.instate(privilege_policy) else: if PolicyController.is_instated(privilege_policy): PolicyController.revoke(privilege_policy) self.load_systemwide_settings() self.update_status.update_status( "System-wide policy settings updated") except CommandNotFoundError as e: self.systemwide_status_value.setText(str(e)) self.systemwide_status_value.setStyleSheet( f"color: red; font-size: 14px; {self.font_style}") except (PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError) as e: self.systemwide_status_value.setText(str(e)) self.systemwide_status_value.setStyleSheet( f"color: red; font-size: 14px; {self.font_style}") except Exception: self.systemwide_status_value.setText("Failed to update policy") self.systemwide_status_value.setStyleSheet( f"color: red; font-size: 14px; {self.font_style}") def load_bwrap_settings(self): enabled = False try: capability_policy = PolicyController.get('capability') if capability_policy is not None: enabled = PolicyController.is_instated(capability_policy) except Exception: enabled = False self.bwrap_toggle.setChecked(enabled) if enabled: self.bwrap_status_value.setText("Enabled") self.bwrap_status_value.setStyleSheet( f"color: #2ecc71; font-size: 14px; {self.font_style}") else: self.bwrap_status_value.setText("Disabled") self.bwrap_status_value.setStyleSheet( f"color: #e67e22; font-size: 14px; {self.font_style}") def save_bwrap_settings(self): enable = self.bwrap_toggle.isChecked() try: capability_policy = PolicyController.get('capability') if capability_policy is not None: if enable: PolicyController.instate(capability_policy) else: if PolicyController.is_instated(capability_policy): PolicyController.revoke(capability_policy) self.load_bwrap_settings() self.update_status.update_status( "Capability policy settings updated") except CommandNotFoundError as e: self.bwrap_status_value.setText(str(e)) self.bwrap_status_value.setStyleSheet( f"color: red; font-size: 14px; {self.font_style}") except (PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError) as e: self.bwrap_status_value.setText(str(e)) self.bwrap_status_value.setStyleSheet( f"color: red; font-size: 14px; {self.font_style}") except Exception: self.bwrap_status_value.setText("Failed to update policy") self.bwrap_status_value.setStyleSheet( f"color: red; font-size: 14px; {self.font_style}") def load_registrations_settings(self) -> None: try: config = self.update_status._load_gui_config() if config and "registrations" in config: registrations = config["registrations"] auto_sync = registrations.get("auto_sync_enabled", False) fast_reg = registrations.get( "fast_registration_enabled", False) dynamic_profiles = registrations.get( "dynamic_larger_profiles", False) if "auto_sync_enabled" not in registrations or "fast_registration_enabled" not in registrations or "dynamic_larger_profiles" not in registrations: registrations["auto_sync_enabled"] = auto_sync registrations["fast_registration_enabled"] = fast_reg registrations["dynamic_larger_profiles"] = dynamic_profiles self.update_status._save_gui_config(config) self.enable_auto_sync.setChecked(auto_sync) self.enable_fast_registration.setChecked(fast_reg) self.enable_dynamic_profiles.setChecked(dynamic_profiles) self.dynamic_profiles_container.setVisible(fast_reg) else: self.enable_auto_sync.setChecked(False) self.enable_fast_registration.setChecked(False) self.enable_dynamic_profiles.setChecked(False) self.dynamic_profiles_container.setVisible(False) except Exception as e: logging.error(f"Error loading registration settings: {str(e)}") self.enable_auto_sync.setChecked(False) self.enable_fast_registration.setChecked(False) self.enable_dynamic_profiles.setChecked(False) self.dynamic_profiles_container.setVisible(False) def save_registrations_settings(self) -> None: try: config = self.update_status._load_gui_config() if config is None: config = { "logging": { "gui_logging_enabled": True, "log_level": "INFO" }, "registrations": { "auto_sync_enabled": False, "fast_registration_enabled": False } } if "registrations" not in config: config["registrations"] = {} config["registrations"]["auto_sync_enabled"] = self.enable_auto_sync.isChecked() config["registrations"]["fast_registration_enabled"] = self.enable_fast_registration.isChecked() config["registrations"]["dynamic_larger_profiles"] = self.enable_dynamic_profiles.isChecked() self.update_status._save_gui_config(config) self.update_status.update_status("Registration settings saved") except Exception as e: 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