New sync workflow. Sync goes to a new function. Forced DB Sync at startup with an empty database

This commit is contained in:
SimplifiedPrivacy 2026-08-05 02:47:52 -04:00
parent b502f9130d
commit 1d886728e0
5 changed files with 590 additions and 64 deletions

View file

@ -6,12 +6,15 @@ from core.Constants import Constants
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.models.manage.migrations import migrate_sql from core.models.manage.migrations import migrate_sql
from core.models.manage.clear_sql_model import clear_sql_model from core.models.manage.clear_sql_model import clear_sql_model
from core.controllers.SyncController import new_sync
from essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.ClientObserver import ClientObserver
# generic # generic
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from functools import partial
# ============================================================================ # ============================================================================
# UTIL FUNCTIONS AND RECOVERY UI # UTIL FUNCTIONS AND RECOVERY UI
@ -65,6 +68,43 @@ 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.") logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.")
sys.exit() sys.exit()
def emergency_sync():
from gui.v2.ui.popups.generic_choice import show_generic_options
from gui.v2.ui.popups.terminal_threading import show_terminal
option = show_generic_options(
title="Sync the Database",
option_one="Clearweb (fastest)",
option_two="Tor",
option_three="No, Exit."
)
if option == 3:
sys.exit()
client_observer = ClientObserver()
connection_observer = ConnectionObserver()
if option == 1 or option == 2:
# # setup the function to do:
do_this_function = partial(new_sync, client_observer, connection_observer)
sync_worked = show_terminal(
do_this_function=do_this_function,
connection_observer=connection_observer,
client_observer=client_observer
)
if sync_worked:
print("it worked")
# ============================================================================
# BEGIN PROGRAM
# ============================================================================
# ============================================================================ # ============================================================================
# ASSETS FOLDER # ASSETS FOLDER
# ============================================================================ # ============================================================================
@ -100,8 +140,28 @@ except:
# ============================================================================ # ============================================================================
migration_happened = False migration_happened = False
# ============================================================================
# ZERO DATABASE = SYNC NOW
# ============================================================================
if not main_db_exists:
made_tables = create_ALL_tables()
if made_tables:
# force sync:
emergency_sync()
# we don't have to worry about migrations, they just got a new DB
create_ONLY_db_version_table()
from gui.main_ui import start_ui
start_ui(force_sync=False)
sys.exit()
# ============================================================================
# LEGACY VERSION. MIGRATE
# ============================================================================
# are they upgrading from a legacy version? that would mean the table existed, but not the version table, # 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: if not version_table_exists:
logger.info(f"[DB MANAGEMENT] We are dealing with a legacy database, we need to transition the user.") logger.info(f"[DB MANAGEMENT] We are dealing with a legacy database, we need to transition the user.")
migration = migrate_sql() migration = migrate_sql()
@ -122,6 +182,7 @@ if not version_table_exists and main_db_exists:
if not version_table_exists: if not version_table_exists:
create_ONLY_db_version_table() create_ONLY_db_version_table()
# ============================================================================ # ============================================================================
# COMPARE DB VERISONS # COMPARE DB VERISONS
# ============================================================================ # ============================================================================
@ -131,66 +192,9 @@ is_compatable = compatability_dict.get("result", False)
logger.info(f"[DB MANAGEMENT] The result of the check is {is_compatable} and the reason is {reason}") logger.info(f"[DB MANAGEMENT] The result of the check is {is_compatable} and the reason is {reason}")
# ============================================================================ # ============================================================================
# IF THE VERSIONS MATCH # IF the versions do NOT match
# ============================================================================ # ============================================================================
if is_compatable: if not 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 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")
# UPDATE DATA
did_it_insert = insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS)
if not did_it_insert:
logger.error(f"[DB MANAGEMENT] Critical Failure with updating/inserting the new DB version into the database.")
# Load GUI either way:
from gui.main_ui import start_ui
# If the user lacks a database, we want to force them to sync the new data, to avoid NoneType errors when enabling existing profiles,
if reason == "no_database":
logger.info(f"[DB MANAGEMENT] User had no database to begin with, so now we're entering GUI with sync on..")
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)
# ============================================================================
# but IF the versions do NOT match
# ============================================================================
else:
logger.error(f"[DB MANAGEMENT] The App is expecting a different DB version than the real database. The reason is {reason}") logger.error(f"[DB MANAGEMENT] The App is expecting a different DB version than the real database. The reason is {reason}")
db_version_you_have = compatability_dict.get("old_db_version", "Error getting the Version") db_version_you_have = compatability_dict.get("old_db_version", "Error getting the Version")
@ -202,4 +206,57 @@ else:
# THEN DISPLAY CHOICES AND RECOVERY UI # THEN DISPLAY CHOICES AND RECOVERY UI
recovery_dialog(custom_error, db_version_you_have) recovery_dialog(custom_error, db_version_you_have)
# goodbye. exits.
# ============================================================================
# THE VERSIONS MATCH IF THEY ARE STILL HERE
# ============================================================================
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 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")
# UPDATE DATA
did_it_insert = insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS)
if not did_it_insert:
logger.error(f"[DB MANAGEMENT] Critical Failure with updating/inserting the new DB version into the database.")
# Load GUI either way:
from gui.main_ui import start_ui
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,24 @@
fields:
- name: id
path: ['id']
required: true
- name: application_code
path: ['application', 'code']
required: true
- name: version_number
path: ['version_number']
required: true
- name: format_revision
path: ['format_revision']
- name: download_path
path: ['download_path']
- name: released_at
path: ['released_at']
- name: file_hash
path: ['file_hash']

View file

@ -0,0 +1,163 @@
import sys
from PyQt6.QtWidgets import QApplication, QDialog, QWidget, QVBoxLayout, QLabel, QPushButton
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QColor
from PyQt6.QtWidgets import QGraphicsDropShadowEffect
class GenericOptionMenu(QDialog):
def __init__(self, title, option_one, option_two, option_three):
super().__init__()
self.selected_option = None
self.init_ui(title, option_one, option_two, option_three)
def init_ui(self, title, option_one, option_two, option_three):
self.setWindowTitle(title)
self.setGeometry(100, 100, 500, 600)
self.setModal(True)
# Main container
layout = QVBoxLayout(self)
layout.setSpacing(25)
layout.setContentsMargins(40, 50, 40, 50)
# Apply dark cyberpunk background
self.setStyleSheet("""
QDialog {
background-color: #0a0e27;
color: #00ff88;
}
""")
# Title Label
title_label = QLabel(title)
title_label.setFont(QFont("Courier New", 16, QFont.Weight.Bold))
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("""
QLabel {
color: #00ff88;
background-color: transparent;
padding: 20px;
border: 2px solid #00ff88;
border-radius: 5px;
letter-spacing: 2px;
}
""")
# Add glow effect to title
title_shadow = QGraphicsDropShadowEffect()
title_shadow.setBlurRadius(15)
title_shadow.setColor(QColor(0, 255, 136, 200))
title_shadow.setOffset(0, 0)
title_label.setGraphicsEffect(title_shadow)
layout.addWidget(title_label)
layout.addSpacing(30)
# Button 1
btn1 = self.create_button(option_one, 1)
layout.addWidget(btn1)
# Button 2
btn2 = self.create_button(option_two, 2)
layout.addWidget(btn2)
# Button 3
btn3 = self.create_button(option_three, 3)
layout.addWidget(btn3)
layout.addStretch()
# Status label
status_label = QLabel("▓░ READY FOR INPUT ░▓")
status_label.setFont(QFont("Courier New", 10))
status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
status_label.setStyleSheet("""
QLabel {
color: #ff0088;
background-color: transparent;
padding: 10px;
letter-spacing: 1px;
}
""")
layout.addWidget(status_label)
def create_button(self, text, button_num):
button = QPushButton(text)
button.setFont(QFont("Courier New", 12, QFont.Weight.Bold))
button.setMinimumHeight(60)
button.setCursor(Qt.CursorShape.PointingHandCursor)
# Color variations for each button
if button_num == 1:
neon_color = "#00ff88" # Cyan green
rgb_color = QColor(0, 255, 136)
elif button_num == 2:
neon_color = "#00d4ff" # Cyan blue
rgb_color = QColor(0, 212, 255)
else:
neon_color = "#ff0088" # Magenta
rgb_color = QColor(255, 0, 136)
button.setStyleSheet(f"""
QPushButton {{
background-color: #1a1f3a;
color: {neon_color};
border: 2px solid {neon_color};
border-radius: 8px;
padding: 10px 20px;
font-weight: bold;
letter-spacing: 2px;
}}
QPushButton:hover {{
background-color: {neon_color};
color: #0a0e27;
border: 2px solid {neon_color};
}}
QPushButton:pressed {{
background-color: #0a0e27;
border: 3px solid {neon_color};
}}
""")
# Add drop shadow glow effect
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(20)
shadow.setColor(rgb_color)
shadow.setOffset(0, 0)
button.setGraphicsEffect(shadow)
button.clicked.connect(lambda: self.on_button_clicked(button_num))
return button
def on_button_clicked(self, button_num):
self.selected_option = button_num
self.accept()
def show_generic_options(
title: str,
option_one: str,
option_two: str,
option_three: str
) -> int:
app = QApplication(sys.argv)
# Create the menu with custom title and options
menu = GenericOptionMenu(
title=title,
option_one=option_one,
option_two=option_two,
option_three=option_three
)
# Show the menu and get the result
result = menu.exec()
if result == QDialog.DialogCode.Accepted:
print(f"User selected option: {menu.selected_option}")
return menu.selected_option
else:
print("User cancelled")
return None
sys.exit(0)

View file

@ -0,0 +1,281 @@
from essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.ClientObserver import ClientObserver
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.controllers.SyncController import new_sync
from core.models.Result import Result, ResultError
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTextEdit,
QLabel, QPushButton, QApplication)
from PyQt6.QtGui import QFont, QColor, QTextCursor
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import QGraphicsDropShadowEffect
import sys
import traceback
from typing import Callable, Any
from PyQt6.QtGui import QTextCharFormat
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
import threading
from threading import Thread
# mainthread = ThreadSafetyTool()
class NotificationTerminalUI(QDialog):
def __init__(self, connection_observer: ConnectionObserver, client_observer: ClientObserver):
super().__init__()
self.mainthread = ThreadSafetyTool()
self.connection_observer = connection_observer
self.client_observer = client_observer
self.synchronized = False
self.worker_thread = None
self.task_success = False
self.connection_observer.subscribe("connecting",
lambda msg: self.add_notification("CONNECT", msg, "#00ff88"))
self.connection_observer.subscribe("tor_bootstrapping",
lambda msg: self.add_notification("BOOTSTRAP", msg, "#00d4ff"))
self.connection_observer.subscribe("tor_bootstrap_progressing",
lambda msg: self.add_notification("PROGRESS", msg, "#ff0088"))
self.connection_observer.subscribe("tor_bootstrapped",
lambda msg: self.add_notification("SUCCESS", msg, "#00ff88"))
self.connection_observer.subscribe("custom_message",
lambda msg: self.add_notification("INFO", msg, "#ffff00"))
# Subscribe to client_observer events
self.client_observer.subscribe("synchronizing",
lambda event: self.add_notification("SYNC",
event.subject if event.subject else "Sync in progress...", "#00d4ff"))
self.client_observer.subscribe("synchronized",
lambda event: self.on_synchronized())
self.client_observer.subscribe("updating",
lambda event: self.add_notification("UPDATE", "Updating client...", "#ff0088"))
self.client_observer.subscribe("update_progressing",
lambda event: self.add_notification("PROGRESS",
f'Current progress: {event.meta.get("progress", 0):.2f}%', "#ffff00"))
self.client_observer.subscribe("updated",
lambda event: self.add_notification("COMPLETE",
"Restart client to apply update.", "#00ff88"))
self.client_observer.subscribe("custom_message",
lambda event: self.add_notification("ERROR",
event.subject if event.subject else "Error, check logs", "#ff0088"))
self.init_ui()
def init_ui(self):
self.setWindowTitle("SYNC PROCESS")
self.setGeometry(100, 100, 700, 600)
self.setModal(True)
layout = QVBoxLayout(self)
layout.setSpacing(15)
layout.setContentsMargins(30, 30, 30, 30)
self.setStyleSheet("""
QDialog {
background-color: #0a0e27;
color: #00ff88;
}
""")
# Title Label
title_label = QLabel("▓░ NOTIFICATION TERMINAL ░▓")
title_label.setFont(QFont("Courier New", 14, QFont.Weight.Bold))
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("""
QLabel {
color: #00ff88;
background-color: transparent;
padding: 15px;
border: 2px solid #00ff88;
border-radius: 5px;
letter-spacing: 2px;
}
""")
title_shadow = QGraphicsDropShadowEffect()
title_shadow.setBlurRadius(15)
title_shadow.setColor(QColor(0, 255, 136, 200))
title_shadow.setOffset(0, 0)
title_label.setGraphicsEffect(title_shadow)
layout.addWidget(title_label)
# Scrollable text display
self.output_text = QTextEdit()
self.output_text.setReadOnly(True)
self.output_text.setFont(QFont("Courier New", 10))
self.output_text.setMinimumHeight(400)
self.output_text.setStyleSheet("""
QTextEdit {
background-color: #1a1f3a;
color: #00ff88;
border: 2px solid #00ff88;
border-radius: 5px;
padding: 15px;
font-size: 16pt;
font-family: 'Courier New', monospace;
selection-background-color: #ff0088;
}
QScrollBar:vertical {
background-color: #0a0e27;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #00ff88;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #00d4ff;
}
""")
layout.addWidget(self.output_text)
# Status label
self.status_label = QLabel("◄ WAITING FOR TASK START ►")
self.status_label.setFont(QFont("Courier New", 9))
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet("""
QLabel {
color: #ff0088;
background-color: transparent;
padding: 8px;
letter-spacing: 1px;
}
""")
layout.addWidget(self.status_label)
# Button layout
button_layout = QHBoxLayout()
clear_btn = QPushButton("CLEAR")
clear_btn.setFont(QFont("Courier New", 10, QFont.Weight.Bold))
clear_btn.setMinimumHeight(40)
clear_btn.setCursor(Qt.CursorShape.PointingHandCursor)
clear_btn.setStyleSheet("""
QPushButton {
background-color: #1a1f3a;
color: #00d4ff;
border: 2px solid #00d4ff;
border-radius: 5px;
padding: 8px 20px;
font-weight: bold;
}
QPushButton:hover {
background-color: #00d4ff;
color: #0a0e27;
}
QPushButton:pressed {
background-color: #0a0e27;
border: 3px solid #00d4ff;
}
""")
clear_btn.clicked.connect(self.clear_output)
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(15)
shadow.setColor(QColor(0, 212, 255))
shadow.setOffset(0, 0)
clear_btn.setGraphicsEffect(shadow)
button_layout.addWidget(clear_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
def add_notification(self, prefix: str, message: str, color: str = "#00ff88"):
"""Queue to main thread safely."""
self.mainthread(lambda: self._add_notification_impl(prefix, message, color))()
def _add_notification_impl(self, prefix: str, message: str, color: str):
"""Actual implementation on main thread."""
cursor = self.output_text.textCursor()
cursor.movePosition(cursor.MoveOperation.End)
fmt = QTextCharFormat()
fmt.setForeground(QColor(color))
cursor.insertText(f"[{prefix}] {message}\n", fmt)
self.output_text.setTextCursor(cursor)
self.output_text.ensureCursorVisible()
def set_status(self, text: str):
"""This runs on the main thread, safely."""
self.status_label.setText(text)
def on_synchronized(self):
"""Handle synchronized notification and exit."""
self.add_notification("SYNC", "Sync complete ✓", "#00ff88")
self.set_status("◄ SYNCHRONIZED - CLOSING ►")
self.synchronized = True
self.task_success = True
QTimer.singleShot(500, self.accept)
def run_task(self, task_func: Callable):
"""
Purpose:
Insert any function for a pure python background thread.
Which then pushes to the main UI via ThreadSafety
"""
if self.worker_thread is not None and self.worker_thread.is_alive():
self.add_notification("SYSTEM", "Task already running!", "#ff0088")
return
self.set_status("◄ TASK RUNNING ►")
self.add_notification("SYSTEM", "Background task started", "#00d4ff")
# Start in pure Python thread (daemon=True so it doesn't block exit)
self.worker_thread = Thread(target=self._run_task_wrapper, args=(task_func,), daemon=True)
self.worker_thread.start()
def _run_task_wrapper(self, task_func: Callable):
"""Wrapper that runs task and handles errors."""
try:
task_func()
self.task_success = True
except Exception as e:
self.add_notification("ERROR", f"Task failed: {str(e)}", "#ff0088")
self.set_status("◄ TASK FAILED ►")
self.task_success = False
def clear_output(self):
"""Clear the terminal output."""
self.output_text.clear()
self.add_notification("SYSTEM", "Terminal cleared", "#00d4ff")
def get_result(self) -> bool:
"""Return True if synchronized, False otherwise."""
return self.synchronized and self.task_success
def closeEvent(self, event):
"""Ensure thread is properly cleaned up on close."""
if self.worker_thread is not None and self.worker_thread.is_alive():
self.worker_thread.join(timeout=1)
event.accept()
def show_terminal(
do_this_function: Callable[[Any, Any], None],
connection_observer: ConnectionObserver,
client_observer: ClientObserver
) -> bool:
app = QApplication.instance() or QApplication(sys.argv)
# Create the UI
dialog = NotificationTerminalUI(connection_observer=connection_observer, client_observer=client_observer)
# Start the background task
dialog.run_task(do_this_function)
# Show dialog and wait
dialog.exec()
# Return success
return dialog.get_result()

View file

@ -5,7 +5,9 @@ from PyQt6.QtCore import QThread, pyqtSignal
from core.controllers.ApplicationVersionController import ApplicationVersionController from core.controllers.ApplicationVersionController import ApplicationVersionController
from core.controllers.ClientController import ClientController from core.controllers.ClientController import ClientController
from core.controllers.ConfigurationController import ConfigurationController from core.controllers.ConfigurationController import ConfigurationController, ConnectionChoice
from core.controllers.SyncController import new_sync
from core.controllers.InvoiceController import InvoiceController from core.controllers.InvoiceController import InvoiceController
from core.controllers.LocationController import LocationController from core.controllers.LocationController import LocationController
from core.controllers.ProfileController import ProfileController from core.controllers.ProfileController import ProfileController
@ -98,8 +100,7 @@ class WorkerThread(QThread):
def check_for_update(self): def check_for_update(self):
self.text_output.emit("Checking for updates...") self.text_output.emit("Checking for updates...")
ClientController.sync(client_observer=client_observer, new_sync(client_observer=client_observer, connection_observer=connection_observer)
connection_observer=connection_observer)
update_available = ClientController.can_be_updated() update_available = ClientController.can_be_updated()
if update_available: if update_available:
self.text_output.emit("An update is available. Downloading...") self.text_output.emit("An update is available. Downloading...")