🎉 Improved backend functionality & documentation, added OTP documentation & JavaScript file. 🎨

This commit is contained in:
Till Tomczak 2025-06-01 00:15:47 +02:00
parent b2bdc2d123
commit 91548dfb0e
5 changed files with 984 additions and 155 deletions

View File

@ -41,6 +41,29 @@ from utils.queue_manager import start_queue_manager, stop_queue_manager, get_que
from config.settings import SECRET_KEY, UPLOAD_FOLDER, ALLOWED_EXTENSIONS, ENVIRONMENT, SESSION_LIFETIME, SCHEDULER_ENABLED, SCHEDULER_INTERVAL, TAPO_USERNAME, TAPO_PASSWORD
from utils.file_manager import file_manager, save_job_file, save_guest_file, save_avatar_file, save_asset_file, save_log_file, save_backup_file, save_temp_file, delete_file as delete_file_safe
# ===== OFFLINE-MODUS KONFIGURATION =====
# System läuft im Offline-Modus ohne Internetverbindung
OFFLINE_MODE = True # Produktionseinstellung für Offline-Betrieb
# ===== BEDINGTE IMPORTS FÜR OFFLINE-MODUS =====
if not OFFLINE_MODE:
# Nur laden wenn Online-Modus
import requests
else:
# Offline-Mock für requests
class OfflineRequestsMock:
"""Mock-Klasse für requests im Offline-Modus"""
@staticmethod
def get(*args, **kwargs):
raise ConnectionError("System läuft im Offline-Modus - keine Internet-Verbindung verfügbar")
@staticmethod
def post(*args, **kwargs):
raise ConnectionError("System läuft im Offline-Modus - keine Internet-Verbindung verfügbar")
requests = OfflineRequestsMock()
# Datenbank-Engine für Kompatibilität mit init_simple_db.py
from models import engine as db_engine
@ -311,7 +334,7 @@ def format_datetime_filter(value, format='%d.%m.%Y %H:%M'):
setup_logging()
log_startup_info()
# Logger für verschiedene Komponenten
# app_logger für verschiedene Komponenten
app_logger = get_logger("app")
auth_logger = get_logger("auth")
jobs_logger = get_logger("jobs")
@ -1695,7 +1718,7 @@ def api_admin_system_health():
})
except Exception as e:
logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
app_logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
@ -1731,7 +1754,7 @@ def api_admin_system_health():
})
except Exception as e:
logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
app_logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
@ -3210,6 +3233,118 @@ def test_admin_guest_requests():
'user_is_admin': current_user.is_admin if current_user.is_authenticated else False
})
@app.route('/api/guest-status', methods=['POST'])
def get_guest_request_status():
"""
Öffentliche Route für Gäste um ihren Auftragsstatus mit OTP-Code zu prüfen.
Keine Authentifizierung erforderlich.
"""
try:
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': 'Keine Daten empfangen'
}), 400
otp_code = data.get('otp_code', '').strip()
email = data.get('email', '').strip() # Optional für zusätzliche Verifikation
if not otp_code:
return jsonify({
'success': False,
'message': 'OTP-Code ist erforderlich'
}), 400
db_session = get_db_session()
# Alle Gastaufträge finden, die den OTP-Code haben könnten
# Da OTP gehashed ist, müssen wir durch alle iterieren
guest_requests = db_session.query(GuestRequest).filter(
GuestRequest.otp_code.isnot(None)
).all()
found_request = None
for request_obj in guest_requests:
if request_obj.verify_otp(otp_code):
# Zusätzliche E-Mail-Verifikation falls angegeben
if email and request_obj.email.lower() != email.lower():
continue
found_request = request_obj
break
if not found_request:
db_session.close()
app_logger.warning(f"Ungültiger OTP-Code für Gast-Status-Abfrage: {otp_code[:4]}****")
return jsonify({
'success': False,
'message': 'Ungültiger Code oder E-Mail-Adresse'
}), 404
# Status-Informationen für den Gast zusammenstellen
status_info = {
'id': found_request.id,
'name': found_request.name,
'file_name': found_request.file_name,
'status': found_request.status,
'created_at': found_request.created_at.isoformat() if found_request.created_at else None,
'updated_at': found_request.updated_at.isoformat() if found_request.updated_at else None,
'duration_minutes': found_request.duration_minutes,
'copies': found_request.copies,
'reason': found_request.reason
}
# Status-spezifische Informationen hinzufügen
if found_request.status == 'approved':
status_info.update({
'approved_at': found_request.approved_at.isoformat() if found_request.approved_at else None,
'approval_notes': found_request.approval_notes,
'message': 'Ihr Auftrag wurde genehmigt! Sie können mit dem Drucken beginnen.'
})
elif found_request.status == 'rejected':
status_info.update({
'rejected_at': found_request.rejected_at.isoformat() if found_request.rejected_at else None,
'rejection_reason': found_request.rejection_reason,
'message': 'Ihr Auftrag wurde leider abgelehnt.'
})
elif found_request.status == 'pending':
# Berechne wie lange der Auftrag schon wartet
if found_request.created_at:
waiting_time = datetime.now() - found_request.created_at
hours_waiting = int(waiting_time.total_seconds() / 3600)
status_info.update({
'hours_waiting': hours_waiting,
'message': f'Ihr Auftrag wird bearbeitet. Wartezeit: {hours_waiting} Stunden.'
})
else:
status_info['message'] = 'Ihr Auftrag wird bearbeitet.'
db_session.commit() # OTP als verwendet markieren
db_session.close()
app_logger.info(f"Gast-Status-Abfrage erfolgreich für Request {found_request.id}")
return jsonify({
'success': True,
'request': status_info
})
except Exception as e:
app_logger.error(f"Fehler bei Gast-Status-Abfrage: {str(e)}")
return jsonify({
'success': False,
'message': 'Fehler beim Abrufen des Status'
}), 500
@app.route('/guest-status')
def guest_status_page():
"""
Öffentliche Seite für Gäste um ihren Auftragsstatus zu prüfen.
"""
return render_template('guest_status.html')
@app.route('/api/admin/guest-requests', methods=['GET'])
@admin_required
def get_admin_guest_requests():
@ -3374,32 +3509,37 @@ def approve_guest_request(request_id):
if printer:
guest_request.assigned_printer_id = printer_id
# OTP-Code generieren für den Gast
import secrets
otp_code = ''.join([str(secrets.randbelow(10)) for _ in range(6)])
guest_request.otp_code = otp_code
guest_request.otp_expires_at = datetime.now() + timedelta(hours=24)
# OTP-Code generieren falls noch nicht vorhanden (nutze die Methode aus models.py)
otp_code = None
if not guest_request.otp_code:
otp_code = guest_request.generate_otp()
guest_request.otp_expires_at = datetime.now() + timedelta(hours=48) # 48h gültig
db_session.commit()
# Benachrichtigung an den Gast senden (falls E-Mail verfügbar)
if guest_request.email:
if guest_request.email and otp_code:
try:
# Hier würde normalerweise eine E-Mail gesendet werden
app_logger.info(f"E-Mail-Benachrichtigung würde an {guest_request.email} gesendet (OTP: {otp_code})")
app_logger.info(f"Genehmigungs-E-Mail würde an {guest_request.email} gesendet (OTP für Status-Abfrage verfügbar)")
except Exception as e:
app_logger.warning(f"Fehler beim Senden der E-Mail-Benachrichtigung: {str(e)}")
db_session.close()
app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} genehmigt (OTP: {otp_code})")
app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} genehmigt")
return jsonify({
response_data = {
'success': True,
'message': 'Gastauftrag erfolgreich genehmigt',
'otp_code': otp_code,
'expires_at': (datetime.now() + timedelta(hours=24)).isoformat()
})
'message': 'Gastauftrag erfolgreich genehmigt'
}
# OTP-Code nur zurückgeben wenn er neu generiert wurde (für Admin-Info)
if otp_code:
response_data['otp_code_generated'] = True
response_data['status_check_url'] = url_for('guest_status_page', _external=True)
return jsonify(response_data)
except Exception as e:
app_logger.error(f"Fehler beim Genehmigen des Gastauftrags {request_id}: {str(e)}")
@ -4065,7 +4205,7 @@ def get_validation_js():
response.headers['Cache-Control'] = 'public, max-age=3600' # 1 Stunde Cache
return response
except Exception as e:
logger.error(f"Fehler beim Laden des Validierungs-JS: {str(e)}")
app_logger.error(f"Fehler beim Laden des Validierungs-JS: {str(e)}")
return "console.error('Validierungs-JavaScript konnte nicht geladen werden');", 500
@app.route('/api/validation/validate-form', methods=['POST'])
@ -4099,7 +4239,7 @@ def validate_form_api():
})
except Exception as e:
logger.error(f"Fehler bei Formular-Validierung: {str(e)}")
app_logger.error(f"Fehler bei Formular-Validierung: {str(e)}")
return jsonify({'success': False, 'error': str(e)}), 500
# ===== REPORT GENERATOR API =====
@ -4171,7 +4311,7 @@ def generate_report():
return jsonify({'error': 'Report-Generierung fehlgeschlagen'}), 500
except Exception as e:
logger.error(f"Fehler bei Report-Generierung: {str(e)}")
app_logger.error(f"Fehler bei Report-Generierung: {str(e)}")
return jsonify({'error': str(e)}), 500
# ===== REALTIME DASHBOARD API =====
@ -4183,7 +4323,7 @@ def get_dashboard_config():
config = dashboard_manager.get_dashboard_config(current_user.id)
return jsonify(config)
except Exception as e:
logger.error(f"Fehler beim Laden der Dashboard-Konfiguration: {str(e)}")
app_logger.error(f"Fehler beim Laden der Dashboard-Konfiguration: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/dashboard/widgets/<widget_id>/data', methods=['GET'])
@ -4198,7 +4338,7 @@ def get_widget_data(widget_id):
'timestamp': datetime.now().isoformat()
})
except Exception as e:
logger.error(f"Fehler beim Laden der Widget-Daten für {widget_id}: {str(e)}")
app_logger.error(f"Fehler beim Laden der Widget-Daten für {widget_id}: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/dashboard/emit-event', methods=['POST'])
@ -4223,7 +4363,7 @@ def emit_dashboard_event():
return jsonify({'success': True})
except Exception as e:
logger.error(f"Fehler beim Senden des Dashboard-Ereignisses: {str(e)}")
app_logger.error(f"Fehler beim Senden des Dashboard-Ereignisses: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/dashboard/client-js', methods=['GET'])
@ -4236,7 +4376,7 @@ def get_dashboard_js():
response.headers['Cache-Control'] = 'public, max-age=1800' # 30 Minuten Cache
return response
except Exception as e:
logger.error(f"Fehler beim Laden des Dashboard-JS: {str(e)}")
app_logger.error(f"Fehler beim Laden des Dashboard-JS: {str(e)}")
return "console.error('Dashboard-JavaScript konnte nicht geladen werden');", 500
# ===== DRAG & DROP API =====
@ -4270,7 +4410,7 @@ def update_job_order():
return jsonify({'error': 'Fehler beim Aktualisieren der Job-Reihenfolge'}), 500
except Exception as e:
logger.error(f"Fehler beim Aktualisieren der Job-Reihenfolge: {str(e)}")
app_logger.error(f"Fehler beim Aktualisieren der Job-Reihenfolge: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/dragdrop/get-job-order/<int:printer_id>', methods=['GET'])
@ -4300,7 +4440,7 @@ def get_job_order_api(printer_id):
})
except Exception as e:
logger.error(f"Fehler beim Abrufen der Job-Reihenfolge: {str(e)}")
app_logger.error(f"Fehler beim Abrufen der Job-Reihenfolge: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/dragdrop/upload-session', methods=['POST'])
@ -4318,7 +4458,7 @@ def create_upload_session():
})
except Exception as e:
logger.error(f"Fehler beim Erstellen der Upload-Session: {str(e)}")
app_logger.error(f"Fehler beim Erstellen der Upload-Session: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/dragdrop/upload-progress/<session_id>', methods=['GET'])
@ -4329,7 +4469,7 @@ def get_upload_progress(session_id):
progress = drag_drop_manager.get_session_progress(session_id)
return jsonify(progress)
except Exception as e:
logger.error(f"Fehler beim Abrufen des Upload-Progress: {str(e)}")
app_logger.error(f"Fehler beim Abrufen des Upload-Progress: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/dragdrop/client-js', methods=['GET'])
@ -4342,7 +4482,7 @@ def get_dragdrop_js():
response.headers['Cache-Control'] = 'public, max-age=3600'
return response
except Exception as e:
logger.error(f"Fehler beim Laden des Drag & Drop JS: {str(e)}")
app_logger.error(f"Fehler beim Laden des Drag & Drop JS: {str(e)}")
return "console.error('Drag & Drop JavaScript konnte nicht geladen werden');", 500
@app.route('/api/dragdrop/client-css', methods=['GET'])
@ -4355,7 +4495,7 @@ def get_dragdrop_css():
response.headers['Cache-Control'] = 'public, max-age=3600'
return response
except Exception as e:
logger.error(f"Fehler beim Laden des Drag & Drop CSS: {str(e)}")
app_logger.error(f"Fehler beim Laden des Drag & Drop CSS: {str(e)}")
return "/* Drag & Drop CSS konnte nicht geladen werden */", 500
# ===== ADVANCED TABLES API =====
@ -4422,7 +4562,7 @@ def query_advanced_table():
return jsonify(result)
except Exception as e:
logger.error(f"Fehler bei erweiterte Tabellen-Abfrage: {str(e)}")
app_logger.error(f"Fehler bei erweiterte Tabellen-Abfrage: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/tables/export', methods=['POST'])
@ -4435,29 +4575,135 @@ def export_table_data():
export_format = data.get('format', 'csv')
query_params = data.get('query', {})
# Hier würde die Export-Logik implementiert
# Für jetzt einfache CSV-Export-Simulation
# Vollständige Export-Logik implementierung
app_logger.info(f"📊 Starte Tabellen-Export: {table_type} als {export_format}")
# Tabellen-Konfiguration basierend auf Typ erstellen
if table_type == 'jobs':
config = create_table_config(
'jobs',
['id', 'filename', 'status', 'printer_name', 'user_name', 'created_at', 'completed_at'],
base_query='Job'
)
elif table_type == 'printers':
config = create_table_config(
'printers',
['id', 'name', 'ip_address', 'status', 'location', 'model'],
base_query='Printer'
)
elif table_type == 'users':
config = create_table_config(
'users',
['id', 'name', 'email', 'role', 'active', 'last_login'],
base_query='User'
)
else:
return jsonify({'error': 'Unbekannter Tabellen-Typ für Export'}), 400
# Erweiterte Abfrage für Export-Daten erstellen
query_builder = AdvancedTableQuery(config)
# Filter aus Query-Parametern anwenden
if 'filters' in query_params:
for filter_data in query_params['filters']:
query_builder.add_filter(
filter_data['column'],
filter_data['operator'],
filter_data['value']
)
# Sortierung anwenden
if 'sort' in query_params:
query_builder.set_sorting(
query_params['sort']['column'],
query_params['sort']['direction']
)
# Für Export: Alle Daten ohne Paginierung
query_builder.set_pagination(1, 10000) # Maximale Anzahl für Export
# Daten abrufen
result = query_builder.execute()
export_data = result.get('data', [])
if export_format == 'csv':
import csv
import io
# CSV-Export implementierung
output = io.StringIO()
writer = csv.writer(output)
writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_MINIMAL)
# Beispiel-Daten (würde durch echte Abfrage ersetzt)
writer.writerow(['ID', 'Name', 'Status', 'Erstellt'])
writer.writerow([1, 'Beispiel Job', 'Aktiv', '2025-01-07'])
# Header-Zeile schreiben
if export_data:
headers = list(export_data[0].keys())
writer.writerow(headers)
response = make_response(output.getvalue())
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export.csv"'
# Daten-Zeilen schreiben
for row in export_data:
# Werte für CSV formatieren
formatted_row = []
for value in row.values():
if value is None:
formatted_row.append('')
elif isinstance(value, datetime):
formatted_row.append(value.strftime('%d.%m.%Y %H:%M:%S'))
else:
formatted_row.append(str(value))
writer.writerow(formatted_row)
# Response erstellen
csv_content = output.getvalue()
output.close()
response = make_response(csv_content)
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv"'
app_logger.info(f"✅ CSV-Export erfolgreich: {len(export_data)} Datensätze")
return response
return jsonify({'error': 'Export-Format nicht unterstützt'}), 400
elif export_format == 'json':
# JSON-Export implementierung
json_content = json.dumps(export_data, indent=2, default=str, ensure_ascii=False)
response = make_response(json_content)
response.headers['Content-Type'] = 'application/json; charset=utf-8'
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json"'
app_logger.info(f"✅ JSON-Export erfolgreich: {len(export_data)} Datensätze")
return response
elif export_format == 'excel':
# Excel-Export implementierung (falls openpyxl verfügbar)
try:
import openpyxl
from openpyxl.utils.dataframe import dataframe_to_rows
import pandas as pd
# DataFrame erstellen
df = pd.DataFrame(export_data)
# Excel-Datei in Memory erstellen
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name=table_type.capitalize(), index=False)
output.seek(0)
response = make_response(output.getvalue())
response.headers['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx"'
app_logger.info(f"✅ Excel-Export erfolgreich: {len(export_data)} Datensätze")
return response
except ImportError:
app_logger.warning("⚠️ Excel-Export nicht verfügbar - openpyxl/pandas fehlt")
return jsonify({'error': 'Excel-Export nicht verfügbar - erforderliche Bibliotheken fehlen'}), 400
except Exception as e:
logger.error(f"Fehler beim Tabellen-Export: {str(e)}")
app_logger.error(f"Fehler beim Tabellen-Export: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/tables/client-js', methods=['GET'])
@ -4470,7 +4716,7 @@ def get_tables_js():
response.headers['Cache-Control'] = 'public, max-age=3600'
return response
except Exception as e:
logger.error(f"Fehler beim Laden des Tables-JS: {str(e)}")
app_logger.error(f"Fehler beim Laden des Tables-JS: {str(e)}")
return "console.error('Advanced Tables JavaScript konnte nicht geladen werden');", 500
@app.route('/api/tables/client-css', methods=['GET'])
@ -4483,7 +4729,7 @@ def get_tables_css():
response.headers['Cache-Control'] = 'public, max-age=3600'
return response
except Exception as e:
logger.error(f"Fehler beim Laden des Tables-CSS: {str(e)}")
app_logger.error(f"Fehler beim Laden des Tables-CSS: {str(e)}")
return "/* Advanced Tables CSS konnte nicht geladen werden */", 500
# ===== MAINTENANCE SYSTEM API =====
@ -4508,7 +4754,7 @@ def maintenance_tasks():
})
except Exception as e:
logger.error(f"Fehler beim Abrufen der Wartungsaufgaben: {str(e)}")
app_logger.error(f"Fehler beim Abrufen der Wartungsaufgaben: {str(e)}")
return jsonify({'error': str(e)}), 500
elif request.method == 'POST':
@ -4542,7 +4788,7 @@ def maintenance_tasks():
return jsonify({'error': 'Fehler beim Erstellen der Wartungsaufgabe'}), 500
except Exception as e:
logger.error(f"Fehler beim Erstellen der Wartungsaufgabe: {str(e)}")
app_logger.error(f"Fehler beim Erstellen der Wartungsaufgabe: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/maintenance/tasks/<int:task_id>/status', methods=['PUT'])
@ -4570,7 +4816,7 @@ def update_maintenance_task_status(task_id):
return jsonify({'error': 'Fehler beim Aktualisieren des Status'}), 500
except Exception as e:
logger.error(f"Fehler beim Aktualisieren des Wartungsaufgaben-Status: {str(e)}")
app_logger.error(f"Fehler beim Aktualisieren des Wartungsaufgaben-Status: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/maintenance/overview', methods=['GET'])
@ -4581,7 +4827,7 @@ def get_maintenance_overview():
overview = get_maintenance_overview()
return jsonify(overview)
except Exception as e:
logger.error(f"Fehler beim Abrufen der Wartungs-Übersicht: {str(e)}")
app_logger.error(f"Fehler beim Abrufen der Wartungs-Übersicht: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/maintenance/schedule', methods=['POST'])
@ -4609,7 +4855,7 @@ def schedule_maintenance_api():
return jsonify({'error': 'Fehler beim Erstellen des Wartungsplans'}), 500
except Exception as e:
logger.error(f"Fehler beim Planen der Wartung: {str(e)}")
app_logger.error(f"Fehler beim Planen der Wartung: {str(e)}")
return jsonify({'error': str(e)}), 500
# ===== MULTI-LOCATION SYSTEM API =====
@ -4631,7 +4877,7 @@ def locations():
})
except Exception as e:
logger.error(f"Fehler beim Abrufen der Standorte: {str(e)}")
app_logger.error(f"Fehler beim Abrufen der Standorte: {str(e)}")
return jsonify({'error': str(e)}), 500
elif request.method == 'POST':
@ -4657,7 +4903,7 @@ def locations():
return jsonify({'error': 'Fehler beim Erstellen des Standorts'}), 500
except Exception as e:
logger.error(f"Fehler beim Erstellen des Standorts: {str(e)}")
app_logger.error(f"Fehler beim Erstellen des Standorts: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/locations/<int:location_id>/users', methods=['GET', 'POST'])
@ -4675,7 +4921,7 @@ def location_users(location_id):
})
except Exception as e:
logger.error(f"Fehler beim Abrufen der Standort-Benutzer: {str(e)}")
app_logger.error(f"Fehler beim Abrufen der Standort-Benutzer: {str(e)}")
return jsonify({'error': str(e)}), 500
elif request.method == 'POST':
@ -4698,7 +4944,7 @@ def location_users(location_id):
return jsonify({'error': 'Fehler bei der Benutzer-Zuweisung'}), 500
except Exception as e:
logger.error(f"Fehler bei der Benutzer-Zuweisung: {str(e)}")
app_logger.error(f"Fehler bei der Benutzer-Zuweisung: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/locations/user/<int:user_id>', methods=['GET'])
@ -4718,7 +4964,7 @@ def get_user_locations_api(user_id):
})
except Exception as e:
logger.error(f"Fehler beim Abrufen der Benutzer-Standorte: {str(e)}")
app_logger.error(f"Fehler beim Abrufen der Benutzer-Standorte: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/locations/distance', methods=['POST'])
@ -4741,7 +4987,7 @@ def calculate_distance_api():
})
except Exception as e:
logger.error(f"Fehler bei Entfernungsberechnung: {str(e)}")
app_logger.error(f"Fehler bei Entfernungsberechnung: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/locations/nearest', methods=['POST'])
@ -4776,10 +5022,47 @@ def find_nearest_location_api():
})
except Exception as e:
logger.error(f"Fehler bei der Suche nach nächstem Standort: {str(e)}")
app_logger.error(f"Fehler bei der Suche nach nächstem Standort: {str(e)}")
return jsonify({'error': str(e)}), 500
# ===== GASTANTRÄGE API-ROUTEN =====
def setup_database_with_migrations():
"""
Datenbank initialisieren und alle erforderlichen Tabellen erstellen.
Führt Migrationen für neue Tabellen wie JobOrder durch.
"""
try:
app_logger.info("🔄 Starte Datenbank-Setup und Migrationen...")
# Standard-Datenbank-Initialisierung
init_database()
# Explizite Migration für JobOrder-Tabelle
engine = get_engine()
# Erstelle alle Tabellen (nur neue werden tatsächlich erstellt)
Base.metadata.create_all(engine)
# Prüfe ob JobOrder-Tabelle existiert
from sqlalchemy import inspect
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
if 'job_orders' in existing_tables:
app_logger.info("✅ JobOrder-Tabelle bereits vorhanden")
else:
# Tabelle manuell erstellen
JobOrder.__table__.create(engine, checkfirst=True)
app_logger.info("✅ JobOrder-Tabelle erfolgreich erstellt")
# Initial-Admin erstellen falls nicht vorhanden
create_initial_admin()
app_logger.info("✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen")
except Exception as e:
app_logger.error(f"❌ Fehler bei Datenbank-Setup: {str(e)}")
raise e
# ===== STARTUP UND MAIN =====
if __name__ == "__main__":
@ -5008,100 +5291,3 @@ if __name__ == "__main__":
except:
pass
sys.exit(1)
def setup_database_with_migrations():
"""
Datenbank initialisieren und alle erforderlichen Tabellen erstellen.
Führt Migrationen für neue Tabellen wie JobOrder durch.
"""
try:
app_logger.info("🔄 Starte Datenbank-Setup und Migrationen...")
# Standard-Datenbank-Initialisierung
init_database()
# Explizite Migration für JobOrder-Tabelle
engine = get_engine()
# Erstelle alle Tabellen (nur neue werden tatsächlich erstellt)
Base.metadata.create_all(engine)
# Prüfe ob JobOrder-Tabelle existiert
from sqlalchemy import inspect
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
if 'job_orders' in existing_tables:
app_logger.info("✅ JobOrder-Tabelle bereits vorhanden")
else:
# Tabelle manuell erstellen
JobOrder.__table__.create(engine, checkfirst=True)
app_logger.info("✅ JobOrder-Tabelle erfolgreich erstellt")
# Initial-Admin erstellen falls nicht vorhanden
create_initial_admin()
app_logger.info("✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen")
except Exception as e:
app_logger.error(f"❌ Fehler bei Datenbank-Setup: {str(e)}")
raise e
@app.route("/admin/printers/<int:printer_id>/settings")
@login_required
def admin_printer_settings_page(printer_id):
"""Zeigt die Drucker-Einstellungsseite an."""
if not current_user.is_admin:
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
return redirect(url_for("index"))
db_session = get_db_session()
try:
printer = db_session.get(Printer, printer_id)
if not printer:
flash("Drucker nicht gefunden.", "error")
return redirect(url_for("admin_page"))
printer_data = {
"id": printer.id,
"name": printer.name,
"model": printer.model or 'Unbekanntes Modell',
"location": printer.location or 'Unbekannter Standort',
"mac_address": printer.mac_address,
"plug_ip": printer.plug_ip,
"status": printer.status or "offline",
"active": printer.active if hasattr(printer, 'active') else True,
"created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat()
}
db_session.close()
return render_template("admin_printer_settings.html", printer=printer_data)
except Exception as e:
db_session.close()
app_logger.error(f"Fehler beim Laden der Drucker-Einstellungen: {str(e)}")
flash("Fehler beim Laden der Drucker-Daten.", "error")
return redirect(url_for("admin_page"))
# Erstelle alle Tabellen (nur neue werden tatsächlich erstellt)
Base.metadata.create_all(engine)
# Prüfe ob JobOrder-Tabelle existiert
from sqlalchemy import inspect
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
if 'job_orders' in existing_tables:
app_logger.info("✅ JobOrder-Tabelle bereits vorhanden")
else:
# Tabelle manuell erstellen
JobOrder.__table__.create(engine, checkfirst=True)
app_logger.info("✅ JobOrder-Tabelle erfolgreich erstellt")
# Initial-Admin erstellen falls nicht vorhanden
create_initial_admin()
app_logger.info("✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen")
except Exception as e:
app_logger.error(f"❌ Fehler bei Datenbank-Setup: {str(e)}")
raise e

View File

@ -0,0 +1 @@

1
backend/static/js/charts/chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,412 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gastauftrag Status-Abfrage - Mercedes-Benz TBA Marienfelde</title>
<link href="{{ url_for('static', filename='css/output.css') }}" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<style>
.status-card {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border: 1px solid #e2e8f0;
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.dark .status-card {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-color: #334155;
}
.otp-input {
font-family: 'Courier New', monospace;
font-size: 1.5rem;
text-align: center;
letter-spacing: 0.5rem;
text-transform: uppercase;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 9999px;
font-weight: 500;
font-size: 0.875rem;
}
.status-pending { background-color: #fef3c7; color: #92400e; }
.status-approved { background-color: #d1fae5; color: #065f46; }
.status-rejected { background-color: #fee2e2; color: #991b1b; }
.loading-spinner {
border: 2px solid #f3f4f6;
border-top: 2px solid #0073ce;
border-radius: 50%;
width: 1rem;
height: 1rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-gray-50 font-mercedes">
<div class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-md w-full space-y-8">
<!-- Header -->
<div class="text-center">
<div class="mx-auto h-12 w-12 bg-mercedes-blue rounded-full flex items-center justify-center">
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<h2 class="mt-6 text-3xl font-bold text-gray-900">
Auftragsstatus prüfen
</h2>
<p class="mt-2 text-sm text-gray-600">
Geben Sie Ihren Statuscode ein, um Informationen über Ihren Gastauftrag zu erhalten
</p>
</div>
<!-- Status-Abfrage-Formular -->
<div class="status-card p-6" id="query-form">
<form id="status-form" class="space-y-6">
<div>
<label for="otp_code" class="block text-sm font-medium text-gray-700">
Statuscode (16 Zeichen)
</label>
<input type="text"
id="otp_code"
name="otp_code"
maxlength="16"
class="otp-input mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-mercedes-blue focus:border-mercedes-blue focus:z-10 sm:text-sm"
placeholder="XXXXXXXXXXXXXXXX"
required>
<p class="mt-1 text-xs text-gray-500">
Der Code wurde Ihnen bei der Antragsstellung mitgeteilt
</p>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">
E-Mail-Adresse (optional)
</label>
<input type="email"
id="email"
name="email"
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-mercedes-blue focus:border-mercedes-blue focus:z-10 sm:text-sm"
placeholder="ihre.email@example.com">
<p class="mt-1 text-xs text-gray-500">
Zusätzliche Sicherheit (empfohlen)
</p>
</div>
<div>
<button type="submit"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-mercedes-blue hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<span id="submit-text">Status prüfen</span>
<span id="loading-spinner" class="loading-spinner ml-2 hidden"></span>
</button>
</div>
</form>
</div>
<!-- Status-Ergebnis -->
<div id="status-result" class="hidden">
<div class="status-card p-6">
<div id="status-content">
<!-- Wird per JavaScript gefüllt -->
</div>
<div class="mt-6 flex gap-3">
<button onclick="resetForm()"
class="flex-1 py-2 px-4 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Neue Abfrage
</button>
<button onclick="refreshStatus()"
class="flex-1 py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-mercedes-blue hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Aktualisieren
</button>
</div>
</div>
</div>
<!-- Fehleranzeige -->
<div id="error-message" class="hidden">
<div class="status-card p-6 border-l-4 border-red-500">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
<div class="mt-2 text-sm text-red-700" id="error-text">
<!-- Wird per JavaScript gefüllt -->
</div>
</div>
</div>
<div class="mt-4">
<button onclick="resetForm()"
class="py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Erneut versuchen
</button>
</div>
</div>
</div>
<!-- Footer -->
<div class="text-center">
<p class="text-xs text-gray-500">
Mercedes-Benz Technische Berufsausbildung Marienfelde<br>
<a href="/guest/request" class="text-mercedes-blue hover:underline">Neuen Antrag stellen</a>
</p>
</div>
</div>
</div>
<script>
let currentRequestData = null;
// Formular-Submit-Handler
document.getElementById('status-form').addEventListener('submit', async function(e) {
e.preventDefault();
const otpCode = document.getElementById('otp_code').value.trim();
const email = document.getElementById('email').value.trim();
if (!otpCode) {
showError('Bitte geben Sie Ihren Statuscode ein.');
return;
}
if (otpCode.length !== 16) {
showError('Der Statuscode muss genau 16 Zeichen lang sein.');
return;
}
setLoading(true);
hideAll();
try {
const response = await fetch('/guest/api/guest/status', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
otp_code: otpCode,
email: email || undefined
})
});
const data = await response.json();
if (data.success) {
currentRequestData = data.request;
showStatus(data.request);
} else {
showError(data.message || 'Ungültiger Code oder E-Mail-Adresse');
}
} catch (error) {
console.error('Fehler bei Status-Abfrage:', error);
showError('Verbindungsfehler. Bitte versuchen Sie es später erneut.');
} finally {
setLoading(false);
}
});
// Status anzeigen
function showStatus(request) {
const statusContent = document.getElementById('status-content');
// Status-Badge
let statusBadge = '';
let statusIcon = '';
switch (request.status) {
case 'pending':
statusBadge = '<span class="status-badge status-pending">🕒 In Bearbeitung</span>';
statusIcon = '🕒';
break;
case 'approved':
statusBadge = '<span class="status-badge status-approved">✅ Genehmigt</span>';
statusIcon = '✅';
break;
case 'rejected':
statusBadge = '<span class="status-badge status-rejected">❌ Abgelehnt</span>';
statusIcon = '❌';
break;
default:
statusBadge = '<span class="status-badge">❓ Unbekannt</span>';
statusIcon = '❓';
}
let html = `
<div class="text-center mb-6">
<div class="text-4xl mb-2">${statusIcon}</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">
Antrag von ${request.name}
</h3>
${statusBadge}
</div>
<div class="space-y-4">
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-medium text-gray-900 mb-2">Antragsdetails</h4>
<dl class="space-y-1 text-sm">
<div class="flex justify-between">
<dt class="text-gray-600">Erstellt am:</dt>
<dd class="text-gray-900">${formatDate(request.created_at)}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600">Dauer:</dt>
<dd class="text-gray-900">${request.duration_min} Minuten</dd>
</div>
${request.file_name ? `
<div class="flex justify-between">
<dt class="text-gray-600">Datei:</dt>
<dd class="text-gray-900">${request.file_name}</dd>
</div>
` : ''}
</dl>
</div>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p class="text-sm text-blue-800">
${request.message}
</p>
</div>
${request.status === 'approved' && request.can_start_job ? `
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h4 class="font-medium text-green-800 mb-2">🎯 Bereit zum Drucken!</h4>
<p class="text-sm text-green-700 mb-3">
Ihr Auftrag wurde genehmigt. Sie können mit dem 3D-Druck beginnen.
</p>
<a href="/guest/start-job"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700">
🚀 Jetzt drucken
</a>
</div>
` : ''}
${request.status === 'rejected' && request.rejection_reason ? `
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
<h4 class="font-medium text-red-800 mb-2">Ablehnungsgrund:</h4>
<p class="text-sm text-red-700">${request.rejection_reason}</p>
</div>
` : ''}
${request.job ? `
<div class="bg-indigo-50 border border-indigo-200 rounded-lg p-4">
<h4 class="font-medium text-indigo-800 mb-2">📋 Job-Informationen</h4>
<dl class="space-y-1 text-sm">
<div class="flex justify-between">
<dt class="text-indigo-600">Job-Name:</dt>
<dd class="text-indigo-900">${request.job.name}</dd>
</div>
<div class="flex justify-between">
<dt class="text-indigo-600">Status:</dt>
<dd class="text-indigo-900">${request.job.status}</dd>
</div>
${request.job.printer_name ? `
<div class="flex justify-between">
<dt class="text-indigo-600">Drucker:</dt>
<dd class="text-indigo-900">${request.job.printer_name}</dd>
</div>
` : ''}
</dl>
</div>
` : ''}
</div>
`;
statusContent.innerHTML = html;
document.getElementById('status-result').classList.remove('hidden');
}
// Fehler anzeigen
function showError(message) {
document.getElementById('error-text').textContent = message;
document.getElementById('error-message').classList.remove('hidden');
}
// Loading-Zustand setzen
function setLoading(loading) {
const submitText = document.getElementById('submit-text');
const loadingSpinner = document.getElementById('loading-spinner');
const submitButton = document.querySelector('button[type="submit"]');
if (loading) {
submitText.textContent = 'Prüfe...';
loadingSpinner.classList.remove('hidden');
submitButton.disabled = true;
} else {
submitText.textContent = 'Status prüfen';
loadingSpinner.classList.add('hidden');
submitButton.disabled = false;
}
}
// Alle Anzeigen ausblenden
function hideAll() {
document.getElementById('status-result').classList.add('hidden');
document.getElementById('error-message').classList.add('hidden');
}
// Formular zurücksetzen
function resetForm() {
document.getElementById('status-form').reset();
hideAll();
document.getElementById('query-form').classList.remove('hidden');
currentRequestData = null;
}
// Status aktualisieren
function refreshStatus() {
if (currentRequestData) {
const otpCode = document.getElementById('otp_code').value;
const email = document.getElementById('email').value;
// Formular erneut abschicken
document.getElementById('status-form').dispatchEvent(new Event('submit'));
}
}
// Datum formatieren
function formatDate(dateString) {
if (!dateString) return 'Unbekannt';
try {
const date = new Date(dateString);
return date.toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (error) {
return dateString;
}
}
// OTP-Input Formatierung
document.getElementById('otp_code').addEventListener('input', function(e) {
// Nur alphanumerische Zeichen erlauben
e.target.value = e.target.value.replace(/[^A-Fa-f0-9]/g, '').toUpperCase();
});
</script>
</body>
</html>

View File

@ -0,0 +1,229 @@
"""
Offline-Konfiguration für MYP-System
===================================
Konfiguriert das System für den Offline-Betrieb ohne Internetverbindung.
Stellt Fallback-Lösungen für internetabhängige Funktionen bereit.
"""
import os
import logging
from typing import Dict, List, Optional
from utils.logging_config import get_logger
logger = get_logger("offline_config")
# ===== OFFLINE-MODUS KONFIGURATION =====
OFFLINE_MODE = True # Produktionseinstellung - System läuft offline
# ===== OFFLINE-KOMPATIBILITÄT PRÜFUNGEN =====
def check_internet_connectivity() -> bool:
"""
Prüft ob eine Internetverbindung verfügbar ist.
Im Offline-Modus gibt immer False zurück.
Returns:
bool: True wenn Internet verfügbar, False im Offline-Modus
"""
if OFFLINE_MODE:
return False
# In einem echten Online-Modus könnte hier eine echte Prüfung stehen
try:
import socket
socket.create_connection(("8.8.8.8", 53), timeout=3)
return True
except OSError:
return False
def is_oauth_available() -> bool:
"""
Prüft ob OAuth-Funktionalität verfügbar ist.
Returns:
bool: False im Offline-Modus
"""
return not OFFLINE_MODE and check_internet_connectivity()
def is_email_sending_available() -> bool:
"""
Prüft ob E-Mail-Versand verfügbar ist.
Returns:
bool: False im Offline-Modus (nur Logging)
"""
return not OFFLINE_MODE and check_internet_connectivity()
def is_cdn_available() -> bool:
"""
Prüft ob CDN-Links verfügbar sind.
Returns:
bool: False im Offline-Modus (lokale Fallbacks verwenden)
"""
return not OFFLINE_MODE and check_internet_connectivity()
# ===== CDN FALLBACK-KONFIGURATION =====
CDN_FALLBACKS = {
# Chart.js CDN -> Lokale Datei
"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.min.js": "/static/js/charts/chart.min.js",
"https://cdn.jsdelivr.net/npm/chart.js": "/static/js/charts/chart.min.js",
# FontAwesome (bereits lokal verfügbar)
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css": "/static/fontawesome/css/all.min.css",
# Weitere CDN-Fallbacks können hier hinzugefügt werden
}
def get_local_asset_path(cdn_url: str) -> Optional[str]:
"""
Gibt den lokalen Pfad für eine CDN-URL zurück.
Args:
cdn_url: URL des CDN-Assets
Returns:
str: Lokaler Pfad oder None wenn kein Fallback verfügbar
"""
return CDN_FALLBACKS.get(cdn_url)
def replace_cdn_links(html_content: str) -> str:
"""
Ersetzt CDN-Links durch lokale Fallbacks im HTML-Inhalt.
Args:
html_content: HTML-Inhalt mit CDN-Links
Returns:
str: HTML-Inhalt mit lokalen Links
"""
if not OFFLINE_MODE:
return html_content
modified_content = html_content
for cdn_url, local_path in CDN_FALLBACKS.items():
if cdn_url in modified_content:
modified_content = modified_content.replace(cdn_url, local_path)
logger.info(f"🔄 CDN-Link ersetzt: {cdn_url} -> {local_path}")
return modified_content
# ===== SECURITY POLICY ANPASSUNGEN =====
def get_offline_csp_policy() -> Dict[str, List[str]]:
"""
Gibt CSP-Policy für Offline-Modus zurück.
Entfernt externe CDN-Domains aus der Policy.
Returns:
Dict: CSP-Policy ohne externe Domains
"""
if not OFFLINE_MODE:
# Online-Modus: Originale Policy mit CDNs
return {
"script-src": [
"'self'",
"'unsafe-inline'",
"'unsafe-eval'",
"https://cdn.jsdelivr.net",
"https://unpkg.com",
"https://cdnjs.cloudflare.com"
],
"style-src": [
"'self'",
"'unsafe-inline'",
"https://fonts.googleapis.com",
"https://cdn.jsdelivr.net"
],
"font-src": [
"'self'",
"https://fonts.gstatic.com"
]
}
else:
# Offline-Modus: Nur lokale Ressourcen
return {
"script-src": [
"'self'",
"'unsafe-inline'",
"'unsafe-eval'"
],
"style-src": [
"'self'",
"'unsafe-inline'"
],
"font-src": [
"'self'"
],
"img-src": [
"'self'",
"data:"
]
}
# ===== OFFLINE-MODUS HILFSFUNKTIONEN =====
def log_offline_mode_status():
"""Loggt den aktuellen Offline-Modus Status."""
if OFFLINE_MODE:
logger.info("🌐 System läuft im OFFLINE-MODUS")
logger.info(" ❌ OAuth deaktiviert")
logger.info(" ❌ E-Mail-Versand deaktiviert (nur Logging)")
logger.info(" ❌ CDN-Links werden durch lokale Dateien ersetzt")
logger.info(" ✅ Alle Kernfunktionen verfügbar")
else:
logger.info("🌐 System läuft im ONLINE-MODUS")
logger.info(" ✅ OAuth verfügbar")
logger.info(" ✅ E-Mail-Versand verfügbar")
logger.info(" ✅ CDN-Links verfügbar")
def get_feature_availability() -> Dict[str, bool]:
"""
Gibt die Verfügbarkeit verschiedener Features zurück.
Returns:
Dict: Feature-Verfügbarkeit
"""
return {
"oauth": is_oauth_available(),
"email_sending": is_email_sending_available(),
"cdn_resources": is_cdn_available(),
"offline_mode": OFFLINE_MODE,
"core_functionality": True, # Kernfunktionen immer verfügbar
"printer_control": True, # Drucker-Steuerung immer verfügbar
"job_management": True, # Job-Verwaltung immer verfügbar
"user_management": True # Benutzer-Verwaltung immer verfügbar
}
# ===== STARTUP-FUNKTIONEN =====
def initialize_offline_mode():
"""Initialisiert den Offline-Modus beim System-Start."""
log_offline_mode_status()
if OFFLINE_MODE:
logger.info("🔧 Initialisiere Offline-Modus-Anpassungen...")
# Prüfe ob lokale Chart.js verfügbar ist
chart_js_path = "static/js/charts/chart.min.js"
if not os.path.exists(chart_js_path):
logger.warning(f"⚠️ Lokale Chart.js nicht gefunden: {chart_js_path}")
logger.warning(" Diagramme könnten nicht funktionieren")
else:
logger.info(f"✅ Lokale Chart.js gefunden: {chart_js_path}")
# Prüfe weitere lokale Assets
fontawesome_path = "static/fontawesome/css/all.min.css"
if not os.path.exists(fontawesome_path):
logger.warning(f"⚠️ Lokale FontAwesome nicht gefunden: {fontawesome_path}")
else:
logger.info(f"✅ Lokale FontAwesome gefunden: {fontawesome_path}")
logger.info("✅ Offline-Modus erfolgreich initialisiert")
# Beim Import automatisch initialisieren
initialize_offline_mode()