🎉 Feature: Optimized CSS build process for improved performance 🎉
This commit is contained in:
415
backend/app.py
415
backend/app.py
@@ -22,6 +22,124 @@ import shutil
|
||||
from contextlib import contextmanager
|
||||
import threading
|
||||
|
||||
# ===== OPTIMIERTE KONFIGURATION FÜR RASPBERRY PI =====
|
||||
class OptimizedConfig:
|
||||
"""Configuration for performance-optimized deployment on Raspberry Pi"""
|
||||
|
||||
# Performance optimization flags
|
||||
OPTIMIZED_MODE = True
|
||||
USE_MINIFIED_ASSETS = True
|
||||
DISABLE_ANIMATIONS = True
|
||||
LIMIT_GLASSMORPHISM = True
|
||||
|
||||
# Flask performance settings
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 31536000 # 1 year cache for static files
|
||||
|
||||
# Template settings
|
||||
TEMPLATES_AUTO_RELOAD = False
|
||||
EXPLAIN_TEMPLATE_LOADING = False
|
||||
|
||||
# Session configuration
|
||||
SESSION_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
|
||||
# Performance optimizations
|
||||
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max upload
|
||||
JSON_SORT_KEYS = False
|
||||
JSONIFY_PRETTYPRINT_REGULAR = False
|
||||
|
||||
# Database optimizations
|
||||
SQLALCHEMY_ECHO = False
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
'pool_size': 5,
|
||||
'pool_recycle': 3600,
|
||||
'pool_pre_ping': True,
|
||||
'connect_args': {
|
||||
'check_same_thread': False
|
||||
}
|
||||
}
|
||||
|
||||
# Cache configuration
|
||||
CACHE_TYPE = 'simple'
|
||||
CACHE_DEFAULT_TIMEOUT = 300
|
||||
CACHE_KEY_PREFIX = 'myp_'
|
||||
|
||||
# Static file caching headers
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 31536000 # 1 year
|
||||
|
||||
@staticmethod
|
||||
def init_app(app):
|
||||
"""Initialize application with optimized settings"""
|
||||
# Set optimized template
|
||||
app.jinja_env.globals['optimized_mode'] = True
|
||||
app.jinja_env.globals['base_template'] = 'base-optimized.html'
|
||||
|
||||
# Add cache headers for static files
|
||||
@app.after_request
|
||||
def add_cache_headers(response):
|
||||
if 'static' in response.headers.get('Location', ''):
|
||||
response.headers['Cache-Control'] = 'public, max-age=31536000'
|
||||
response.headers['Vary'] = 'Accept-Encoding'
|
||||
return response
|
||||
|
||||
# Disable unnecessary features
|
||||
app.config['EXPLAIN_TEMPLATE_LOADING'] = False
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = False
|
||||
|
||||
print("🚀 Running in OPTIMIZED mode for Raspberry Pi")
|
||||
|
||||
def detect_raspberry_pi():
|
||||
"""Erkennt ob das System auf einem Raspberry Pi läuft"""
|
||||
try:
|
||||
# Prüfe auf Raspberry Pi Hardware
|
||||
with open('/proc/cpuinfo', 'r') as f:
|
||||
cpuinfo = f.read()
|
||||
if 'Raspberry Pi' in cpuinfo or 'BCM' in cpuinfo:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Prüfe auf ARM-Architektur
|
||||
import platform
|
||||
machine = platform.machine().lower()
|
||||
if 'arm' in machine or 'aarch64' in machine:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
# Umgebungsvariable für manuelle Aktivierung
|
||||
return os.getenv('FORCE_OPTIMIZED_MODE', '').lower() in ['true', '1', 'yes']
|
||||
|
||||
def should_use_optimized_config():
|
||||
"""Bestimmt ob die optimierte Konfiguration verwendet werden soll"""
|
||||
# Kommandozeilen-Argument prüfen
|
||||
if '--optimized' in sys.argv:
|
||||
return True
|
||||
|
||||
# Raspberry Pi-Erkennung
|
||||
if detect_raspberry_pi():
|
||||
return True
|
||||
|
||||
# Umgebungsvariable
|
||||
if os.getenv('USE_OPTIMIZED_CONFIG', '').lower() in ['true', '1', 'yes']:
|
||||
return True
|
||||
|
||||
# Schwache Hardware-Erkennung (weniger als 2GB RAM)
|
||||
try:
|
||||
import psutil
|
||||
memory_gb = psutil.virtual_memory().total / (1024**3)
|
||||
if memory_gb < 2.0:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
# Windows-spezifische Fixes früh importieren (sichere Version)
|
||||
if os.name == 'nt':
|
||||
try:
|
||||
@@ -311,9 +429,75 @@ register_aggressive_shutdown()
|
||||
# Flask-App initialisieren
|
||||
app = Flask(__name__)
|
||||
app.secret_key = SECRET_KEY
|
||||
app.config["PERMANENT_SESSION_LIFETIME"] = SESSION_LIFETIME
|
||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
app.config["WTF_CSRF_ENABLED"] = True
|
||||
|
||||
# ===== OPTIMIERTE KONFIGURATION ANWENDEN =====
|
||||
# Prüfe ob optimierte Konfiguration verwendet werden soll
|
||||
USE_OPTIMIZED_CONFIG = should_use_optimized_config()
|
||||
|
||||
if USE_OPTIMIZED_CONFIG:
|
||||
app_logger.info("🚀 Aktiviere optimierte Konfiguration für schwache Hardware/Raspberry Pi")
|
||||
|
||||
# Optimierte Flask-Konfiguration anwenden
|
||||
app.config.update({
|
||||
"DEBUG": OptimizedConfig.DEBUG,
|
||||
"TESTING": OptimizedConfig.TESTING,
|
||||
"SEND_FILE_MAX_AGE_DEFAULT": OptimizedConfig.SEND_FILE_MAX_AGE_DEFAULT,
|
||||
"TEMPLATES_AUTO_RELOAD": OptimizedConfig.TEMPLATES_AUTO_RELOAD,
|
||||
"EXPLAIN_TEMPLATE_LOADING": OptimizedConfig.EXPLAIN_TEMPLATE_LOADING,
|
||||
"SESSION_COOKIE_SECURE": OptimizedConfig.SESSION_COOKIE_SECURE,
|
||||
"SESSION_COOKIE_HTTPONLY": OptimizedConfig.SESSION_COOKIE_HTTPONLY,
|
||||
"SESSION_COOKIE_SAMESITE": OptimizedConfig.SESSION_COOKIE_SAMESITE,
|
||||
"MAX_CONTENT_LENGTH": OptimizedConfig.MAX_CONTENT_LENGTH,
|
||||
"JSON_SORT_KEYS": OptimizedConfig.JSON_SORT_KEYS,
|
||||
"JSONIFY_PRETTYPRINT_REGULAR": OptimizedConfig.JSONIFY_PRETTYPRINT_REGULAR,
|
||||
"SQLALCHEMY_ECHO": OptimizedConfig.SQLALCHEMY_ECHO,
|
||||
"SQLALCHEMY_TRACK_MODIFICATIONS": OptimizedConfig.SQLALCHEMY_TRACK_MODIFICATIONS,
|
||||
"SQLALCHEMY_ENGINE_OPTIONS": OptimizedConfig.SQLALCHEMY_ENGINE_OPTIONS
|
||||
})
|
||||
|
||||
# Session-Konfiguration
|
||||
app.config["PERMANENT_SESSION_LIFETIME"] = SESSION_LIFETIME
|
||||
app.config["WTF_CSRF_ENABLED"] = True
|
||||
|
||||
# Jinja2-Globals für optimierte Templates
|
||||
app.jinja_env.globals.update({
|
||||
'optimized_mode': True,
|
||||
'use_minified_assets': OptimizedConfig.USE_MINIFIED_ASSETS,
|
||||
'disable_animations': OptimizedConfig.DISABLE_ANIMATIONS,
|
||||
'limit_glassmorphism': OptimizedConfig.LIMIT_GLASSMORPHISM,
|
||||
'base_template': 'base-optimized.html'
|
||||
})
|
||||
|
||||
# Optimierte After-Request-Handler
|
||||
@app.after_request
|
||||
def add_optimized_cache_headers(response):
|
||||
"""Fügt optimierte Cache-Header für statische Dateien hinzu"""
|
||||
if request.endpoint == 'static' or '/static/' in request.path:
|
||||
response.headers['Cache-Control'] = 'public, max-age=31536000'
|
||||
response.headers['Vary'] = 'Accept-Encoding'
|
||||
# Preload-Header für kritische Assets
|
||||
if request.path.endswith(('.css', '.js')):
|
||||
response.headers['X-Optimized-Asset'] = 'true'
|
||||
return response
|
||||
|
||||
app_logger.info("✅ Optimierte Konfiguration aktiviert")
|
||||
|
||||
else:
|
||||
# Standard-Konfiguration
|
||||
app.config["PERMANENT_SESSION_LIFETIME"] = SESSION_LIFETIME
|
||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
app.config["WTF_CSRF_ENABLED"] = True
|
||||
|
||||
# Standard Jinja2-Globals
|
||||
app.jinja_env.globals.update({
|
||||
'optimized_mode': False,
|
||||
'use_minified_assets': False,
|
||||
'disable_animations': False,
|
||||
'limit_glassmorphism': False,
|
||||
'base_template': 'base.html'
|
||||
})
|
||||
|
||||
app_logger.info("📋 Standard-Konfiguration verwendet")
|
||||
|
||||
# Globale db-Variable für Kompatibilität mit init_simple_db.py
|
||||
db = db_engine
|
||||
@@ -549,6 +733,23 @@ def format_datetime_filter(value, format='%d.%m.%Y %H:%M'):
|
||||
return value
|
||||
return value.strftime(format)
|
||||
|
||||
# Template-Helper für Optimierungsstatus
|
||||
@app.template_global()
|
||||
def is_optimized_mode():
|
||||
"""Prüft ob die Anwendung im optimierten Modus läuft"""
|
||||
return USE_OPTIMIZED_CONFIG
|
||||
|
||||
@app.template_global()
|
||||
def get_optimization_info():
|
||||
"""Gibt Optimierungsinformationen für Templates zurück"""
|
||||
return {
|
||||
'active': USE_OPTIMIZED_CONFIG,
|
||||
'raspberry_pi': detect_raspberry_pi(),
|
||||
'minified_assets': app.jinja_env.globals.get('use_minified_assets', False),
|
||||
'disabled_animations': app.jinja_env.globals.get('disable_animations', False),
|
||||
'limited_glassmorphism': app.jinja_env.globals.get('limit_glassmorphism', False)
|
||||
}
|
||||
|
||||
# HTTP-Request/Response-Middleware für automatisches Debug-Logging
|
||||
@app.before_request
|
||||
def log_request_info():
|
||||
@@ -5610,6 +5811,22 @@ def admin_advanced_settings():
|
||||
user_settings = session.get('user_settings', {})
|
||||
optimization_settings = user_settings.get('optimization', default_settings)
|
||||
|
||||
# Performance-Optimierungs-Status hinzufügen
|
||||
performance_optimization = {
|
||||
'active': USE_OPTIMIZED_CONFIG,
|
||||
'raspberry_pi_detected': detect_raspberry_pi(),
|
||||
'forced_mode': os.getenv('FORCE_OPTIMIZED_MODE', '').lower() in ['true', '1', 'yes'],
|
||||
'cli_mode': '--optimized' in sys.argv,
|
||||
'current_settings': {
|
||||
'minified_assets': app.jinja_env.globals.get('use_minified_assets', False),
|
||||
'disabled_animations': app.jinja_env.globals.get('disable_animations', False),
|
||||
'limited_glassmorphism': app.jinja_env.globals.get('limit_glassmorphism', False),
|
||||
'template_caching': not app.config.get('TEMPLATES_AUTO_RELOAD', True),
|
||||
'json_optimization': not app.config.get('JSON_SORT_KEYS', True),
|
||||
'static_cache_age': app.config.get('SEND_FILE_MAX_AGE_DEFAULT', 0)
|
||||
}
|
||||
}
|
||||
|
||||
# System-Statistiken sammeln
|
||||
stats = {
|
||||
'total_users': db_session.query(User).count(),
|
||||
@@ -5659,6 +5876,7 @@ def admin_advanced_settings():
|
||||
'admin_advanced_settings.html',
|
||||
title='Erweiterte Einstellungen',
|
||||
optimization_settings=optimization_settings,
|
||||
performance_optimization=performance_optimization,
|
||||
stats=stats,
|
||||
maintenance_info=maintenance_info
|
||||
)
|
||||
@@ -5668,6 +5886,59 @@ def admin_advanced_settings():
|
||||
flash('Fehler beim Laden der erweiterten Einstellungen', 'error')
|
||||
return redirect(url_for('admin_page'))
|
||||
|
||||
@app.route("/admin/performance-optimization")
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_performance_optimization():
|
||||
"""Performance-Optimierungs-Verwaltungsseite für Admins"""
|
||||
try:
|
||||
app_logger.info(f"🚀 Performance-Optimierung-Seite aufgerufen von Admin {current_user.username}")
|
||||
|
||||
# Aktuelle Optimierungseinstellungen sammeln
|
||||
optimization_status = {
|
||||
'mode_active': USE_OPTIMIZED_CONFIG,
|
||||
'detection': {
|
||||
'raspberry_pi': detect_raspberry_pi(),
|
||||
'forced_mode': os.getenv('FORCE_OPTIMIZED_MODE', '').lower() in ['true', '1', 'yes'],
|
||||
'cli_mode': '--optimized' in sys.argv,
|
||||
'low_memory': False
|
||||
},
|
||||
'settings': {
|
||||
'minified_assets': app.jinja_env.globals.get('use_minified_assets', False),
|
||||
'disabled_animations': app.jinja_env.globals.get('disable_animations', False),
|
||||
'limited_glassmorphism': app.jinja_env.globals.get('limit_glassmorphism', False),
|
||||
'template_caching': not app.config.get('TEMPLATES_AUTO_RELOAD', True),
|
||||
'json_optimization': not app.config.get('JSON_SORT_KEYS', True),
|
||||
'debug_disabled': not app.config.get('DEBUG', False),
|
||||
'secure_sessions': app.config.get('SESSION_COOKIE_SECURE', False)
|
||||
},
|
||||
'performance': {
|
||||
'static_cache_age_hours': app.config.get('SEND_FILE_MAX_AGE_DEFAULT', 0) / 3600,
|
||||
'max_upload_mb': app.config.get('MAX_CONTENT_LENGTH', 0) / (1024 * 1024) if app.config.get('MAX_CONTENT_LENGTH') else 0,
|
||||
'sqlalchemy_echo': app.config.get('SQLALCHEMY_ECHO', True)
|
||||
}
|
||||
}
|
||||
|
||||
# Memory-Erkennung hinzufügen
|
||||
try:
|
||||
import psutil
|
||||
memory_gb = psutil.virtual_memory().total / (1024**3)
|
||||
optimization_status['detection']['low_memory'] = memory_gb < 2.0
|
||||
optimization_status['system_memory_gb'] = round(memory_gb, 2)
|
||||
except ImportError:
|
||||
optimization_status['system_memory_gb'] = None
|
||||
|
||||
return render_template(
|
||||
'admin_performance_optimization.html',
|
||||
title='Performance-Optimierung',
|
||||
optimization_status=optimization_status
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler beim Laden der Performance-Optimierung-Seite: {str(e)}")
|
||||
flash('Fehler beim Laden der Performance-Optimierung-Seite', 'error')
|
||||
return redirect(url_for('admin_page'))
|
||||
|
||||
@app.route('/api/admin/maintenance/cleanup-logs', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -8100,6 +8371,128 @@ def api_admin_system_status():
|
||||
}), 500
|
||||
|
||||
|
||||
# ===== OPTIMIERUNGSSTATUS API =====
|
||||
@app.route("/api/system/optimization-status", methods=['GET'])
|
||||
def api_optimization_status():
|
||||
"""
|
||||
API-Endpunkt für den aktuellen Optimierungsstatus.
|
||||
|
||||
Gibt Informationen über aktivierte Optimierungen zurück.
|
||||
"""
|
||||
try:
|
||||
status = {
|
||||
"optimized_mode_active": USE_OPTIMIZED_CONFIG,
|
||||
"hardware_detected": {
|
||||
"is_raspberry_pi": detect_raspberry_pi(),
|
||||
"forced_optimization": os.getenv('FORCE_OPTIMIZED_MODE', '').lower() in ['true', '1', 'yes'],
|
||||
"cli_optimization": '--optimized' in sys.argv
|
||||
},
|
||||
"active_optimizations": {
|
||||
"minified_assets": app.jinja_env.globals.get('use_minified_assets', False),
|
||||
"disabled_animations": app.jinja_env.globals.get('disable_animations', False),
|
||||
"limited_glassmorphism": app.jinja_env.globals.get('limit_glassmorphism', False),
|
||||
"cache_headers": USE_OPTIMIZED_CONFIG,
|
||||
"template_caching": not app.config.get('TEMPLATES_AUTO_RELOAD', True),
|
||||
"json_optimization": not app.config.get('JSON_SORT_KEYS', True)
|
||||
},
|
||||
"performance_settings": {
|
||||
"max_upload_mb": app.config.get('MAX_CONTENT_LENGTH', 0) / (1024 * 1024) if app.config.get('MAX_CONTENT_LENGTH') else None,
|
||||
"static_cache_age": app.config.get('SEND_FILE_MAX_AGE_DEFAULT', 0),
|
||||
"sqlalchemy_echo": app.config.get('SQLALCHEMY_ECHO', True),
|
||||
"session_secure": app.config.get('SESSION_COOKIE_SECURE', False)
|
||||
}
|
||||
}
|
||||
|
||||
# Zusätzliche System-Informationen wenn verfügbar
|
||||
try:
|
||||
import psutil
|
||||
import platform
|
||||
|
||||
status["system_info"] = {
|
||||
"cpu_count": psutil.cpu_count(),
|
||||
"memory_gb": round(psutil.virtual_memory().total / (1024**3), 2),
|
||||
"platform": platform.machine(),
|
||||
"system": platform.system()
|
||||
}
|
||||
except ImportError:
|
||||
status["system_info"] = {"error": "psutil nicht verfügbar"}
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"status": status,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Abrufen des Optimierungsstatus: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
@app.route("/api/admin/optimization/toggle", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def api_admin_toggle_optimization():
|
||||
"""
|
||||
API-Endpunkt zum Umschalten der Optimierungen zur Laufzeit (nur Admins).
|
||||
|
||||
Achtung: Einige Optimierungen erfordern einen Neustart.
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Welche Optimierung soll umgeschaltet werden?
|
||||
optimization_type = data.get('type')
|
||||
enabled = data.get('enabled', True)
|
||||
|
||||
changes_made = []
|
||||
restart_required = False
|
||||
|
||||
if optimization_type == 'animations':
|
||||
app.jinja_env.globals['disable_animations'] = enabled
|
||||
changes_made.append(f"Animationen {'deaktiviert' if enabled else 'aktiviert'}")
|
||||
|
||||
elif optimization_type == 'glassmorphism':
|
||||
app.jinja_env.globals['limit_glassmorphism'] = enabled
|
||||
changes_made.append(f"Glassmorphism {'begrenzt' if enabled else 'vollständig'}")
|
||||
|
||||
elif optimization_type == 'minified_assets':
|
||||
app.jinja_env.globals['use_minified_assets'] = enabled
|
||||
changes_made.append(f"Minifizierte Assets {'aktiviert' if enabled else 'deaktiviert'}")
|
||||
|
||||
elif optimization_type == 'template_caching':
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = not enabled
|
||||
changes_made.append(f"Template-Caching {'aktiviert' if enabled else 'deaktiviert'}")
|
||||
restart_required = True
|
||||
|
||||
elif optimization_type == 'debug_mode':
|
||||
app.config['DEBUG'] = not enabled
|
||||
changes_made.append(f"Debug-Modus {'deaktiviert' if enabled else 'aktiviert'}")
|
||||
restart_required = True
|
||||
|
||||
else:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Unbekannter Optimierungstyp"
|
||||
}), 400
|
||||
|
||||
app_logger.info(f"Admin {current_user.username} hat Optimierung '{optimization_type}' auf {enabled} gesetzt")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"changes": changes_made,
|
||||
"restart_required": restart_required,
|
||||
"message": f"Optimierung '{optimization_type}' erfolgreich {'aktiviert' if enabled else 'deaktiviert'}"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Umschalten der Optimierung: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
# ===== ÖFFENTLICHE STATISTIK-API =====
|
||||
@app.route("/api/statistics/public", methods=['GET'])
|
||||
def api_public_statistics():
|
||||
@@ -9030,6 +9423,22 @@ if __name__ == "__main__":
|
||||
# Template-Hilfsfunktionen registrieren
|
||||
register_template_helpers(app)
|
||||
|
||||
# Optimierungsstatus beim Start anzeigen
|
||||
if USE_OPTIMIZED_CONFIG:
|
||||
app_logger.info("🚀 === OPTIMIERTE KONFIGURATION AKTIV ===")
|
||||
app_logger.info(f"📊 Hardware erkannt: Raspberry Pi={detect_raspberry_pi()}")
|
||||
app_logger.info(f"⚙️ Erzwungen: {os.getenv('FORCE_OPTIMIZED_MODE', '').lower() in ['true', '1', 'yes']}")
|
||||
app_logger.info(f"🔧 CLI-Parameter: {'--optimized' in sys.argv}")
|
||||
app_logger.info("🔧 Aktive Optimierungen:")
|
||||
app_logger.info(f" - Minifizierte Assets: {app.jinja_env.globals.get('use_minified_assets', False)}")
|
||||
app_logger.info(f" - Animationen deaktiviert: {app.jinja_env.globals.get('disable_animations', False)}")
|
||||
app_logger.info(f" - Glassmorphism begrenzt: {app.jinja_env.globals.get('limit_glassmorphism', False)}")
|
||||
app_logger.info(f" - Template-Caching: {not app.config.get('TEMPLATES_AUTO_RELOAD', True)}")
|
||||
app_logger.info(f" - Static Cache: {app.config.get('SEND_FILE_MAX_AGE_DEFAULT', 0) / 3600:.1f}h")
|
||||
app_logger.info("🚀 ========================================")
|
||||
else:
|
||||
app_logger.info("📋 Standard-Konfiguration aktiv (keine Optimierungen)")
|
||||
|
||||
# Drucker-Monitor Steckdosen-Initialisierung beim Start
|
||||
try:
|
||||
app_logger.info("🖨️ Starte automatische Steckdosen-Initialisierung...")
|
||||
|
Reference in New Issue
Block a user