"Add database backup schedule for 2025-05-29 18:58:34"

This commit is contained in:
2025-05-29 19:00:12 +02:00
parent 122551df3d
commit cf297e8e16
7 changed files with 335 additions and 395 deletions

View File

@@ -4361,6 +4361,87 @@ def export_admin_logs():
"message": f"Fehler beim Exportieren: {str(e)}" "message": f"Fehler beim Exportieren: {str(e)}"
}), 500 }), 500
@app.route('/api/logs', methods=['GET'])
@login_required
def get_system_logs():
"""API-Endpunkt zum Laden der System-Logs für das Dashboard."""
if not current_user.is_admin:
return jsonify({"success": False, "error": "Berechtigung verweigert"}), 403
try:
import os
from datetime import datetime
log_level = request.args.get('log_level', 'all')
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
# Logeinträge sammeln
app_logs = []
for category in ['app', 'auth', 'jobs', 'printers', 'scheduler', 'errors']:
log_file = os.path.join(log_dir, category, f'{category}.log')
if os.path.exists(log_file):
try:
with open(log_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Nur die letzten 100 Zeilen pro Datei
for line in lines[-100:]:
line = line.strip()
if not line:
continue
# Log-Level-Filter anwenden
if log_level != 'all':
if log_level.upper() not in line:
continue
# Log-Eintrag parsen
parts = line.split(' - ')
if len(parts) >= 3:
timestamp = parts[0]
level = parts[1]
message = ' - '.join(parts[2:])
else:
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
level = 'INFO'
message = line
app_logs.append({
'timestamp': timestamp,
'level': level,
'category': category,
'module': category,
'message': message,
'source': category
})
except Exception as file_error:
app_logger.warning(f"Fehler beim Lesen der Log-Datei {log_file}: {str(file_error)}")
continue
# Nach Zeitstempel sortieren (neueste zuerst)
try:
logs = sorted(app_logs, key=lambda x: x['timestamp'] if x['timestamp'] else '', reverse=True)[:100]
except:
# Falls Sortierung fehlschlägt, einfach die letzten 100 nehmen
logs = app_logs[-100:]
app_logger.info(f"Logs erfolgreich geladen: {len(logs)} Einträge")
return jsonify({
"success": True,
"logs": logs,
"count": len(logs),
"message": f"{len(logs)} Log-Einträge geladen"
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der Logs: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Laden der Logs",
"message": str(e),
"logs": []
}), 500
# ===== ENDE FEHLENDE ADMIN-API-ENDPUNKTE ===== # ===== ENDE FEHLENDE ADMIN-API-ENDPUNKTE =====
# ===== BENACHRICHTIGUNGS-API-ENDPUNKTE ===== # ===== BENACHRICHTIGUNGS-API-ENDPUNKTE =====

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -131,21 +131,29 @@ def schedule_maintenance():
""" """
def maintenance_worker(): def maintenance_worker():
time.sleep(300) # 5 Minuten warten time.sleep(300) # 5 Minuten warten
try: while True:
with get_maintenance_session() as session: try:
# WAL-Checkpoint ausführen with get_maintenance_session() as session:
session.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")) # WAL-Checkpoint ausführen (aggressive Strategie)
checkpoint_result = session.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone()
# Statistiken aktualisieren # Nur loggen wenn tatsächlich Daten übertragen wurden
session.execute(text("ANALYZE")) if checkpoint_result and checkpoint_result[1] > 0:
logger.info(f"WAL-Checkpoint: {checkpoint_result[1]} Seiten übertragen, {checkpoint_result[2]} Seiten zurückgesetzt")
# Incremental Vacuum # Statistiken aktualisieren (alle 30 Minuten)
session.execute(text("PRAGMA incremental_vacuum")) session.execute(text("ANALYZE"))
session.commit() # Incremental Vacuum (alle 60 Minuten)
logger.info("Datenbank-Wartung erfolgreich durchgeführt") session.execute(text("PRAGMA incremental_vacuum"))
except Exception as e:
logger.error(f"Fehler bei Datenbank-Wartung: {str(e)}") session.commit()
except Exception as e:
logger.error(f"Fehler bei Datenbank-Wartung: {str(e)}")
# Warte 30 Minuten bis zur nächsten Wartung
time.sleep(1800)
# Wartung in separatem Thread ausführen # Wartung in separatem Thread ausführen
maintenance_thread = threading.Thread(target=maintenance_worker, daemon=True) maintenance_thread = threading.Thread(target=maintenance_worker, daemon=True)

View File

@@ -1,14 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Umfassendes Datenbank-Schema-Migrationsskript Optimiertes Datenbank-Schema-Migrationsskript
Erkennt und fügt alle fehlenden Spalten basierend auf den Models hinzu. Mit WAL-Checkpoint und ordnungsgemäßer Ressourcenverwaltung
""" """
import os import os
import sys import sys
import sqlite3 import sqlite3
import signal
import time
from datetime import datetime from datetime import datetime
import logging import logging
from contextlib import contextmanager
# Pfad zur App hinzufügen - KORRIGIERT # Pfad zur App hinzufügen - KORRIGIERT
app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -32,408 +35,256 @@ except ImportError:
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("schema_migration") logger = logging.getLogger("schema_migration")
def get_table_columns(cursor, table_name): # Globale Variable für sauberes Shutdown
"""Ermittelt alle Spalten einer Tabelle.""" _migration_running = False
cursor.execute(f"PRAGMA table_info({table_name})") _current_connection = None
return {row[1]: row[2] for row in cursor.fetchall()} # {column_name: column_type}
def get_table_exists(cursor, table_name): def signal_handler(signum, frame):
"""Prüft, ob eine Tabelle existiert.""" """Signal-Handler für ordnungsgemäßes Shutdown"""
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,)) global _migration_running, _current_connection
return cursor.fetchone() is not None print(f"\n🛑 Signal {signum} empfangen - beende Migration sauber...")
_migration_running = False
def migrate_users_table(cursor): if _current_connection:
"""Migriert die users Tabelle für fehlende Spalten."""
logger.info("Migriere users Tabelle...")
if not get_table_exists(cursor, 'users'):
logger.warning("users Tabelle existiert nicht - wird bei init_db erstellt")
return False
existing_columns = get_table_columns(cursor, 'users')
# Definition der erwarteten Spalten
required_columns = {
'id': 'INTEGER PRIMARY KEY',
'email': 'VARCHAR(120) UNIQUE NOT NULL',
'username': 'VARCHAR(100) UNIQUE NOT NULL',
'password_hash': 'VARCHAR(128) NOT NULL',
'name': 'VARCHAR(100) NOT NULL',
'role': 'VARCHAR(20) DEFAULT "user"',
'active': 'BOOLEAN DEFAULT 1',
'created_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
'last_login': 'DATETIME',
'updated_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
'settings': 'TEXT',
'department': 'VARCHAR(100)',
'position': 'VARCHAR(100)',
'phone': 'VARCHAR(50)',
'bio': 'TEXT'
}
migrations_performed = []
for column_name, column_def in required_columns.items():
if column_name not in existing_columns:
try:
# Spezielle Behandlung für updated_at mit Trigger
if column_name == 'updated_at':
cursor.execute(f"ALTER TABLE users ADD COLUMN {column_name} DATETIME DEFAULT CURRENT_TIMESTAMP")
# Trigger für automatische Aktualisierung
cursor.execute("""
CREATE TRIGGER IF NOT EXISTS update_users_updated_at
AFTER UPDATE ON users
BEGIN
UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END
""")
logger.info(f"Spalte '{column_name}' hinzugefügt mit Auto-Update-Trigger")
else:
cursor.execute(f"ALTER TABLE users ADD COLUMN {column_name} {column_def}")
logger.info(f"Spalte '{column_name}' hinzugefügt")
migrations_performed.append(column_name)
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Spalte '{column_name}': {str(e)}")
return len(migrations_performed) > 0
def migrate_printers_table(cursor):
"""Migriert die printers Tabelle für fehlende Spalten."""
logger.info("Migriere printers Tabelle...")
if not get_table_exists(cursor, 'printers'):
logger.warning("printers Tabelle existiert nicht - wird bei init_db erstellt")
return False
existing_columns = get_table_columns(cursor, 'printers')
required_columns = {
'id': 'INTEGER PRIMARY KEY',
'name': 'VARCHAR(100) NOT NULL',
'model': 'VARCHAR(100)',
'location': 'VARCHAR(100)',
'ip_address': 'VARCHAR(50)',
'mac_address': 'VARCHAR(50) NOT NULL UNIQUE',
'plug_ip': 'VARCHAR(50) NOT NULL',
'plug_username': 'VARCHAR(100) NOT NULL',
'plug_password': 'VARCHAR(100) NOT NULL',
'status': 'VARCHAR(20) DEFAULT "offline"',
'active': 'BOOLEAN DEFAULT 1',
'created_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
'last_checked': 'DATETIME'
}
migrations_performed = []
for column_name, column_def in required_columns.items():
if column_name not in existing_columns:
try:
cursor.execute(f"ALTER TABLE printers ADD COLUMN {column_name} {column_def}")
logger.info(f"Spalte '{column_name}' zu printers hinzugefügt")
migrations_performed.append(column_name)
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Spalte '{column_name}' zu printers: {str(e)}")
return len(migrations_performed) > 0
def migrate_jobs_table(cursor):
"""Migriert die jobs Tabelle für fehlende Spalten."""
logger.info("Migriere jobs Tabelle...")
if not get_table_exists(cursor, 'jobs'):
logger.warning("jobs Tabelle existiert nicht - wird bei init_db erstellt")
return False
existing_columns = get_table_columns(cursor, 'jobs')
required_columns = {
'id': 'INTEGER PRIMARY KEY',
'name': 'VARCHAR(200) NOT NULL',
'description': 'VARCHAR(500)',
'user_id': 'INTEGER NOT NULL',
'printer_id': 'INTEGER NOT NULL',
'start_at': 'DATETIME',
'end_at': 'DATETIME',
'actual_end_time': 'DATETIME',
'status': 'VARCHAR(20) DEFAULT "scheduled"',
'created_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
'notes': 'VARCHAR(500)',
'material_used': 'FLOAT',
'file_path': 'VARCHAR(500)',
'owner_id': 'INTEGER',
'duration_minutes': 'INTEGER NOT NULL'
}
migrations_performed = []
for column_name, column_def in required_columns.items():
if column_name not in existing_columns:
try:
cursor.execute(f"ALTER TABLE jobs ADD COLUMN {column_name} {column_def}")
logger.info(f"Spalte '{column_name}' zu jobs hinzugefügt")
migrations_performed.append(column_name)
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Spalte '{column_name}' zu jobs: {str(e)}")
return len(migrations_performed) > 0
def migrate_guest_requests_table(cursor):
"""Migriert die guest_requests Tabelle für fehlende Spalten."""
logger.info("Migriere guest_requests Tabelle...")
if not get_table_exists(cursor, 'guest_requests'):
logger.warning("guest_requests Tabelle existiert nicht - wird bei init_db erstellt")
return False
existing_columns = get_table_columns(cursor, 'guest_requests')
# Vollständige Definition aller erwarteten Spalten basierend auf dem GuestRequest Modell
required_columns = {
'id': 'INTEGER PRIMARY KEY',
'name': 'VARCHAR(100) NOT NULL',
'email': 'VARCHAR(120)',
'reason': 'TEXT',
'duration_min': 'INTEGER', # Bestehende Spalte für Backward-Kompatibilität
'duration_minutes': 'INTEGER', # Neue Spalte für API-Kompatibilität - HIER IST DAS PROBLEM!
'created_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
'status': 'VARCHAR(20) DEFAULT "pending"',
'printer_id': 'INTEGER',
'otp_code': 'VARCHAR(100)',
'job_id': 'INTEGER',
'author_ip': 'VARCHAR(50)',
'otp_used_at': 'DATETIME',
'file_name': 'VARCHAR(255)',
'file_path': 'VARCHAR(500)',
'copies': 'INTEGER DEFAULT 1',
'processed_by': 'INTEGER',
'processed_at': 'DATETIME',
'approval_notes': 'TEXT',
'rejection_reason': 'TEXT',
'updated_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
'approved_at': 'DATETIME',
'rejected_at': 'DATETIME',
'approved_by': 'INTEGER',
'rejected_by': 'INTEGER',
'otp_expires_at': 'DATETIME',
'assigned_printer_id': 'INTEGER'
}
migrations_performed = []
for column_name, column_def in required_columns.items():
if column_name not in existing_columns:
try:
# Spezielle Behandlung für updated_at mit Trigger
if column_name == 'updated_at':
cursor.execute(f"ALTER TABLE guest_requests ADD COLUMN {column_name} {column_def}")
# Trigger für automatische Aktualisierung
cursor.execute("""
CREATE TRIGGER IF NOT EXISTS update_guest_requests_updated_at
AFTER UPDATE ON guest_requests
BEGIN
UPDATE guest_requests SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END
""")
logger.info(f"Spalte '{column_name}' zu guest_requests hinzugefügt mit Auto-Update-Trigger")
else:
cursor.execute(f"ALTER TABLE guest_requests ADD COLUMN {column_name} {column_def}")
logger.info(f"Spalte '{column_name}' zu guest_requests hinzugefügt")
migrations_performed.append(column_name)
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Spalte '{column_name}' zu guest_requests: {str(e)}")
# Wenn duration_minutes hinzugefügt wurde, kopiere Werte von duration_min
if 'duration_minutes' in migrations_performed:
try: try:
cursor.execute("UPDATE guest_requests SET duration_minutes = duration_min WHERE duration_minutes IS NULL") print("🔄 Führe WAL-Checkpoint durch...")
logger.info("Werte von duration_min zu duration_minutes kopiert") _current_connection.execute("PRAGMA wal_checkpoint(TRUNCATE)")
_current_connection.commit()
_current_connection.close()
print("✅ Datenbank ordnungsgemäß geschlossen")
except Exception as e: except Exception as e:
logger.error(f"Fehler beim Kopieren der duration_min Werte: {str(e)}") print(f"⚠️ Fehler beim Schließen: {e}")
return len(migrations_performed) > 0 print("🏁 Migration beendet")
sys.exit(0)
def create_missing_tables(cursor): # Signal-Handler registrieren
"""Erstellt fehlende Tabellen.""" signal.signal(signal.SIGINT, signal_handler)
logger.info("Prüfe auf fehlende Tabellen...") signal.signal(signal.SIGTERM, signal_handler)
# user_permissions Tabelle @contextmanager
if not get_table_exists(cursor, 'user_permissions'): def get_database_connection(timeout=30):
cursor.execute(""" """Context Manager für sichere Datenbankverbindung mit WAL-Optimierung"""
CREATE TABLE user_permissions ( global _current_connection
user_id INTEGER PRIMARY KEY, conn = None
can_start_jobs BOOLEAN DEFAULT 0,
needs_approval BOOLEAN DEFAULT 1,
can_approve_jobs BOOLEAN DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users (id)
)
""")
logger.info("Tabelle 'user_permissions' erstellt")
# notifications Tabelle
if not get_table_exists(cursor, 'notifications'):
cursor.execute("""
CREATE TABLE notifications (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
type VARCHAR(50) NOT NULL,
payload TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
read BOOLEAN DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users (id)
)
""")
logger.info("Tabelle 'notifications' erstellt")
# stats Tabelle
if not get_table_exists(cursor, 'stats'):
cursor.execute("""
CREATE TABLE stats (
id INTEGER PRIMARY KEY,
total_print_time INTEGER DEFAULT 0,
total_jobs_completed INTEGER DEFAULT 0,
total_material_used FLOAT DEFAULT 0.0,
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
logger.info("Tabelle 'stats' erstellt")
# system_logs Tabelle
if not get_table_exists(cursor, 'system_logs'):
cursor.execute("""
CREATE TABLE system_logs (
id INTEGER PRIMARY KEY,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
level VARCHAR(20) NOT NULL,
message VARCHAR(1000) NOT NULL,
module VARCHAR(100),
user_id INTEGER,
ip_address VARCHAR(50),
user_agent VARCHAR(500),
FOREIGN KEY (user_id) REFERENCES users (id)
)
""")
logger.info("Tabelle 'system_logs' erstellt")
def optimize_database(cursor):
"""Führt Datenbankoptimierungen durch."""
logger.info("Führe Datenbankoptimierungen durch...")
try: try:
# Indices für bessere Performance # Verbindung mit optimierten Einstellungen
cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)") conn = sqlite3.connect(
cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)") DATABASE_PATH,
cursor.execute("CREATE INDEX IF NOT EXISTS idx_jobs_user_id ON jobs(user_id)") timeout=timeout,
cursor.execute("CREATE INDEX IF NOT EXISTS idx_jobs_printer_id ON jobs(printer_id)") isolation_level=None # Autocommit aus für manuelle Transaktionen
cursor.execute("CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status)") )
cursor.execute("CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id)") _current_connection = conn
cursor.execute("CREATE INDEX IF NOT EXISTS idx_system_logs_timestamp ON system_logs(timestamp)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_guest_requests_status ON guest_requests(status)")
# Statistiken aktualisieren # WAL-Modus und Optimierungen
cursor.execute("ANALYZE") conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL") # Bessere Performance mit WAL
conn.execute("PRAGMA foreign_keys=ON")
conn.execute("PRAGMA busy_timeout=30000") # 30 Sekunden Timeout
conn.execute("PRAGMA wal_autocheckpoint=1000") # Automatischer Checkpoint alle 1000 Seiten
logger.info("Datenbankoptimierungen abgeschlossen") logger.info("Datenbankverbindung mit WAL-Optimierungen hergestellt")
yield conn
except Exception as e: except Exception as e:
logger.error(f"Fehler bei Datenbankoptimierungen: {str(e)}") logger.error(f"Datenbankverbindungsfehler: {e}")
if conn:
conn.rollback()
raise
finally:
if conn:
try:
# Kritisch: WAL-Checkpoint vor dem Schließen
logger.info("Führe finalen WAL-Checkpoint durch...")
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
conn.commit()
# Prüfe WAL-Status
wal_info = conn.execute("PRAGMA wal_checkpoint").fetchone()
if wal_info:
logger.info(f"WAL-Checkpoint: {wal_info[0]} Seiten übertragen, {wal_info[1]} Seiten zurückgesetzt")
conn.close()
logger.info("Datenbankverbindung ordnungsgemäß geschlossen")
except Exception as e:
logger.error(f"Fehler beim Schließen der Datenbankverbindung: {e}")
finally:
_current_connection = None
def force_wal_checkpoint():
"""Erzwingt WAL-Checkpoint um alle Daten in die Hauptdatei zu schreiben"""
try:
with get_database_connection(timeout=10) as conn:
# Aggressive WAL-Checkpoint-Strategien
strategies = [
("TRUNCATE", "Vollständiger Checkpoint mit WAL-Truncate"),
("RESTART", "Checkpoint mit WAL-Restart"),
("FULL", "Vollständiger Checkpoint")
]
for strategy, description in strategies:
try:
result = conn.execute(f"PRAGMA wal_checkpoint({strategy})").fetchone()
if result and result[0] == 0: # Erfolg
logger.info(f"{description} erfolgreich: {result}")
return True
else:
logger.warning(f"⚠️ {description} teilweise erfolgreich: {result}")
except Exception as e:
logger.warning(f"⚠️ {description} fehlgeschlagen: {e}")
continue
# Fallback: VACUUM für komplette Reorganisation
logger.info("Führe VACUUM als Fallback durch...")
conn.execute("VACUUM")
logger.info("✅ VACUUM erfolgreich")
return True
except Exception as e:
logger.error(f"Kritischer Fehler bei WAL-Checkpoint: {e}")
return False
def optimize_migration_performance():
"""Optimiert die Datenbank für die Migration"""
try:
with get_database_connection(timeout=5) as conn:
# Performance-Optimierungen für Migration
optimizations = [
("PRAGMA cache_size = -64000", "Cache-Größe auf 64MB erhöht"),
("PRAGMA temp_store = MEMORY", "Temp-Store in Memory"),
("PRAGMA mmap_size = 268435456", "Memory-Mapped I/O aktiviert"),
("PRAGMA optimize", "Automatische Optimierungen")
]
for pragma, description in optimizations:
try:
conn.execute(pragma)
logger.info(f"{description}")
except Exception as e:
logger.warning(f"⚠️ Optimierung fehlgeschlagen ({description}): {e}")
except Exception as e:
logger.warning(f"Fehler bei Performance-Optimierung: {e}")
def main(): def main():
"""Führt die komplette Schema-Migration aus.""" """Führt die optimierte Schema-Migration aus."""
try: global _migration_running
logger.info("Starte umfassende Datenbank-Schema-Migration...") _migration_running = True
# Verbindung zur Datenbank try:
logger.info("🚀 Starte optimierte Datenbank-Schema-Migration...")
# Überprüfe Datenbankdatei
if not os.path.exists(DATABASE_PATH): if not os.path.exists(DATABASE_PATH):
logger.error(f"Datenbankdatei nicht gefunden: {DATABASE_PATH}") logger.error(f"Datenbankdatei nicht gefunden: {DATABASE_PATH}")
# Erste Initialisierung return False
from models import init_database
logger.info("Führe Erstinitialisierung durch...")
init_database()
logger.info("Erstinitialisierung abgeschlossen")
return
conn = sqlite3.connect(DATABASE_PATH) # Initial WAL-Checkpoint um sauberen Zustand sicherzustellen
cursor = conn.cursor() logger.info("🔄 Führe initialen WAL-Checkpoint durch...")
force_wal_checkpoint()
# WAL-Modus aktivieren für bessere Concurrent-Performance # Performance-Optimierungen
cursor.execute("PRAGMA journal_mode=WAL") optimize_migration_performance()
cursor.execute("PRAGMA foreign_keys=ON")
logger.info(f"Verbunden mit Datenbank: {DATABASE_PATH}") # Eigentliche Migration mit optimierter Verbindung
with get_database_connection(timeout=60) as conn:
cursor = conn.cursor()
# Backup erstellen # Backup erstellen (mit Timeout)
backup_path = f"{DATABASE_PATH}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" backup_path = f"{DATABASE_PATH}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
cursor.execute(f"VACUUM INTO '{backup_path}'") try:
logger.info(f"Backup erstellt: {backup_path}") logger.info(f"📦 Erstelle Backup: {backup_path}")
cursor.execute(f"VACUUM INTO '{backup_path}'")
logger.info("✅ Backup erfolgreich erstellt")
except Exception as e:
logger.warning(f"⚠️ Backup-Erstellung fehlgeschlagen: {e}")
# Migrationen durchführen # Migrationen durchführen (verkürzt für bessere Performance)
migrations_performed = [] migrations_performed = []
# Fehlende Tabellen erstellen if not _migration_running:
create_missing_tables(cursor) return False
migrations_performed.append("missing_tables")
# Tabellen-spezifische Migrationen # Schnelle Schema-Checks
if migrate_users_table(cursor): try:
migrations_performed.append("users") # Test der kritischen Abfrage
cursor.execute("SELECT COUNT(*) FROM guest_requests WHERE duration_minutes IS NOT NULL")
logger.info("✅ Schema-Integritätstest bestanden")
except Exception:
logger.info("🔧 Führe kritische Schema-Reparaturen durch...")
if migrate_printers_table(cursor): # Nur die wichtigsten Reparaturen
migrations_performed.append("printers") critical_fixes = [
("ALTER TABLE guest_requests ADD COLUMN duration_minutes INTEGER", "duration_minutes zu guest_requests"),
("ALTER TABLE users ADD COLUMN username VARCHAR(100)", "username zu users"),
("UPDATE users SET username = email WHERE username IS NULL", "Username-Fallback")
]
if migrate_jobs_table(cursor): for sql, description in critical_fixes:
migrations_performed.append("jobs") if not _migration_running:
break
try:
cursor.execute(sql)
logger.info(f"{description}")
migrations_performed.append(description)
except sqlite3.OperationalError as e:
if "duplicate column" not in str(e).lower():
logger.warning(f"⚠️ {description}: {e}")
if migrate_guest_requests_table(cursor): # Commit und WAL-Checkpoint zwischen Operationen
migrations_performed.append("guest_requests") if migrations_performed:
conn.commit()
cursor.execute("PRAGMA wal_checkpoint(PASSIVE)")
# Optimierungen # Finale Optimierungen (reduziert)
optimize_database(cursor) if _migration_running:
migrations_performed.append("optimizations") essential_indices = [
"CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)",
"CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status)",
"CREATE INDEX IF NOT EXISTS idx_guest_requests_status ON guest_requests(status)"
]
# Änderungen speichern for index_sql in essential_indices:
conn.commit() try:
conn.close() cursor.execute(index_sql)
except Exception:
pass # Indices sind nicht kritisch
logger.info(f"Schema-Migration erfolgreich abgeschlossen. Migrierte Bereiche: {', '.join(migrations_performed)}") # Finale Statistiken
cursor.execute("ANALYZE")
migrations_performed.append("optimizations")
# Test der Migration # Finale Commit
test_migration() conn.commit()
logger.info(f"✅ Migration abgeschlossen. Bereiche: {', '.join(migrations_performed)}")
# Abschließender WAL-Checkpoint
logger.info("🔄 Führe abschließenden WAL-Checkpoint durch...")
force_wal_checkpoint()
# Kurze Pause um sicherzustellen, dass alle I/O-Operationen abgeschlossen sind
time.sleep(1)
logger.info("🎉 Optimierte Schema-Migration erfolgreich abgeschlossen!")
return True
except KeyboardInterrupt:
logger.info("🔄 Migration durch Benutzer unterbrochen")
return False
except Exception as e: except Exception as e:
logger.error(f"Fehler bei der Schema-Migration: {str(e)}") logger.error(f"❌ Kritischer Fehler bei der Migration: {str(e)}")
if 'conn' in locals(): return False
conn.rollback() finally:
conn.close() _migration_running = False
sys.exit(1) # Finale WAL-Bereinigung
try:
def test_migration(): force_wal_checkpoint()
"""Testet die Migration durch Laden der Models.""" except Exception:
try: pass
logger.info("Teste Migration durch Laden der Models...")
# Models importieren und testen
from models import get_cached_session, User, Printer, Job
with get_cached_session() as session:
# Test User-Query (sollte das updated_at Problem lösen)
users = session.query(User).limit(1).all()
logger.info(f"User-Abfrage erfolgreich - {len(users)} Benutzer gefunden")
# Test Printer-Query
printers = session.query(Printer).limit(1).all()
logger.info(f"Printer-Abfrage erfolgreich - {len(printers)} Drucker gefunden")
# Test Job-Query
jobs = session.query(Job).limit(1).all()
logger.info(f"Job-Abfrage erfolgreich - {len(jobs)} Jobs gefunden")
logger.info("Migrations-Test erfolgreich abgeschlossen")
except Exception as e:
logger.error(f"Fehler beim Migrations-Test: {str(e)}")
raise
if __name__ == "__main__": if __name__ == "__main__":
main() success = main()
if not success:
sys.exit(1)