hydraveil-gui/gui/v2/ui/pages/settings_page.py
2026-07-01 11:01:26 -04:00

2189 lines
89 KiB
Python
Executable file

import logging
import os
import subprocess
import sys
import threading
from datetime import datetime, timezone
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 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
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,
combobox_style,
)
from gui.v2.ui.widgets.clickable_label import ClickableValueLabel
from gui.v2.ui.widgets.terminal_widget import TerminalWidget
from gui.v2.workers.page_data_worker import PageDataWorker
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, prepared=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._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)
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()
@staticmethod
def prepare_data(custom_window):
return settings_data.prepare(custom_window.gui_config_file)
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 = self._prepared_value("profiles", ProfileController.get_all)
profile_ids = self._prepared_value(
"profile_order",
lambda: 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(combobox_style(self.font_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 = self._prepared_value("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 = 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}")
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 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 = settings_data.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 = settings_data.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_checkbox_style(self) -> str:
return checkbox_style(self.font_style)
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(combobox_style(self.font_style))
profiles = self._prepared_value("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(
combobox_style(self.font_style))
profiles = self._prepared_value("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(
self._prepared_value(
"endpoint_verification_enabled",
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 update_verification_info(self, index):
if index < 0:
return
profile_id = self.verification_profile_selector.itemData(index)
profile = None
if profile_id is not None:
profile = ProfileController.get(profile_id)
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:
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:
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:
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)
self._settings_refresh_seq += 1
seq = self._settings_refresh_seq
module_path, class_name = PAGE_REGISTRY["settings"]
worker = PageDataWorker(
"settings", module_path, class_name, self.custom_window)
worker.data_ready.connect(
lambda name, payload, s=seq: self._on_settings_data_ready(payload, s))
worker.failed.connect(
lambda name, error: self._on_settings_data_failed(error))
worker.finished.connect(
lambda w=worker: self._cleanup_settings_worker(w))
self._settings_workers.add(worker)
worker.start()
def _on_settings_data_failed(self, error):
core_logger.warning(f"Settings data refresh failed: {error}")
def _cleanup_settings_worker(self, worker):
self._settings_workers.discard(worker)
worker.deleteLater()
def _on_settings_data_ready(self, payload, seq):
if seq != self._settings_refresh_seq:
return
self._prepared = payload
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 = self._prepared_value("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._prepared_value(
"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._prepared_value(
"ticket_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(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)
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 _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(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'):
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 _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(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.")
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._prepared_value(
"gui_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(
self._prepared_value("systemwide_enabled", settings_data.read_systemwide_enabled))
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(
self._prepared_value("bwrap_enabled", settings_data.read_bwrap_enabled))
return page
def load_systemwide_settings(self, enabled=None):
if enabled is None:
enabled = settings_data.read_systemwide_enabled()
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=None):
if enabled is None:
enabled = settings_data.read_bwrap_enabled()
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._prepared_value(
"gui_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")