Compare commits

...

20 commits

Author SHA1 Message Date
JOhn
2e026c5287 update: adjusted images 2026-07-25 08:22:27 -04:00
JOhn
3483b51200 update: added new firewall and dns page 2026-07-25 08:20:26 -04:00
b444ff9218 Error handling for enabling profiles with issues using the sudo scripts. 2026-07-23 18:45:27 -04:00
1633562325 Introduced Firewall Feature in the Settings Page 2026-07-21 09:45:10 -04:00
b8d8689885 Core introduced a type field to profiles, so now GUI's worker_thread with profile creation needs that field 2026-07-20 13:10:49 -04:00
75bd51b8f8 Deleting the redundant orm startup 2026-07-19 15:15:56 -04:00
JOhn
ab23fc958e update: fixed systemwide profile popup on app closing 2026-07-18 08:31:28 -04:00
d52ca4842c Edit Profile logic pushed to core. GUI nows passes just the profile id, key, and data. And core returns the results of the save 2026-07-17 22:31:27 -04:00
JOhn
cfd9a92a24 update: fixed settings bug 2026-07-17 13:20:08 -04:00
c3cf7e241e Clears the CachedSync after a migration. And prompts for recovery on failure 2026-07-17 12:42:42 -04:00
9cacab4d71 Force sync on database delete 2026-07-16 15:56:41 -04:00
95e0f676df Migrations occur now automatically instead of forcing a database delete 2026-07-16 15:34:02 -04:00
baf7f7cec1 Rollback bwrap settings_page changes 2026-07-16 06:54:29 -04:00
0a6280276f Improved database management on entry 2026-07-15 19:34:24 -04:00
25489bf203 Database check messages were made more user-friendly 2026-07-14 20:57:14 -04:00
e5c83e72b1 Improved Asset Folder management 2026-07-12 21:32:33 -04:00
59cac90c58 Assets folder is part of startup 2026-07-12 20:02:10 -04:00
b536204048 Moving assets folder to root position 2026-07-12 18:08:17 -04:00
f8a09858cc Include assets folder with gui 2026-07-12 18:02:58 -04:00
50e9469178 Thread Safety Tool for bottom left. Transitioned navigator's prints to local logs via core's logger tool. Rolled back previous commit with bottom left status observers doing emit tools directly 2026-07-12 17:06:08 -04:00
20 changed files with 820 additions and 124 deletions

View file

@ -1,14 +1,45 @@
from core.errors.logger import logger
from core.models.manage.session_management import init_session, get_session, close_session, create_ALL_tables, create_ONLY_db_version_table, get_path, does_it_exist, does_db_version_table_exist
from core.models.manage.version_check import check_database_compatibility, insert_new_version, get_custom_message
from core.services.helpers.manage_assets import assets_folder_setup
from core.Constants import Constants
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.models.manage.migrations import migrate_sql
from core.models.manage.clear_sql_model import clear_sql_model
# generic
import os
import sys
from pathlib import Path
# ============================================================================
# DISPLAY CHOICES AND RECOVERY UI
# UTIL FUNCTIONS AND RECOVERY UI
# ============================================================================
def clear_sync_cache():
"""
Purpose:
Clear CachedSync
If that fails, prompt for a database wipe.
Why:
If they migrated, they have new schemas, but old data.
The problem with that, is the newest CachedSync entry will fool the sync data functions into thinking they don't need to sync new data,
Even though they do need to sync with the new schema.
"""
from core.models.orm_models.CachedSync import CachedSync
result = clear_sql_model(CachedSync)
if result:
logger.info(f"[DB MANAGEMENT] After Migration cleared the CachedSync table: {result}.")
else:
custom_error = "There were issues with clearing the Sync Cache in your database. Please delete the old version and fetch the new public data (what locations, browsers, ect) This will NOT affect your profiles or browser sessions."
recovery_dialog(custom_error, "CachedSync Issue")
def failed_migration(db_version_you_have):
close_session() # shut down db connection
custom_error = "There were issues with migrating your database. We transitioned to a new Database format for new features! Please delete the old version and fetch the new public data (what locations, browsers, ect) This will NOT affect your profiles or browser sessions."
recovery_dialog(custom_error, db_version_you_have)
def recovery_dialog(custom_error, db_version_you_have):
"""This ends the app by forcing them to delete the database, move it, or just quit."""
@ -19,13 +50,14 @@ def recovery_dialog(custom_error, db_version_you_have):
if choice == 1:
if os.path.exists(database_path):
os.remove(database_path)
logger.info(f"[DB MANAGEMENT] Deleted the DB file at {database_path}")
logger.info(f"[DB MANAGEMENT] Deleted the DB file at {database_path}. Now putting lock on for forced sync..")
lock_file.touch()
logger.info(f"[DB MANAGEMENT] Closing the app gracefully, for them to reboot..")
sys.exit()
elif choice == 2:
logger.info(f"[DB MANAGEMENT] User opted to move the DB file.")
from v2.ui.popups.pick_folder_to_move import launch_file_picker
from gui.v2.ui.popups.pick_folder_to_move import launch_file_picker
launch_file_picker(database_path)
sys.exit()
@ -33,34 +65,56 @@ def recovery_dialog(custom_error, db_version_you_have):
logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.")
sys.exit()
# ============================================================================
# ASSETS FOLDER
# ============================================================================
logger.info("Welcome, checking assets..")
if not assets_folder_setup():
from gui.v2.ui.popups.generic_error_popup import show_error
custom_error = "Unable to Setup your Assets Folder. Please create an assets directory at ~/.local/share/hydra-veil/assets"
show_error(custom_error, "Critical Error")
# ============================================================================
# INITIALIZE DATABASE
# ============================================================================
# Does the database exist?
system_path = get_path()
database_path = system_path / "storage.db"
main_db_exists = does_it_exist(database_path)
lock_file = Path(f"{Constants.HV_DATA_HOME}/deleted_db.lock")
logger.info("[DB MANAGEMENT] Starting DB init..")
try:
# Does the database exist?
system_path = get_path()
database_path = system_path / "storage.db"
main_db_exists = does_it_exist(database_path)
# Setup operations on the main DB which create it
init_session() # (engine, Session, _session all initialized from session_management)
session = get_session()
# Setup operations on the main DB which create it
init_session() # (engine, Session, _session all initialized from session_management)
session = get_session()
# does the version checker table exist?
version_table_exists = does_db_version_table_exist()
# does the version checker table exist?
version_table_exists = does_db_version_table_exist()
except:
logger.error("Critical Error with database initialization!")
sys.exit()
# are they upgrading from a legacy version?
# that would mean the table existed, but not the version table,
# ============================================================================
# LEGACY DATABASE CHECKS
# ============================================================================
migration_happened = False
# are they upgrading from a legacy version? that would mean the table existed, but not the version table,
if not version_table_exists and main_db_exists:
logger.info(f"[DB MANAGEMENT] We are dealing with a legacy database, we need to transition the user.")
# shut down db connection
close_session()
migration = migrate_sql()
logger.info(f"[DB MANAGEMENT] Migration result is {migration.valid}.")
if migration.valid:
migration_happened = True
force_sync = True
clear_sync_cache()
else:
# THEN DISPLAY CHOICES AND RECOVERY UI
custom_error = "You're using a Legacy version of the Database scheme. Please delete it and fetch the new data."
db_version_you_have = "Old System"
recovery_dialog(custom_error, db_version_you_have)
sys.exit()
failed_migration(db_version_you_have)
# If they're still here, then if it's NOT a legacy version,
@ -80,18 +134,27 @@ logger.info(f"[DB MANAGEMENT] The result of the check is {is_compatable} and the
# IF THE VERSIONS MATCH
# ============================================================================
if is_compatable:
try:
made_tables = create_ALL_tables()
# SCREEN FAILED TABLES
if not made_tables:
custom_error = "Critical Failure with starting the models of the database."
logger.error(f"[DB MANAGEMENT] {custom_error}")
close_session()
from v2.ui.popups.Database_version import show_recovery_dialog
from gui.v2.ui.popups.Database_version import show_recovery_dialog
choice = show_recovery_dialog({"message": custom_error, "db_version": 0, "status": "cant_make"})
logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}")
sys.exit()
except:
logger.info(f"[DB MANAGEMENT] create ALL tables failed. Running migrations...")
migration = migrate_sql()
logger.info(f"[DB MANAGEMENT] Results of migration is {migration.valid}")
if not migration.valid:
db_version_you_have = 0
failed_migration(db_version_you_have)
# ASSUME TABLES CREATED
logger.info("[DB MANAGEMENT] Tables successfully made or initialized if pre-existing")
@ -109,9 +172,19 @@ if is_compatable:
force_sync = True
else:
logger.info(f"[DB MANAGEMENT] Launcher is now passing it off to launch the main GUI window WITHOUT force sync..")
if migration_happened:
force_sync = True
clear_sync_cache()
else:
force_sync = False
# if we deleted their DB, we want to force sync,
if lock_file.exists():
force_sync = True
lock_file.unlink() # unlock for next time
# Start GUI either way:
logger.info(f"[DB MANAGEMENT] Starting GUI..")
start_ui(force_sync)
# ============================================================================

View file

@ -0,0 +1,54 @@
fields:
- name: country_code
path: ['country', 'code']
required: true
- name: code # this is city code
path: ['code']
required: true
- name: id
path: ['id']
- name: country_name
path: ['country', 'name']
- name: name # this is CITY name
path: ['name']
- name: time_zone
path: ['time_zone', 'code']
- name: operator_id
path: ['operator_id']
required: true
- name: provider_name
path: ['provider', 'name']
- name: is_proxy_capable
path: ['is_proxy_capable']
- name: is_wireguard_capable
path: ['is_wireguard_capable']
required: true
- name: is_hysteria2_capable
path: ['is_hysteria2_capable']
- name: is_vless_capable
path: ['is_vless_capable']
# original version:
# locations.append((
# location['country']['code'],
# location['code'],
# location['id'],
# location['country']['name'],
# location['name'],
# location['time_zone']['code'],
# location['operator_id'],
# location['provider']['name'],
# location['is_proxy_capable'],
# location['is_wireguard_capable']))

View file

@ -0,0 +1,8 @@
fields:
- name: group_a_code
path: ['group_a', 'code']
- name: code
path: ['code']
- name: id
path: ['id']
required: true

View file

@ -0,0 +1,19 @@
fields:
- name: id
path: ['id']
required: true
- name: name
path: ['name']
- name: public_key # this is ed25519
path: ['public_key']
- name: nostr_public_key
path: ['nostr_public_key']
- name: nostr_profile_reference
path: ['nostr_profile_reference']
- name: nostr_attestation_event_reference
path: ['nostr_attestation', 'event_reference']

View file

@ -12,17 +12,21 @@ from PyQt6.QtWidgets import (
QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton,
QDialog, QVBoxLayout, QHBoxLayout, QMessageBox
)
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase, QIcon
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase
from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QTimer, QEvent
from core.Constants import Constants
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.ProfileController import ProfileController
from core.Errors import UnknownConnectionTypeError
from core.errors.logger import logger as core_logger
core_logger.propagate = False
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
mainthread = ThreadSafetyTool()
from gui.v2.infrastructure import orm
from gui.v2.infrastructure.navigator import Navigator
from gui.v2.infrastructure.setup_observers import setup_observers
@ -61,7 +65,6 @@ from gui.v2.actions.ticket_failure import (
clear_ticket_verification_failure,
)
from gui.v2.actions.should_be_synchronized import should_be_synchronized
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
class CustomWindow(QMainWindow):
@ -73,9 +76,6 @@ class CustomWindow(QMainWindow):
gui_dir = os.path.dirname(os.path.abspath(__file__))
font_path = os.path.join(gui_dir, 'resources', 'fonts')
# # Get's SQLAlchemy going,
# orm.start()
retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf')
font_id = QFontDatabase.addApplicationFont(retro_gaming_path)
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
@ -126,6 +126,9 @@ class CustomWindow(QMainWindow):
self.animation_step = 0
self.page_history = []
self.log_path = None
self._close_confirmation_pending = False
self._close_disconnect_in_progress = False
self._closing_after_disconnect = False
self.setFixedSize(800, 570)
@ -625,18 +628,95 @@ class CustomWindow(QMainWindow):
def closeEvent(self, event=None):
core_logger.info("HydraVeil application closing")
# Close SQLAlchemy,
if self._closing_after_disconnect:
orm.stop()
if event is not None:
event.accept()
return
connected_profiles = self.connection_manager.get_connected_profiles()
if self._close_confirmation_pending or self._close_disconnect_in_progress:
if event is not None:
event.ignore()
return
connected_profiles = self._get_enabled_profile_ids()
if connected_profiles:
self.update_status('Profiles are still connected (disconnect flow lands in Chunk 9).')
if event is not None:
event.accept()
else:
if event is not None:
event.ignore()
self._show_close_disconnect_confirmation(connected_profiles)
return
orm.stop()
if event is not None:
event.accept()
def _get_enabled_profile_ids(self):
try:
profiles = ProfileController.get_all()
except Exception:
return self.connection_manager.get_connected_profiles()
connected_profiles = []
self.connection_manager._connected_profiles.clear()
for profile_id, profile in profiles.items():
try:
if ProfileController.is_enabled(profile):
connected_profiles.append(profile_id)
self.connection_manager.add_connected_profile(profile_id)
except Exception:
pass
return connected_profiles
def _show_close_disconnect_confirmation(self, connected_profiles):
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
self._close_confirmation_pending = True
message = self._create_close_disconnect_message(connected_profiles)
self.popup = ConfirmationPopup(
self, message=message, action_button_text="Exit", cancel_button_text="Cancel")
self.popup.setWindowModality(Qt.WindowModality.ApplicationModal)
self.popup.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.popup.finished.connect(
lambda result: self._handle_close_disconnect_result(result, connected_profiles))
self.popup.destroyed.connect(
lambda *_: self._handle_close_disconnect_popup_closed())
self.popup.show()
def _create_close_disconnect_message(self, connected_profiles):
profile_numbers = ', '.join(str(profile_id)
for profile_id in connected_profiles)
is_are = "is" if len(connected_profiles) == 1 else "are"
return f'Profile{"" if len(connected_profiles) == 1 else "s"} {profile_numbers} {is_are} still connected.\nAll connected profiles will be disconnected on exit.\nDo you want to proceed?'
def _handle_close_disconnect_result(self, result, connected_profiles):
self._close_confirmation_pending = False
if not result:
return
self._close_disconnect_in_progress = True
self.update_status('Disabling profiles...')
self.worker_thread = WorkerThread(
'DISABLE_ALL_PROFILES', profile_data=connected_profiles)
self.worker_thread.text_output.connect(self.update_status)
self.worker_thread.finished.connect(self._finish_close_disconnect)
self.worker_thread.start()
def _handle_close_disconnect_popup_closed(self):
if self._close_confirmation_pending:
self._close_confirmation_pending = False
self.popup = None
def _finish_close_disconnect(self, _ok=True):
self._close_disconnect_in_progress = False
connected_profiles = self._get_enabled_profile_ids()
if connected_profiles:
self.update_status('Could not disconnect all profiles. Exit canceled.')
return
self.update_status('All profiles disabled. Bye!')
self._closing_after_disconnect = True
QTimer.singleShot(0, self.close)
def update_image(self, appearance_value="original"):
image_path = os.path.join(self.btn_path, f"{appearance_value}.png")
pixmap = QPixmap(image_path)
@ -656,7 +736,21 @@ class CustomWindow(QMainWindow):
self.status_label.setStyleSheet(
f"color: rgb(0, 255, 255); font-size: {font_size}px;")
@mainthread # wrapper of ThreadSafetyTool
def update_status(self, text, clear=False):
"""
Purpose:
Update the bottom left status bar
Features:
Forced on the main Thread by ThreadSafetyTool
Depends on:
v2.infrastructure.ThreadSafetyTool
Called by:
v2.infrastructure.setup_observers (outside this class)
"""
if text is None:
self._set_status_font_size(16)
self.status_label.setText('Status:')
@ -799,5 +893,5 @@ class CustomWindow(QMainWindow):
def start_ui(force_sync):
app = QApplication(sys.argv)
window = CustomWindow(force_sync)
window = CustomWindow(force_sync=force_sync)
sys.exit(app.exec())

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -5,12 +5,23 @@ 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.services.networking.systemwide.systemwide_utils import get_firewall_setting
from gui.v2.actions.config import load_gui_config
from gui.v2.actions.profile_order import normalize_profile_order
from gui.v2.actions.ticket_failure import get_ticket_verification_failure
def read_firewall_setting():
try:
firewall_setting = get_firewall_setting()
return firewall_setting
except Exception:
pass
return False
def read_systemwide_enabled():
try:
privilege_policy = PolicyController.get('privilege')
@ -45,6 +56,7 @@ def prepare(gui_config_file):
"endpoint_verification_enabled": endpoint_verification_enabled,
"systemwide_enabled": read_systemwide_enabled(),
"bwrap_enabled": read_bwrap_enabled(),
"firewall_setting": read_firewall_setting(),
"gui_config": load_gui_config(gui_config_file),
"ticket_failure": get_ticket_verification_failure(gui_config_file),
}
@ -58,6 +70,7 @@ def empty_payload():
"endpoint_verification_enabled": False,
"systemwide_enabled": False,
"bwrap_enabled": False,
"firewall_setting": False,
"gui_config": None,
"ticket_failure": None,
}

View file

@ -0,0 +1,21 @@
import sys
import time
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QLabel
from PyQt6.QtCore import QObject, pyqtSignal, Qt
class ThreadSafetyTool(QObject):
"""Marshal function calls to the main thread."""
_signal = pyqtSignal(object, tuple, dict)
def __init__(self):
super().__init__()
self._signal.connect(self._execute, Qt.ConnectionType.QueuedConnection)
def _execute(self, func, args, kwargs):
func(*args, **kwargs)
def __call__(self, func):
"""Decorator that forces a function to run on the main thread."""
def wrapper(*args, **kwargs):
self._signal.emit(func, args, kwargs)
return wrapper

View file

@ -13,6 +13,7 @@ from gui.v2.infrastructure.page_registry import (
)
from gui.v2.workers.page_data_worker import PageDataWorker
from gui.v2.actions.flags import is_fast_registration_enabled
from core.errors.logger import logger
class Navigator(QObject):
@ -79,26 +80,25 @@ class Navigator(QObject):
def _preload_one(self, name):
if name in self._instances:
print(f"[preload] {name:30s} SKIP (already cached)", flush=True)
logger.debug(f"[preload] {name:30s} SKIP (already cached)")
return False
if name not in PAGE_REGISTRY:
print(f"[preload] {name:30s} SKIP (not in registry)", flush=True)
logger.debug(f"[preload] {name:30s} SKIP (not in registry)")
return False
module_path, class_name = PAGE_REGISTRY[name]
print(f"[preload] {name:30s} LOAD {module_path}.{class_name}", flush=True)
logger.debug(f"[preload] {name:30s} LOAD {module_path}.{class_name}")
t0 = time.perf_counter()
try:
page = self._instantiate(name)
self.register_instance(name, page)
dt_ms = (time.perf_counter() - t0) * 1000.0
print(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, "
f"cached={len(self._instances)}, stack_size={self.page_stack.count()})",
flush=True)
logger.debug(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, "
f"cached={len(self._instances)}, stack_size={self.page_stack.count()})")
return True
except Exception as e:
dt_ms = (time.perf_counter() - t0) * 1000.0
print(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) "
f"{type(e).__name__}: {e}", flush=True)
logger.debug(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) "
f"{type(e).__name__}: {e}")
traceback.print_exc()
from core.errors.logger import logger as core_logger
core_logger.warning(f"Background preload failed for '{name}': "
@ -121,29 +121,29 @@ class Navigator(QObject):
self._preload_inflight = 0
self._preload_summary_emitted = False
self._preload_t_start = time.perf_counter()
print(f"[preload] ============================================================", flush=True)
print(f"[preload] starting background preload: {self._preload_total} pages queued", flush=True)
print(f"[preload] order : {', '.join(self._preload_queue)}", flush=True)
print(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)", flush=True)
logger.debug(f"[preload] ============================================================")
logger.debug(f"[preload] starting background preload: {self._preload_total} pages queued")
logger.debug(f"[preload] order : {', '.join(self._preload_queue)}")
logger.debug(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)")
if fast_mode:
print(f"[preload] fastmode: ON -> skipping regular-flow pages: "
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True)
print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True)
print(f"[preload] ============================================================", flush=True)
logger.debug(f"[preload] fastmode: ON -> skipping regular-flow pages: "
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}")
logger.debug(f"[preload] gap : 250 ms between loads, 700 ms initial delay")
logger.debug(f"[preload] ============================================================")
self._preload_timer.start(700)
def _preload_next(self):
while self._preload_queue and self._preload_queue[0] in self._instances:
skipped = self._preload_queue.pop(0)
self._preload_skipped_runtime += 1
print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True)
logger.debug(f"[preload] {skipped:30s} SKIP (user navigated here first)")
if not self._preload_queue:
self._maybe_emit_preload_summary()
return
self._preload_dispatched += 1
idx = self._preload_dispatched
name = self._preload_queue.pop(0)
print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True)
logger.debug(f"[preload] ---- {idx}/{self._preload_total} ----")
if name in ASYNC_PREPARE:
self._dispatch_async_preload(name)
else:
@ -165,7 +165,7 @@ class Navigator(QObject):
def _dispatch_async_preload(self, name):
module_path, class_name = PAGE_REGISTRY[name]
print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True)
logger.debug(f"[preload] {name:30s} ASYNC {module_path}.{class_name}")
worker = PageDataWorker(name, self._prepare_page_data, name)
worker.data_ready.connect(self._on_preload_data_ready)
worker.failed.connect(self._on_preload_failed)
@ -177,22 +177,22 @@ class Navigator(QObject):
def _on_preload_data_ready(self, name, payload):
self._preload_inflight -= 1
if name in self._instances:
print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True)
logger.debug(f"[preload] {name:30s} SKIP (already cached, async result dropped)")
else:
try:
page = self._instantiate(name, prepared=payload)
self.register_instance(name, page)
self._preload_done += 1
print(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, "
f"stack_size={self.page_stack.count()})", flush=True)
logger.debug(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, "
f"stack_size={self.page_stack.count()})")
except Exception as e:
self._preload_failed += 1
print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True)
logger.debug(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}")
self._maybe_emit_preload_summary()
def _on_preload_failed(self, name, error):
self._preload_inflight -= 1
print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True)
logger.debug(f"[preload] {name:30s} FAIL (async prepare) {error}")
if name not in self._instances:
ok = self._preload_one(name)
if ok:
@ -215,10 +215,10 @@ class Navigator(QObject):
def _emit_preload_summary(self):
total_s = time.perf_counter() - self._preload_t_start
print(f"[preload] ============================================================", flush=True)
print(f"[preload] complete: {self._preload_done} loaded, "
logger.debug(f"[preload] ============================================================")
logger.debug(f"[preload] complete: {self._preload_done} loaded, "
f"{self._preload_failed} failed, "
f"{self._preload_skipped_runtime} skipped (user beat us to it) "
f"in {total_s:.2f}s", flush=True)
print(f"[preload] cached pages now: {sorted(self._instances.keys())}", flush=True)
print(f"[preload] ============================================================", flush=True)
f"in {total_s:.2f}s")
logger.debug(f"[preload] cached pages now: {sorted(self._instances.keys())}")
logger.debug(f"[preload] ============================================================")

View file

@ -1,6 +1,7 @@
PAGE_REGISTRY = {
"blank": ("gui.v2.ui.pages._blank_page", "BlankPage"),
"welcome": ("gui.v2.ui.pages.welcome_page", "WelcomePage"),
"networking_setup": ("gui.v2.ui.pages.networking_setup_page", "NetworkingSetupPage"),
"menu": ("gui.v2.ui.pages.menu_page", "MenuPage"),
"protocol": ("gui.v2.ui.pages.protocol_page", "ProtocolPage"),
"hidetor": ("gui.v2.ui.pages.hidetor_page", "HidetorPage"),

View file

@ -10,6 +10,8 @@ from PyQt6 import QtCore, QtGui
from core.controllers.ProfileController import ProfileController
from core.controllers.LocationController import LocationController
from core.models.Result import Result, ResultError
from core.controllers.profile_state.update_profile import update_profile
from gui.v2.ui.pages.Page import Page
from gui.v2.ui.pages.browser_page import BrowserPage
@ -795,49 +797,24 @@ class EditorPage(Page):
self._prefetched_profiles = None
def update_core_profiles(self, key, new_value):
profile = ProfileController.get(
int(self.update_status.current_profile_id))
self.update_res = False
if key == 'dimentions':
profile.resolution = new_value
profile_id = int(self.update_status.current_profile_id)
# Sends to core directly,
result = update_profile(profile_id, key, new_value)
if result.valid:
self.update_status.update_status(
f'Updated profile {profile_id}')
if key == "resolution":
self.update_res = True
elif key == 'name':
profile.name = new_value
elif key == 'connection':
if new_value == 'tor':
profile.connection.code = new_value
profile.connection.masked = True
elif new_value == 'just proxy':
profile.connection.code = 'system'
profile.connection.masked = True
else:
self.update_status.update_status(
'System wide profiles not supported atm')
elif key == 'browser':
browser_type, browser_version = new_value.split(':', 1)
profile.application_version.application_code = browser_type
profile.application_version.version_number = browser_version
elif key == 'protocol':
if self.data_profile.get('connection') == 'system-wide':
if result.data == "edit_to_session":
self.edit_to_session()
else:
profile.connection.code = 'wireguard' if new_value == 'wireguard' else 'tor'
profile.connection.masked = False if new_value == 'wireguard' else True
else:
location = self.connection_manager.get_location_info(new_value)
self.update_status.update_status(result.message)
if location:
profile.location.code = location.code
profile.location.country_code = location.country_code
profile.location.time_zone = location.time_zone
profile.subscription = None
ProfileController.update(profile)
def edit_to_session(self):
id = int(self.update_status.current_profile_id)

View file

@ -0,0 +1,191 @@
import os
from PyQt6.QtWidgets import QLabel, QPushButton, QWidget
from PyQt6.QtGui import QPixmap
from PyQt6.QtCore import Qt, QTimer
from core.services.helpers.setup_sudo_scripts import auto_install_sudo_script, test_if_in_sudo_folder
from core.services.helpers.manage_assets import sudo_assets_folder_setup
from core.models.Result import Result, ResultError
import sys
from gui.v2.ui.pages.Page import Page
class NetworkingSetupPage(Page):
def __init__(self, page_stack, main_window=None, parent=None):
super().__init__("NetworkingSetup", page_stack, main_window, parent)
self.btn_path = main_window.btn_path
self.update_status = main_window
self.manual_scripts_ready = False
self.button_next.setVisible(True)
self.button_next.clicked.connect(self.go_to_install)
self._setup_ui()
def _setup_ui(self):
header_icon = self._icon_label("networking_shield.png", 48, self)
header_icon.setGeometry(74, 66, 48, 48)
title = QLabel("Firewall & Managed-DNS Setup", self)
title.setGeometry(138, 68, 590, 44)
title.setStyleSheet("font-size: 26px; font-weight: bold; color: cyan;")
subtitle = QLabel(
"Optional privileged networking helpers",
self)
subtitle.setGeometry(140, 108, 560, 24)
subtitle.setStyleSheet("font-size: 14px; color: #d8ffff;")
description = QLabel(
"These firewall and managed-DNS helpers are separate bash scripts. "
"They stay outside the AppImage Python runtime, and if installed they are placed "
"in sudo-protected folders owned by root.",
self)
description.setGeometry(80, 152, 640, 72)
description.setWordWrap(True)
description.setStyleSheet("font-size: 15px; color: cyan;")
manual_panel = self._option_panel(74, 248, "Option 1", "Manual review")
manual_text = QLabel(
"Copy the scripts to Downloads, review them, then run sudo bash setup.sh yourself.",
manual_panel)
manual_text.setGeometry(24, 76, 252, 48)
manual_text.setWordWrap(True)
manual_text.setStyleSheet("font-size: 13px; color: #d8ffff;")
self.manual_button = QPushButton("Get scripts", manual_panel)
self.manual_button.setGeometry(24, 132, 252, 38)
self.manual_button.setStyleSheet(self._button_style("#007AFF", "#0056CC"))
self.manual_button.clicked.connect(self.handle_manual_setup)
auto_panel = self._option_panel(426, 248, "Option 2", "Automated installer")
auto_text = QLabel(
"Let HydraVeil run the installer, then test that the scripts reached the sudo folder.",
auto_panel)
auto_text.setGeometry(24, 76, 252, 48)
auto_text.setWordWrap(True)
auto_text.setStyleSheet("font-size: 13px; color: #d8ffff;")
self.auto_button = QPushButton("Automated installer", auto_panel)
self.auto_button.setGeometry(24, 132, 252, 38)
self.auto_button.setStyleSheet(self._button_style("#16a085", "#117a65"))
self.auto_button.clicked.connect(self.install_sudo_scripts_automatically)
self.status_label = QLabel("Choose manual or automated setup, or press Next to skip and continue.", self)
self.status_label.setGeometry(82, 462, 636, 38)
self.status_label.setWordWrap(True)
self.status_label.setStyleSheet("font-size: 13px; color: #d8ffff;")
def _option_panel(self, x, y, label, title):
panel = QWidget(self)
panel.setObjectName("networkingOptionPanel")
panel.setGeometry(x, y, 300, 186)
panel.setStyleSheet("""
QWidget#networkingOptionPanel {
background-color: #17323d;
border: 1px solid #2c7a8d;
border-radius: 6px;
}
""")
option_label = QLabel(label, panel)
option_label.setGeometry(24, 20, 120, 20)
option_label.setStyleSheet("font-size: 11px; font-weight: bold; color: #8de9ff; border: none;")
title_label = QLabel(title, panel)
title_label.setGeometry(24, 42, 240, 28)
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: cyan; border: none;")
return panel
def _icon_label(self, icon_name, size, parent):
label = QLabel(parent)
icon_path = os.path.join(self.btn_path, icon_name)
label.setPixmap(QPixmap(icon_path).scaled(
size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
label.setFixedSize(size, size)
label.setStyleSheet("border: none;")
return label
def _button_style(self, background, hover):
return f"""
QPushButton {{
background: {background};
color: #f4ffff;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
padding: 4px 8px;
}}
QPushButton:hover {{
background: {hover};
}}
QPushButton:disabled {{
background: #3f4954;
color: #94a3ad;
}}
"""
def _set_status(self, message, color="#d8ffff"):
self.status_label.setText(message)
self.status_label.setStyleSheet(f"font-size: 13px; color: {color};")
self.update_status.update_status(message)
def _result_is_valid(self, result: Result):
if hasattr(result, "valid"):
return result.valid
if result is None:
return True
return bool(result)
def _result_message(self, result: Result, fallback):
return getattr(result, "message", fallback)
def handle_manual_setup(self):
if self.manual_scripts_ready:
self.confirm_sudo_scripts()
return
results = sudo_assets_folder_setup()
if not self._result_is_valid(results):
self._set_status(
f"Could not copy scripts: {self._result_message(results, 'Unknown error')}", "#ff6b6b")
return
self.manual_scripts_ready = True
self.manual_button.setText("Confirm manual install")
self._set_status(
"Copied to Downloads/hydraveil_sudo_scripts. Review them, run 'sudo bash setup.sh', then click Confirm manual install.", "#d8ffff")
def confirm_sudo_scripts(self):
confirmed = test_if_in_sudo_folder()
if confirmed.valid:
self._set_status("Confirmed it worked. Continuing to prerequisite installation.", "#2ecc71")
QTimer.singleShot(600, self.go_to_install)
else:
self._set_status(
f"I'm sorry it did not work, {confirmed.message}", "#ff6b6b")
def install_sudo_scripts_automatically(self):
results = auto_install_sudo_script()
if results.valid:
confirmed = test_if_in_sudo_folder()
if confirmed.valid:
self._set_status("The setup script ran successfully. Continuing to prerequisite installation.", "#2ecc71")
QTimer.singleShot(600, self.go_to_install)
else:
self._set_status(
f"I'm sorry it did not work, {confirmed.message}", "#ff6b6b")
else:
self._set_status(
f"Setup script did NOT work because {results.message}", "#ff6b6b")
def go_to_install(self):
self.custom_window.navigator.navigate("install_system_package")
install_page = self.custom_window.navigator.get_cached("install_system_package")
if install_page is not None:
install_page.configure(package_name='all', distro='debian')
def back_to_welcome(self):
self.custom_window.navigator.navigate("welcome")

View file

@ -31,6 +31,7 @@ from core.Errors import (
PolicyRevocationError,
)
from core.errors.logger import logger as core_logger
from core.services.networking.systemwide.systemwide_utils import get_firewall_setting
from gui.v2.actions import settings_data
from gui.v2.actions.key_interpretation import interpret_key_results
@ -102,6 +103,8 @@ class Settings(Page):
("Create/Edit", self.show_registrations_page),
("Verification", self.show_verification_page),
("Legacy-Version", self.show_systemwide_page),
("Bwrap Permission", self.show_bwrap_page),
("Connections", self.show_connection_page),
("Delete Profile", self.show_delete_page),
("Error Logs", self.show_logs_page),
("Debug Help", self.show_debug_page)
@ -1223,6 +1226,14 @@ class Settings(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.connection_page)
self.connection_page = self.create_connection_page()
self.content_layout.addWidget(self.connection_page)
self.content_layout.removeWidget(self.delete_page)
self.delete_page = self.create_delete_page()
self.content_layout.addWidget(self.delete_page)
@ -1436,6 +1447,8 @@ class Settings(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.connection_page = self.create_connection_page()
self.logs_page = self.create_logs_page()
self.delete_page = self.create_delete_page()
self.debug_page = self.create_debug_page()
@ -1446,6 +1459,8 @@ class Settings(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.connection_page)
self.content_layout.addWidget(self.logs_page)
self.content_layout.addWidget(self.delete_page)
self.content_layout.addWidget(self.debug_page)
@ -1499,6 +1514,16 @@ class Settings(Page):
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_connection_page(self):
core_logger.info("User navigated to Settings -> Connection")
self.content_layout.setCurrentWidget(self.connection_page)
self._select_menu_button("Connection Page")
def show_logs_page(self):
core_logger.info("User navigated to Settings -> Error Logs")
self.content_layout.setCurrentWidget(self.logs_page)
@ -1981,6 +2006,59 @@ class Settings(Page):
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()
@ -2020,6 +2098,83 @@ class Settings(Page):
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 load_firewall_settings(self, enabled=None):
if enabled is None:
enabled = settings_data.load(read_firewall_setting)
self.firewall_toggle.setChecked(enabled)
if enabled:
self.firewall_status_value.setText("Enabled")
self.firewall_status_value.setStyleSheet(
f"color: #2ecc71; font-size: 14px; {self.font_style}")
else:
self.firewall_status_value.setText("Disabled")
self.firewall_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 save_firewall_settings(self):
enable = self.firewall_toggle.isChecked()
try:
if enable:
ConfigurationController.change_firewall(True)
self.firewall_status_value.setText("Enabled")
else:
ConfigurationController.change_firewall(False)
self.firewall_status_value.setText("Disabled")
self.update_status.update_status(
"Firewall settings updated")
except CommandNotFoundError as e:
self.firewall_status_value.setText(str(e))
self.firewall_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
except Exception as e:
print(e)
self.firewall_status_value.setText(f"Failed to update firewall setting {e}")
self.firewall_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
def load_registrations_settings(self) -> None:
try:
config = self._prepared_value(
@ -2081,3 +2236,58 @@ class Settings(Page):
logging.error(f"Error saving registration settings: {str(e)}")
self.update_status.update_status(
"Error saving registration settings")
def create_connection_page(self):
page = QWidget()
layout = QVBoxLayout(page)
layout.setSpacing(20)
layout.setContentsMargins(20, 20, 20, 20)
title = QLabel("CONNECTION PAGE")
title.setStyleSheet(
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
layout.addWidget(title)
description = QLabel(
"Control connection settings such as the firewall.")
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.firewall_status_value = QLabel("")
self.firewall_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
status_layout.addWidget(status_label)
status_layout.addWidget(self.firewall_status_value)
status_layout.addStretch()
layout.addLayout(status_layout)
toggle_layout = QHBoxLayout()
self.firewall_toggle = QCheckBox("Enable Firewall Automatically when Systemwide Profiles are Activated")
self.firewall_toggle.setStyleSheet(self.get_checkbox_style())
toggle_layout.addWidget(self.firewall_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_firewall_settings)
button_layout = QHBoxLayout()
button_layout.addWidget(save_button)
button_layout.addStretch()
layout.addLayout(button_layout)
layout.addStretch()
self.load_firewall_settings(
self._prepared_value("firewall_setting", settings_data.read_firewall_setting))
return page

View file

@ -13,7 +13,7 @@ class WelcomePage(Page):
self.btn_path = main_window.btn_path
self.update_status = main_window
self.ui_elements = []
self.button_next.clicked.connect(self.go_to_install)
self.button_next.clicked.connect(self.go_to_networking_setup)
self.button_next.setVisible(True)
self._setup_welcome_ui()
self._setup_stats_display()
@ -30,7 +30,7 @@ class WelcomePage(Page):
welcome_msg = QLabel(
"Before we begin your journey, we need to set up a few essential components. "
"Click 'Next' to take you to the installation page.", self)
"Click 'Next' to review the optional networking setup.", self)
welcome_msg.setGeometry(40, 100, 720, 80)
welcome_msg.setWordWrap(True)
welcome_msg.setStyleSheet("""
@ -95,7 +95,7 @@ class WelcomePage(Page):
"font-size: 16px; font-weight: bold; color: #2c3e50;")
title_layout.addWidget(title_label)
status_indicator = QLabel("")
status_indicator = QLabel(chr(9679))
status_indicator.setStyleSheet("color: #2ecc71; font-size: 16px;")
title_layout.addWidget(status_indicator)
@ -117,8 +117,5 @@ class WelcomePage(Page):
grid_layout.addWidget(stat_widget, row, col)
def go_to_install(self):
self.custom_window.navigator.navigate("install_system_package")
install_page = self.custom_window.navigator.get_cached("install_system_package")
if install_page is not None:
install_page.configure(package_name='all', distro='debian')
def go_to_networking_setup(self):
self.custom_window.navigator.navigate("networking_setup")

View file

@ -56,7 +56,7 @@ class DatabaseConflictDialog(QDialog):
# First which messages to display,
if check["status"] == "version_mismatch":
error_title = "Database Compatibility"
error_title = "Upgrade Time!"
error_subtitle = "Your App version and Database don't match"
else:
error_title = "Database Error"

View file

@ -0,0 +1,10 @@
from PyQt6.QtWidgets import QApplication, QMessageBox
import sys
def show_error(error_text, title="Error"):
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
QMessageBox.critical(None, title, error_text)
app.quit()

View file

@ -10,6 +10,8 @@ from core.controllers.tickets.UseTicketController import (
)
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.models.Result import Result, ResultError
from core.errors.exceptions import SudoScript, MissingPreReqs
from core.Errors import (
CommandNotFoundError,
EndpointVerificationError,
@ -114,6 +116,10 @@ class Worker(QObject):
"PROFILE_STATE_CONFLICT_ERROR", False, self.profile_data['id'], None, None)
except CommandNotFoundError as e:
self.update_signal.emit(str(e.subject), False, -1, None, None)
except SudoScript as e:
self.update_signal.emit(str(e), False, None, None, None)
except MissingPreReqs as e:
self.update_signal.emit(str(e), False, None, None, None)
except Exception as e:
print(e)
self.update_signal.emit(

View file

@ -14,7 +14,10 @@ from core.controllers.SubscriptionPlanController import SubscriptionPlanControll
from core.models.session.SessionConnection import SessionConnection
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemConnection import SystemConnection
from core.models.BaseProfile import ProfileType
from core.models.system.SystemProfile import SystemProfile
from core.errors.exceptions import SudoScript, MissingPreReqs
from core.models.Result import Result, ResultError
from gui.v2.infrastructure.setup_observers import (
client_observer,
@ -132,6 +135,10 @@ class WorkerThread(QThread):
ProfileController.disable(
profile, ignore=True, profile_observer=profile_observer)
self.text_output.emit("All profiles were successfully disabled")
# except SudoScript as e:
# self.text_output.emit(e)
# except MissingPreReqs as e:
# self.text_output.emit(e)
except Exception:
self.text_output.emit("An error occurred when disabling profile")
finally:
@ -189,12 +196,23 @@ class WorkerThread(QThread):
resolution = self.profile_data['resolution']
connection = SessionConnection(connection_type, mask_connection)
profile = SessionProfile(
profile_id, name, None, location, resolution, application_version, connection)
id=profile_id,
name=name,
subscription=None,
type=ProfileType.SESSION,
location=location,
resolution=resolution,
application_version=application_version,
connection=connection)
elif profile_type == "system":
connection = SystemConnection(connection_type)
profile = SystemProfile(
profile_id, name, None, location, connection)
id=profile_id,
name=name,
type=ProfileType.SYSTEM,
subscription=None,
location=location,
connection=connection)
else:
self.text_output.emit(f"Invalid profile type: {profile_type}")
return
@ -217,6 +235,10 @@ class WorkerThread(QThread):
else:
self.text_output.emit(
f"No profile found with ID: {self.profile_data['id']}")
# except SudoScript as e:
# self.text_output.emit(str(e))
# except MissingPreReqs as e:
# self.text_output.emit(str(e))
except Exception as e:
self.text_output.emit("An error occurred when disabling profile")
finally:

View file

@ -1,6 +1,6 @@
[project]
name = "sp-hydra-veil-gui"
version = "2.4.6"
version = "2.4.7"
authors = [
{ name = "Simplified Privacy" },
]
@ -12,7 +12,7 @@ classifiers = [
"Operating System :: POSIX :: Linux",
]
dependencies = [
"sp-hydra-veil-core == 2.3.8",
"sp-hydra-veil-core == 2.4.3",
"pyperclip ~= 1.9.0",
"pyqt6 ~= 6.7.1",
"qrcode[pil] ~= 8.2"