🎉 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:
@ -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:
|
||||
|
Reference in New Issue
Block a user