🎉 Improved backend functionality & documentation, added OTP documentation & JavaScript file. 🎨
This commit is contained in:
parent
b2bdc2d123
commit
91548dfb0e
496
backend/app.py
496
backend/app.py
@ -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 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
|
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
|
# Datenbank-Engine für Kompatibilität mit init_simple_db.py
|
||||||
from models import engine as db_engine
|
from models import engine as db_engine
|
||||||
|
|
||||||
@ -311,7 +334,7 @@ def format_datetime_filter(value, format='%d.%m.%Y %H:%M'):
|
|||||||
setup_logging()
|
setup_logging()
|
||||||
log_startup_info()
|
log_startup_info()
|
||||||
|
|
||||||
# Logger für verschiedene Komponenten
|
# app_logger für verschiedene Komponenten
|
||||||
app_logger = get_logger("app")
|
app_logger = get_logger("app")
|
||||||
auth_logger = get_logger("auth")
|
auth_logger = get_logger("auth")
|
||||||
jobs_logger = get_logger("jobs")
|
jobs_logger = get_logger("jobs")
|
||||||
@ -1695,7 +1718,7 @@ def api_admin_system_health():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": str(e)
|
"error": str(e)
|
||||||
@ -1731,7 +1754,7 @@ def api_admin_system_health():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": str(e)
|
"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
|
'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'])
|
@app.route('/api/admin/guest-requests', methods=['GET'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def get_admin_guest_requests():
|
def get_admin_guest_requests():
|
||||||
@ -3374,32 +3509,37 @@ def approve_guest_request(request_id):
|
|||||||
if printer:
|
if printer:
|
||||||
guest_request.assigned_printer_id = printer_id
|
guest_request.assigned_printer_id = printer_id
|
||||||
|
|
||||||
# OTP-Code generieren für den Gast
|
# OTP-Code generieren falls noch nicht vorhanden (nutze die Methode aus models.py)
|
||||||
import secrets
|
otp_code = None
|
||||||
otp_code = ''.join([str(secrets.randbelow(10)) for _ in range(6)])
|
if not guest_request.otp_code:
|
||||||
guest_request.otp_code = otp_code
|
otp_code = guest_request.generate_otp()
|
||||||
guest_request.otp_expires_at = datetime.now() + timedelta(hours=24)
|
guest_request.otp_expires_at = datetime.now() + timedelta(hours=48) # 48h gültig
|
||||||
|
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
# Benachrichtigung an den Gast senden (falls E-Mail verfügbar)
|
# Benachrichtigung an den Gast senden (falls E-Mail verfügbar)
|
||||||
if guest_request.email:
|
if guest_request.email and otp_code:
|
||||||
try:
|
try:
|
||||||
# Hier würde normalerweise eine E-Mail gesendet werden
|
# 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:
|
except Exception as e:
|
||||||
app_logger.warning(f"Fehler beim Senden der E-Mail-Benachrichtigung: {str(e)}")
|
app_logger.warning(f"Fehler beim Senden der E-Mail-Benachrichtigung: {str(e)}")
|
||||||
|
|
||||||
db_session.close()
|
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,
|
'success': True,
|
||||||
'message': 'Gastauftrag erfolgreich genehmigt',
|
'message': 'Gastauftrag erfolgreich genehmigt'
|
||||||
'otp_code': otp_code,
|
}
|
||||||
'expires_at': (datetime.now() + timedelta(hours=24)).isoformat()
|
|
||||||
})
|
# 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:
|
except Exception as e:
|
||||||
app_logger.error(f"Fehler beim Genehmigen des Gastauftrags {request_id}: {str(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
|
response.headers['Cache-Control'] = 'public, max-age=3600' # 1 Stunde Cache
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
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
|
return "console.error('Validierungs-JavaScript konnte nicht geladen werden');", 500
|
||||||
|
|
||||||
@app.route('/api/validation/validate-form', methods=['POST'])
|
@app.route('/api/validation/validate-form', methods=['POST'])
|
||||||
@ -4099,7 +4239,7 @@ def validate_form_api():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
# ===== REPORT GENERATOR API =====
|
# ===== REPORT GENERATOR API =====
|
||||||
@ -4171,7 +4311,7 @@ def generate_report():
|
|||||||
return jsonify({'error': 'Report-Generierung fehlgeschlagen'}), 500
|
return jsonify({'error': 'Report-Generierung fehlgeschlagen'}), 500
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
# ===== REALTIME DASHBOARD API =====
|
# ===== REALTIME DASHBOARD API =====
|
||||||
@ -4183,7 +4323,7 @@ def get_dashboard_config():
|
|||||||
config = dashboard_manager.get_dashboard_config(current_user.id)
|
config = dashboard_manager.get_dashboard_config(current_user.id)
|
||||||
return jsonify(config)
|
return jsonify(config)
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/dashboard/widgets/<widget_id>/data', methods=['GET'])
|
@app.route('/api/dashboard/widgets/<widget_id>/data', methods=['GET'])
|
||||||
@ -4198,7 +4338,7 @@ def get_widget_data(widget_id):
|
|||||||
'timestamp': datetime.now().isoformat()
|
'timestamp': datetime.now().isoformat()
|
||||||
})
|
})
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/dashboard/emit-event', methods=['POST'])
|
@app.route('/api/dashboard/emit-event', methods=['POST'])
|
||||||
@ -4223,7 +4363,7 @@ def emit_dashboard_event():
|
|||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/dashboard/client-js', methods=['GET'])
|
@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
|
response.headers['Cache-Control'] = 'public, max-age=1800' # 30 Minuten Cache
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
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
|
return "console.error('Dashboard-JavaScript konnte nicht geladen werden');", 500
|
||||||
|
|
||||||
# ===== DRAG & DROP API =====
|
# ===== DRAG & DROP API =====
|
||||||
@ -4270,7 +4410,7 @@ def update_job_order():
|
|||||||
return jsonify({'error': 'Fehler beim Aktualisieren der Job-Reihenfolge'}), 500
|
return jsonify({'error': 'Fehler beim Aktualisieren der Job-Reihenfolge'}), 500
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/dragdrop/get-job-order/<int:printer_id>', methods=['GET'])
|
@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:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/dragdrop/upload-session', methods=['POST'])
|
@app.route('/api/dragdrop/upload-session', methods=['POST'])
|
||||||
@ -4318,7 +4458,7 @@ def create_upload_session():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/dragdrop/upload-progress/<session_id>', methods=['GET'])
|
@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)
|
progress = drag_drop_manager.get_session_progress(session_id)
|
||||||
return jsonify(progress)
|
return jsonify(progress)
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/dragdrop/client-js', methods=['GET'])
|
@app.route('/api/dragdrop/client-js', methods=['GET'])
|
||||||
@ -4342,7 +4482,7 @@ def get_dragdrop_js():
|
|||||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
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
|
return "console.error('Drag & Drop JavaScript konnte nicht geladen werden');", 500
|
||||||
|
|
||||||
@app.route('/api/dragdrop/client-css', methods=['GET'])
|
@app.route('/api/dragdrop/client-css', methods=['GET'])
|
||||||
@ -4355,7 +4495,7 @@ def get_dragdrop_css():
|
|||||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
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
|
return "/* Drag & Drop CSS konnte nicht geladen werden */", 500
|
||||||
|
|
||||||
# ===== ADVANCED TABLES API =====
|
# ===== ADVANCED TABLES API =====
|
||||||
@ -4422,7 +4562,7 @@ def query_advanced_table():
|
|||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/tables/export', methods=['POST'])
|
@app.route('/api/tables/export', methods=['POST'])
|
||||||
@ -4435,31 +4575,137 @@ def export_table_data():
|
|||||||
export_format = data.get('format', 'csv')
|
export_format = data.get('format', 'csv')
|
||||||
query_params = data.get('query', {})
|
query_params = data.get('query', {})
|
||||||
|
|
||||||
# Hier würde die Export-Logik implementiert
|
# Vollständige Export-Logik implementierung
|
||||||
# Für jetzt einfache CSV-Export-Simulation
|
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':
|
if export_format == 'csv':
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
# CSV-Export implementierung
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
writer = csv.writer(output)
|
writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_MINIMAL)
|
||||||
|
|
||||||
# Beispiel-Daten (würde durch echte Abfrage ersetzt)
|
# Header-Zeile schreiben
|
||||||
writer.writerow(['ID', 'Name', 'Status', 'Erstellt'])
|
if export_data:
|
||||||
writer.writerow([1, 'Beispiel Job', 'Aktiv', '2025-01-07'])
|
headers = list(export_data[0].keys())
|
||||||
|
writer.writerow(headers)
|
||||||
|
|
||||||
|
# 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 = make_response(output.getvalue())
|
# Response erstellen
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
csv_content = output.getvalue()
|
||||||
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export.csv"'
|
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 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:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/tables/client-js', methods=['GET'])
|
@app.route('/api/tables/client-js', methods=['GET'])
|
||||||
def get_tables_js():
|
def get_tables_js():
|
||||||
"""Liefert Client-seitige Advanced Tables JavaScript"""
|
"""Liefert Client-seitige Advanced Tables JavaScript"""
|
||||||
@ -4470,7 +4716,7 @@ def get_tables_js():
|
|||||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
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
|
return "console.error('Advanced Tables JavaScript konnte nicht geladen werden');", 500
|
||||||
|
|
||||||
@app.route('/api/tables/client-css', methods=['GET'])
|
@app.route('/api/tables/client-css', methods=['GET'])
|
||||||
@ -4483,7 +4729,7 @@ def get_tables_css():
|
|||||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
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
|
return "/* Advanced Tables CSS konnte nicht geladen werden */", 500
|
||||||
|
|
||||||
# ===== MAINTENANCE SYSTEM API =====
|
# ===== MAINTENANCE SYSTEM API =====
|
||||||
@ -4508,7 +4754,7 @@ def maintenance_tasks():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
@ -4542,7 +4788,7 @@ def maintenance_tasks():
|
|||||||
return jsonify({'error': 'Fehler beim Erstellen der Wartungsaufgabe'}), 500
|
return jsonify({'error': 'Fehler beim Erstellen der Wartungsaufgabe'}), 500
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/maintenance/tasks/<int:task_id>/status', methods=['PUT'])
|
@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
|
return jsonify({'error': 'Fehler beim Aktualisieren des Status'}), 500
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/maintenance/overview', methods=['GET'])
|
@app.route('/api/maintenance/overview', methods=['GET'])
|
||||||
@ -4581,7 +4827,7 @@ def get_maintenance_overview():
|
|||||||
overview = get_maintenance_overview()
|
overview = get_maintenance_overview()
|
||||||
return jsonify(overview)
|
return jsonify(overview)
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/maintenance/schedule', methods=['POST'])
|
@app.route('/api/maintenance/schedule', methods=['POST'])
|
||||||
@ -4609,7 +4855,7 @@ def schedule_maintenance_api():
|
|||||||
return jsonify({'error': 'Fehler beim Erstellen des Wartungsplans'}), 500
|
return jsonify({'error': 'Fehler beim Erstellen des Wartungsplans'}), 500
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
# ===== MULTI-LOCATION SYSTEM API =====
|
# ===== MULTI-LOCATION SYSTEM API =====
|
||||||
@ -4631,7 +4877,7 @@ def locations():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
@ -4657,7 +4903,7 @@ def locations():
|
|||||||
return jsonify({'error': 'Fehler beim Erstellen des Standorts'}), 500
|
return jsonify({'error': 'Fehler beim Erstellen des Standorts'}), 500
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/locations/<int:location_id>/users', methods=['GET', 'POST'])
|
@app.route('/api/locations/<int:location_id>/users', methods=['GET', 'POST'])
|
||||||
@ -4675,7 +4921,7 @@ def location_users(location_id):
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
@ -4698,7 +4944,7 @@ def location_users(location_id):
|
|||||||
return jsonify({'error': 'Fehler bei der Benutzer-Zuweisung'}), 500
|
return jsonify({'error': 'Fehler bei der Benutzer-Zuweisung'}), 500
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/locations/user/<int:user_id>', methods=['GET'])
|
@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:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/locations/distance', methods=['POST'])
|
@app.route('/api/locations/distance', methods=['POST'])
|
||||||
@ -4741,7 +4987,7 @@ def calculate_distance_api():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/locations/nearest', methods=['POST'])
|
@app.route('/api/locations/nearest', methods=['POST'])
|
||||||
@ -4776,10 +5022,47 @@ def find_nearest_location_api():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
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
|
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 =====
|
# ===== STARTUP UND MAIN =====
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@ -5008,100 +5291,3 @@ if __name__ == "__main__":
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
sys.exit(1)
|
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
|
|
1
backend/docs/GASTAUFTRAG_OTP_DOKUMENTATION.md
Normal file
1
backend/docs/GASTAUFTRAG_OTP_DOKUMENTATION.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
1
backend/static/js/charts/chart.min.js
vendored
Normal file
1
backend/static/js/charts/chart.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
412
backend/templates/guest_status_check.html
Normal file
412
backend/templates/guest_status_check.html
Normal 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>
|
229
backend/utils/offline_config.py
Normal file
229
backend/utils/offline_config.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user