🎉 Verbesserte Backend-Funktionalität durch Windows-sichere Disk-Usage-Bestimmung, Uptime-Berechnung und Einführung eines Kiosk-Timers. Dokumentation aktualisiert und nicht mehr benötigte Dateien entfernt. 🧹

This commit is contained in:
2025-06-01 03:00:04 +02:00
parent 486647fade
commit 8969cf6df6
70 changed files with 89065 additions and 85009 deletions

View File

@ -2,7 +2,7 @@ import os
import logging
import threading
import time
from datetime import datetime
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
from contextlib import contextmanager
@ -43,7 +43,7 @@ _cache_lock = threading.Lock()
_cache_ttl = {} # Time-to-live für Cache-Einträge
# Alle exportierten Modelle
__all__ = ['User', 'Printer', 'Job', 'Stats', 'SystemLog', 'Base', 'GuestRequest', 'UserPermission', 'Notification', 'JobOrder', 'init_db', 'init_database', 'create_initial_admin', 'get_db_session', 'get_cached_session', 'clear_cache', 'engine']
__all__ = ['User', 'Printer', 'Job', 'Stats', 'SystemLog', 'Base', 'GuestRequest', 'UserPermission', 'Notification', 'JobOrder', 'SystemTimer', 'init_db', 'init_database', 'create_initial_admin', 'get_db_session', 'get_cached_session', 'clear_cache', 'engine']
# ===== DATENBANK-KONFIGURATION MIT WAL UND OPTIMIERUNGEN =====
@ -968,227 +968,608 @@ class JobOrder(Base):
# Eindeutige Kombination: Ein Job kann nur eine Position pro Drucker haben
__table_args__ = (
# Sicherstellen, dass jeder Job nur einmal pro Drucker existiert
# und jede Position pro Drucker nur einmal vergeben wird
# Hier könnten Constraints definiert werden
)
def to_dict(self) -> dict:
"""
Konvertiert JobOrder zu Dictionary.
"""
cache_key = get_cache_key("JobOrder", f"{self.printer_id}_{self.job_id}", "dict")
cached_result = get_cache(cache_key)
if cached_result is not None:
return cached_result
result = {
return {
"id": self.id,
"printer_id": self.printer_id,
"job_id": self.job_id,
"order_position": self.order_position,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"last_modified_by": self.last_modified_by
"last_modified_by": self.last_modified_by,
"printer": self.printer.to_dict() if self.printer else None,
"job": self.job.to_dict() if self.job else None
}
# Ergebnis cachen (2 Minuten)
set_cache(cache_key, result, 120)
return result
@classmethod
def get_order_for_printer(cls, printer_id: int) -> List['JobOrder']:
"""
Holt die Job-Reihenfolge für einen bestimmten Drucker.
"""
cache_key = get_cache_key("JobOrder", printer_id, "printer_order")
cached_orders = get_cache(cache_key)
cached_order = get_cache(cache_key)
if cached_orders is not None:
return cached_orders
if cached_order is not None:
return cached_order
with get_cached_session() as session:
orders = session.query(cls).filter(
order = session.query(cls).filter(
cls.printer_id == printer_id
).order_by(cls.order_position).all()
).order_by(cls.order_position.asc()).all()
# Ergebnis cachen (1 Minute für häufige Abfragen)
set_cache(cache_key, orders, 60)
return orders
# Ergebnis für 5 Minuten cachen
set_cache(cache_key, order, 300)
return order
@classmethod
def update_printer_order(cls, printer_id: int, job_ids: List[int],
modified_by_user_id: int = None) -> bool:
"""
Aktualisiert die komplette Job-Reihenfolge für einen Drucker.
Args:
printer_id: ID des Druckers
job_ids: Liste der Job-IDs in der gewünschten Reihenfolge
modified_by_user_id: ID des Users der die Änderung durchführt
Returns:
bool: True wenn erfolgreich, False bei Fehler
Aktualisiert die Job-Reihenfolge für einen Drucker.
"""
try:
with get_cached_session() as session:
# Validiere dass alle Jobs existieren und zum Drucker gehören
valid_jobs = session.query(Job).filter(
Job.id.in_(job_ids),
Job.printer_id == printer_id,
Job.status.in_(['scheduled', 'paused'])
).all()
if len(valid_jobs) != len(job_ids):
logger.warning(f"Nicht alle Jobs gültig für Drucker {printer_id}. "
f"Erwartet: {len(job_ids)}, Gefunden: {len(valid_jobs)}")
return False
# Alte Reihenfolge-Einträge für diesen Drucker löschen
# Bestehende Reihenfolge für diesen Drucker löschen
session.query(cls).filter(cls.printer_id == printer_id).delete()
# Neue Reihenfolge-Einträge erstellen
# Neue Reihenfolge erstellen
for position, job_id in enumerate(job_ids):
order_entry = cls(
order = cls(
printer_id=printer_id,
job_id=job_id,
order_position=position,
last_modified_by=modified_by_user_id
)
session.add(order_entry)
session.add(order)
session.commit()
# Cache invalidieren
clear_cache(f"JobOrder:{printer_id}")
logger.info(f"Job-Reihenfolge für Drucker {printer_id} erfolgreich aktualisiert. "
f"Jobs: {job_ids}, Benutzer: {modified_by_user_id}")
invalidate_model_cache("JobOrder", printer_id)
return True
except Exception as e:
logger.error(f"Fehler beim Aktualisieren der Job-Reihenfolge für Drucker {printer_id}: {str(e)}")
logger.error(f"Fehler beim Aktualisieren der Job-Reihenfolge: {str(e)}")
return False
@classmethod
def get_ordered_job_ids(cls, printer_id: int) -> List[int]:
"""
Holt die Job-IDs in der korrekten Reihenfolge für einen Drucker.
Args:
printer_id: ID des Druckers
Returns:
List[int]: Liste der Job-IDs in der richtigen Reihenfolge
Holt die geordneten Job-IDs für einen bestimmten Drucker.
"""
cache_key = get_cache_key("JobOrder", printer_id, "job_ids")
cache_key = get_cache_key("JobOrder", printer_id, "ordered_ids")
cached_ids = get_cache(cache_key)
if cached_ids is not None:
return cached_ids
try:
with get_cached_session() as session:
orders = session.query(cls).filter(
cls.printer_id == printer_id
).order_by(cls.order_position).all()
job_ids = [order.job_id for order in orders]
# Ergebnis cachen (1 Minute)
set_cache(cache_key, job_ids, 60)
return job_ids
except Exception as e:
logger.error(f"Fehler beim Laden der Job-Reihenfolge für Drucker {printer_id}: {str(e)}")
return []
orders = cls.get_order_for_printer(printer_id)
job_ids = [order.job_id for order in orders]
# Ergebnis für 5 Minuten cachen
set_cache(cache_key, job_ids, 300)
return job_ids
@classmethod
def remove_job_from_orders(cls, job_id: int):
"""
Entfernt einen Job aus allen Drucker-Reihenfolgen (z.B. wenn Job gelöscht wird).
Args:
job_id: ID des zu entfernenden Jobs
Entfernt einen Job aus allen Reihenfolgen (wenn Job gelöscht wird).
"""
try:
with get_cached_session() as session:
# Alle Order-Einträge für diesen Job finden
orders_to_remove = session.query(cls).filter(cls.job_id == job_id).all()
printer_ids = {order.printer_id for order in orders_to_remove}
# Job aus allen Reihenfolgen entfernen
affected_printers = session.query(cls.printer_id).filter(
cls.job_id == job_id
).distinct().all()
# Order-Einträge löschen
session.query(cls).filter(cls.job_id == job_id).delete()
# Positionen neu ordnen für betroffene Drucker
for printer_id in printer_ids:
# Positionen neu arrangieren für betroffene Drucker
for (printer_id,) in affected_printers:
remaining_orders = session.query(cls).filter(
cls.printer_id == printer_id
).order_by(cls.order_position).all()
).order_by(cls.order_position.asc()).all()
# Positionen neu setzen (lückenlos)
# Positionen neu vergeben
for new_position, order in enumerate(remaining_orders):
order.order_position = new_position
order.updated_at = datetime.now()
# Cache für diesen Drucker invalidieren
invalidate_model_cache("JobOrder", printer_id)
session.commit()
# Cache für betroffene Drucker invalidieren
for printer_id in printer_ids:
clear_cache(f"JobOrder:{printer_id}")
logger.info(f"Job {job_id} aus allen Drucker-Reihenfolgen entfernt. "
f"Betroffene Drucker: {list(printer_ids)}")
except Exception as e:
logger.error(f"Fehler beim Entfernen des Jobs {job_id} aus Reihenfolgen: {str(e)}")
logger.error(f"Fehler beim Entfernen des Jobs aus Reihenfolgen: {str(e)}")
@classmethod
def cleanup_invalid_orders(cls):
"""
Bereinigt ungültige Order-Einträge (Jobs die nicht mehr existieren oder abgeschlossen sind).
Bereinigt ungültige Reihenfolgen-Einträge (Jobs/Drucker die nicht mehr existieren).
"""
try:
with get_cached_session() as session:
# Finde Order-Einträge mit nicht existierenden oder abgeschlossenen Jobs
invalid_orders = session.query(cls).join(Job).filter(
Job.status.in_(['finished', 'aborted', 'cancelled'])
).all()
# Finde Reihenfolgen mit nicht-existierenden Jobs
invalid_job_orders = session.query(cls).outerjoin(
Job, cls.job_id == Job.id
).filter(Job.id.is_(None)).all()
printer_ids = {order.printer_id for order in invalid_orders}
# Finde Reihenfolgen mit nicht-existierenden Druckern
invalid_printer_orders = session.query(cls).outerjoin(
Printer, cls.printer_id == Printer.id
).filter(Printer.id.is_(None)).all()
# Ungültige Einträge löschen
session.query(cls).join(Job).filter(
Job.status.in_(['finished', 'aborted', 'cancelled'])
).delete(synchronize_session='fetch')
# Positionen für betroffene Drucker neu ordnen
for printer_id in printer_ids:
remaining_orders = session.query(cls).filter(
cls.printer_id == printer_id
).order_by(cls.order_position).all()
for new_position, order in enumerate(remaining_orders):
order.order_position = new_position
order.updated_at = datetime.now()
# Alle ungültigen Einträge löschen
for order in invalid_job_orders + invalid_printer_orders:
session.delete(order)
session.commit()
# Cache für betroffene Drucker invalidieren
for printer_id in printer_ids:
clear_cache(f"JobOrder:{printer_id}")
# Kompletten Cache leeren für Cleanup
clear_cache()
logger.info(f"Bereinigung der Job-Reihenfolgen abgeschlossen. "
f"Entfernte Einträge: {len(invalid_orders)}, "
f"Betroffene Drucker: {list(printer_ids)}")
logger.info(f"Bereinigung: {len(invalid_job_orders + invalid_printer_orders)} ungültige Reihenfolgen-Einträge entfernt")
except Exception as e:
logger.error(f"Fehler bei der Bereinigung der Job-Reihenfolgen: {str(e)}")
class SystemTimer(Base):
"""
System-Timer für Countdown-Zähler mit Force-Quit-Funktionalität.
Unterstützt verschiedene Timer-Typen für Kiosk, Sessions, Jobs, etc.
"""
__tablename__ = "system_timers"
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False) # Eindeutiger Name des Timers
timer_type = Column(String(50), nullable=False) # kiosk, session, job, system, maintenance
duration_seconds = Column(Integer, nullable=False) # Timer-Dauer in Sekunden
remaining_seconds = Column(Integer, nullable=False) # Verbleibende Sekunden
target_timestamp = Column(DateTime, nullable=False) # Ziel-Zeitstempel wann Timer abläuft
# Timer-Status und Kontrolle
status = Column(String(20), default="stopped") # stopped, running, paused, expired, force_quit
auto_start = Column(Boolean, default=False) # Automatischer Start nach Erstellung
auto_restart = Column(Boolean, default=False) # Automatischer Neustart nach Ablauf
# Force-Quit-Konfiguration
force_quit_enabled = Column(Boolean, default=True) # Force-Quit aktiviert
force_quit_action = Column(String(50), default="logout") # logout, restart, shutdown, custom
force_quit_warning_seconds = Column(Integer, default=30) # Warnung X Sekunden vor Force-Quit
# Zusätzliche Konfiguration
show_warning = Column(Boolean, default=True) # Warnung anzeigen
warning_message = Column(Text, nullable=True) # Benutzerdefinierte Warnung
custom_action_endpoint = Column(String(200), nullable=True) # Custom API-Endpoint für Force-Quit
# Verwaltung und Tracking
created_by = Column(Integer, ForeignKey("users.id"), nullable=True) # Ersteller (optional für System-Timer)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
last_activity = Column(DateTime, default=datetime.now) # Letzte Aktivität (für Session-Timer)
# Kontext-spezifische Felder
context_id = Column(Integer, nullable=True) # Job-ID, Session-ID, etc.
context_data = Column(Text, nullable=True) # JSON-String für zusätzliche Kontext-Daten
# Statistiken
start_count = Column(Integer, default=0) # Wie oft wurde der Timer gestartet
force_quit_count = Column(Integer, default=0) # Wie oft wurde Force-Quit ausgeführt
# Beziehungen
created_by_user = relationship("User", foreign_keys=[created_by])
def to_dict(self) -> dict:
"""
Konvertiert den Timer zu einem Dictionary für API-Responses.
"""
# Cache-Key für Timer-Dict
cache_key = get_cache_key("SystemTimer", self.id, "dict")
cached_result = get_cache(cache_key)
if cached_result is not None:
return cached_result
# Berechne aktuelle verbleibende Zeit
current_remaining = self.get_current_remaining_seconds()
result = {
"id": self.id,
"name": self.name,
"timer_type": self.timer_type,
"duration_seconds": self.duration_seconds,
"remaining_seconds": current_remaining,
"target_timestamp": self.target_timestamp.isoformat() if self.target_timestamp else None,
"status": self.status,
"auto_start": self.auto_start,
"auto_restart": self.auto_restart,
"force_quit_enabled": self.force_quit_enabled,
"force_quit_action": self.force_quit_action,
"force_quit_warning_seconds": self.force_quit_warning_seconds,
"show_warning": self.show_warning,
"warning_message": self.warning_message,
"custom_action_endpoint": self.custom_action_endpoint,
"created_by": self.created_by,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"last_activity": self.last_activity.isoformat() if self.last_activity else None,
"context_id": self.context_id,
"context_data": self.context_data,
"start_count": self.start_count,
"force_quit_count": self.force_quit_count,
# Berechnete Felder
"is_running": self.is_running(),
"is_expired": self.is_expired(),
"should_show_warning": self.should_show_warning(),
"progress_percentage": self.get_progress_percentage()
}
# Ergebnis für 10 Sekunden cachen (kurz wegen sich ändernder Zeit)
set_cache(cache_key, result, 10)
return result
def get_current_remaining_seconds(self) -> int:
"""
Berechnet die aktuell verbleibenden Sekunden basierend auf dem Ziel-Zeitstempel.
"""
if self.status != "running":
return self.remaining_seconds
now = datetime.now()
if now >= self.target_timestamp:
return 0
remaining = int((self.target_timestamp - now).total_seconds())
return max(0, remaining)
def is_running(self) -> bool:
"""
Prüft ob der Timer aktuell läuft.
"""
return self.status == "running"
def is_expired(self) -> bool:
"""
Prüft ob der Timer abgelaufen ist.
"""
return self.status == "expired" or self.get_current_remaining_seconds() <= 0
def should_show_warning(self) -> bool:
"""
Prüft ob eine Warnung angezeigt werden soll.
"""
if not self.show_warning or not self.is_running():
return False
remaining = self.get_current_remaining_seconds()
return remaining <= self.force_quit_warning_seconds and remaining > 0
def get_progress_percentage(self) -> float:
"""
Berechnet den Fortschritt in Prozent (0.0 bis 100.0).
"""
if self.duration_seconds <= 0:
return 100.0
elapsed = self.duration_seconds - self.get_current_remaining_seconds()
return min(100.0, max(0.0, (elapsed / self.duration_seconds) * 100.0))
def start_timer(self) -> bool:
"""
Startet den Timer.
"""
try:
if self.status == "running":
return True # Bereits laufend
now = datetime.now()
self.target_timestamp = now + timedelta(seconds=self.remaining_seconds)
self.status = "running"
self.last_activity = now
self.start_count += 1
self.updated_at = now
# Cache invalidieren
invalidate_model_cache("SystemTimer", self.id)
logger.info(f"Timer '{self.name}' gestartet - läuft für {self.remaining_seconds} Sekunden")
return True
except Exception as e:
logger.error(f"Fehler beim Starten des Timers '{self.name}': {str(e)}")
return False
def pause_timer(self) -> bool:
"""
Pausiert den Timer.
"""
try:
if self.status != "running":
return False
# Verbleibende Zeit aktualisieren
self.remaining_seconds = self.get_current_remaining_seconds()
self.status = "paused"
self.updated_at = datetime.now()
# Cache invalidieren
invalidate_model_cache("SystemTimer", self.id)
logger.info(f"Timer '{self.name}' pausiert - {self.remaining_seconds} Sekunden verbleiben")
return True
except Exception as e:
logger.error(f"Fehler beim Pausieren des Timers '{self.name}': {str(e)}")
return False
def stop_timer(self) -> bool:
"""
Stoppt den Timer.
"""
try:
self.status = "stopped"
self.remaining_seconds = self.duration_seconds # Zurücksetzen
self.updated_at = datetime.now()
# Cache invalidieren
invalidate_model_cache("SystemTimer", self.id)
logger.info(f"Timer '{self.name}' gestoppt und zurückgesetzt")
return True
except Exception as e:
logger.error(f"Fehler beim Stoppen des Timers '{self.name}': {str(e)}")
return False
def reset_timer(self) -> bool:
"""
Setzt den Timer auf die ursprüngliche Dauer zurück.
"""
try:
self.remaining_seconds = self.duration_seconds
if self.status == "running":
# Neu berechnen wenn laufend
now = datetime.now()
self.target_timestamp = now + timedelta(seconds=self.duration_seconds)
self.updated_at = datetime.now()
# Cache invalidieren
invalidate_model_cache("SystemTimer", self.id)
logger.info(f"Timer '{self.name}' zurückgesetzt auf {self.duration_seconds} Sekunden")
return True
except Exception as e:
logger.error(f"Fehler beim Zurücksetzen des Timers '{self.name}': {str(e)}")
return False
def extend_timer(self, additional_seconds: int) -> bool:
"""
Verlängert den Timer um zusätzliche Sekunden.
"""
try:
if additional_seconds <= 0:
return False
self.duration_seconds += additional_seconds
self.remaining_seconds += additional_seconds
if self.status == "running":
# Ziel-Zeitstempel aktualisieren
self.target_timestamp = self.target_timestamp + timedelta(seconds=additional_seconds)
self.updated_at = datetime.now()
# Cache invalidieren
invalidate_model_cache("SystemTimer", self.id)
logger.info(f"Timer '{self.name}' um {additional_seconds} Sekunden verlängert")
return True
except Exception as e:
logger.error(f"Fehler beim Verlängern des Timers '{self.name}': {str(e)}")
return False
def force_quit_execute(self) -> bool:
"""
Führt die Force-Quit-Aktion aus.
"""
try:
if not self.force_quit_enabled:
logger.warning(f"Force-Quit für Timer '{self.name}' ist deaktiviert")
return False
self.status = "force_quit"
self.force_quit_count += 1
self.updated_at = datetime.now()
# Cache invalidieren
invalidate_model_cache("SystemTimer", self.id)
logger.warning(f"Force-Quit für Timer '{self.name}' ausgeführt - Aktion: {self.force_quit_action}")
return True
except Exception as e:
logger.error(f"Fehler beim Force-Quit des Timers '{self.name}': {str(e)}")
return False
def update_activity(self) -> bool:
"""
Aktualisiert die letzte Aktivität (für Session-Timer).
"""
try:
self.last_activity = datetime.now()
self.updated_at = datetime.now()
# Cache invalidieren
invalidate_model_cache("SystemTimer", self.id)
return True
except Exception as e:
logger.error(f"Fehler beim Aktualisieren der Aktivität für Timer '{self.name}': {str(e)}")
return False
@classmethod
def get_by_name(cls, name: str) -> Optional['SystemTimer']:
"""
Holt einen Timer anhand des Namens.
"""
cache_key = get_cache_key("SystemTimer", name, "by_name")
cached_timer = get_cache(cache_key)
if cached_timer is not None:
return cached_timer
with get_cached_session() as session:
timer = session.query(cls).filter(cls.name == name).first()
if timer:
# Timer für 5 Minuten cachen
set_cache(cache_key, timer, 300)
return timer
@classmethod
def get_by_type(cls, timer_type: str) -> List['SystemTimer']:
"""
Holt alle Timer eines bestimmten Typs.
"""
cache_key = get_cache_key("SystemTimer", timer_type, "by_type")
cached_timers = get_cache(cache_key)
if cached_timers is not None:
return cached_timers
with get_cached_session() as session:
timers = session.query(cls).filter(cls.timer_type == timer_type).all()
# Timer für 2 Minuten cachen
set_cache(cache_key, timers, 120)
return timers
@classmethod
def get_running_timers(cls) -> List['SystemTimer']:
"""
Holt alle aktuell laufenden Timer.
"""
cache_key = get_cache_key("SystemTimer", "all", "running")
cached_timers = get_cache(cache_key)
if cached_timers is not None:
return cached_timers
with get_cached_session() as session:
timers = session.query(cls).filter(cls.status == "running").all()
# Nur 30 Sekunden cachen wegen sich ändernder Zeiten
set_cache(cache_key, timers, 30)
return timers
@classmethod
def get_expired_timers(cls) -> List['SystemTimer']:
"""
Holt alle abgelaufenen Timer die Force-Quit-Aktionen benötigen.
"""
with get_cached_session() as session:
now = datetime.now()
# Timer die laufen aber abgelaufen sind
expired_timers = session.query(cls).filter(
cls.status == "running",
cls.target_timestamp <= now,
cls.force_quit_enabled == True
).all()
return expired_timers
@classmethod
def cleanup_expired_timers(cls) -> int:
"""
Bereinigt abgelaufene Timer und führt Force-Quit-Aktionen aus.
"""
try:
expired_timers = cls.get_expired_timers()
cleanup_count = 0
for timer in expired_timers:
if timer.force_quit_execute():
cleanup_count += 1
if cleanup_count > 0:
# Cache für alle Timer invalidieren
clear_cache("SystemTimer")
logger.info(f"Cleanup: {cleanup_count} abgelaufene Timer verarbeitet")
return cleanup_count
except Exception as e:
logger.error(f"Fehler beim Cleanup abgelaufener Timer: {str(e)}")
return 0
@classmethod
def create_kiosk_timer(cls, duration_minutes: int = 30, auto_start: bool = True) -> Optional['SystemTimer']:
"""
Erstellt einen Standard-Kiosk-Timer.
"""
try:
with get_cached_session() as session:
# Prüfe ob bereits ein Kiosk-Timer existiert
existing = session.query(cls).filter(
cls.timer_type == "kiosk",
cls.name == "kiosk_session"
).first()
if existing:
# Bestehenden Timer aktualisieren
existing.duration_seconds = duration_minutes * 60
existing.remaining_seconds = duration_minutes * 60
existing.auto_start = auto_start
existing.updated_at = datetime.now()
if auto_start and existing.status != "running":
existing.start_timer()
# Cache invalidieren
invalidate_model_cache("SystemTimer", existing.id)
session.commit()
return existing
# Neuen Timer erstellen
timer = cls(
name="kiosk_session",
timer_type="kiosk",
duration_seconds=duration_minutes * 60,
remaining_seconds=duration_minutes * 60,
auto_start=auto_start,
force_quit_enabled=True,
force_quit_action="logout",
force_quit_warning_seconds=30,
show_warning=True,
warning_message="Kiosk-Session läuft ab. Bitte speichern Sie Ihre Arbeit.",
target_timestamp=datetime.now() + timedelta(minutes=duration_minutes)
)
session.add(timer)
session.commit()
if auto_start:
timer.start_timer()
logger.info(f"Kiosk-Timer erstellt: {duration_minutes} Minuten")
return timer
except Exception as e:
logger.error(f"Fehler beim Erstellen des Kiosk-Timers: {str(e)}")
return None
# ===== DATENBANK-INITIALISIERUNG MIT OPTIMIERUNGEN =====
def init_db() -> None: