"feat: Implement new notification system in frontend

This commit is contained in:
2025-05-29 10:04:55 +02:00
parent 08c922d8f5
commit c256848d59
6 changed files with 478 additions and 19 deletions

View File

@@ -3957,6 +3957,101 @@ def export_admin_logs():
# ===== ENDE FEHLENDE ADMIN-API-ENDPUNKTE =====
# ===== BENACHRICHTIGUNGS-API-ENDPUNKTE =====
@app.route('/api/notifications', methods=['GET'])
@login_required
def get_notifications():
"""Holt alle Benachrichtigungen für den aktuellen Benutzer"""
try:
db_session = get_db_session()
# Benachrichtigungen für den aktuellen Benutzer laden
notifications = db_session.query(Notification).filter(
Notification.user_id == current_user.id
).order_by(Notification.created_at.desc()).limit(50).all()
notifications_data = [notification.to_dict() for notification in notifications]
db_session.close()
return jsonify({
"success": True,
"notifications": notifications_data
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der Benachrichtigungen: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Laden der Benachrichtigungen: {str(e)}"
}), 500
@app.route('/api/notifications/<int:notification_id>/read', methods=['POST'])
@login_required
def mark_notification_read(notification_id):
"""Markiert eine Benachrichtigung als gelesen"""
try:
db_session = get_db_session()
notification = db_session.query(Notification).filter(
Notification.id == notification_id,
Notification.user_id == current_user.id
).first()
if not notification:
db_session.close()
return jsonify({
"success": False,
"message": "Benachrichtigung nicht gefunden"
}), 404
notification.read = True
db_session.commit()
db_session.close()
return jsonify({
"success": True,
"message": "Benachrichtigung als gelesen markiert"
})
except Exception as e:
app_logger.error(f"Fehler beim Markieren der Benachrichtigung: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Markieren: {str(e)}"
}), 500
@app.route('/api/notifications/mark-all-read', methods=['POST'])
@login_required
def mark_all_notifications_read():
"""Markiert alle Benachrichtigungen als gelesen"""
try:
db_session = get_db_session()
# Alle ungelesenen Benachrichtigungen des Benutzers finden und als gelesen markieren
updated_count = db_session.query(Notification).filter(
Notification.user_id == current_user.id,
Notification.read == False
).update({"read": True})
db_session.commit()
db_session.close()
return jsonify({
"success": True,
"message": f"{updated_count} Benachrichtigungen als gelesen markiert"
})
except Exception as e:
app_logger.error(f"Fehler beim Markieren aller Benachrichtigungen: {str(e)}")
return jsonify({
"success": False,
"message": f"Fehler beim Markieren: {str(e)}"
}), 500
# ===== ENDE BENACHRICHTIGUNGS-API-ENDPUNKTE =====
# ===== STARTUP UND MAIN =====
if __name__ == "__main__":

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,287 @@
/**
* Benachrichtigungssystem für die MYP 3D-Druck Platform
* Verwaltet die Anzeige und Interaktion mit Benachrichtigungen
*/
class NotificationManager {
constructor() {
this.notificationToggle = document.getElementById('notificationToggle');
this.notificationDropdown = document.getElementById('notificationDropdown');
this.notificationBadge = document.getElementById('notificationBadge');
this.notificationList = document.getElementById('notificationList');
this.markAllReadBtn = document.getElementById('markAllRead');
this.isOpen = false;
this.notifications = [];
this.init();
}
init() {
if (!this.notificationToggle) return;
// Event Listeners
this.notificationToggle.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleDropdown();
});
if (this.markAllReadBtn) {
this.markAllReadBtn.addEventListener('click', () => {
this.markAllAsRead();
});
}
// Dropdown schließen bei Klick außerhalb
document.addEventListener('click', (e) => {
if (!this.notificationDropdown.contains(e.target) && !this.notificationToggle.contains(e.target)) {
this.closeDropdown();
}
});
// Benachrichtigungen laden
this.loadNotifications();
// Regelmäßige Updates
setInterval(() => {
this.loadNotifications();
}, 30000); // Alle 30 Sekunden
}
toggleDropdown() {
if (this.isOpen) {
this.closeDropdown();
} else {
this.openDropdown();
}
}
openDropdown() {
this.notificationDropdown.classList.remove('hidden');
this.notificationToggle.setAttribute('aria-expanded', 'true');
this.isOpen = true;
// Animation
this.notificationDropdown.style.opacity = '0';
this.notificationDropdown.style.transform = 'translateY(-10px)';
requestAnimationFrame(() => {
this.notificationDropdown.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
this.notificationDropdown.style.opacity = '1';
this.notificationDropdown.style.transform = 'translateY(0)';
});
}
closeDropdown() {
this.notificationDropdown.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
this.notificationDropdown.style.opacity = '0';
this.notificationDropdown.style.transform = 'translateY(-10px)';
setTimeout(() => {
this.notificationDropdown.classList.add('hidden');
this.notificationToggle.setAttribute('aria-expanded', 'false');
this.isOpen = false;
}, 200);
}
async loadNotifications() {
try {
const response = await fetch('/api/notifications');
if (response.ok) {
const data = await response.json();
this.notifications = data.notifications || [];
this.updateUI();
}
} catch (error) {
console.error('Fehler beim Laden der Benachrichtigungen:', error);
}
}
updateUI() {
this.updateBadge();
this.updateNotificationList();
}
updateBadge() {
const unreadCount = this.notifications.filter(n => !n.read).length;
if (unreadCount > 0) {
this.notificationBadge.textContent = unreadCount > 99 ? '99+' : unreadCount.toString();
this.notificationBadge.classList.remove('hidden');
} else {
this.notificationBadge.classList.add('hidden');
}
}
updateNotificationList() {
if (this.notifications.length === 0) {
this.notificationList.innerHTML = `
<div class="p-4 text-center text-slate-500 dark:text-slate-400">
Keine neuen Benachrichtigungen
</div>
`;
return;
}
const notificationHTML = this.notifications.map(notification => {
const isUnread = !notification.read;
const timeAgo = this.formatTimeAgo(new Date(notification.created_at));
return `
<div class="notification-item p-4 border-b border-slate-200 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors ${isUnread ? 'bg-blue-50 dark:bg-blue-900/20' : ''}"
data-notification-id="${notification.id}">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
${this.getNotificationIcon(notification.type)}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-slate-900 dark:text-white">
${this.getNotificationTitle(notification.type)}
</p>
${isUnread ? '<div class="w-2 h-2 bg-blue-500 rounded-full"></div>' : ''}
</div>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
${this.getNotificationMessage(notification)}
</p>
<p class="text-xs text-slate-500 dark:text-slate-500 mt-2">
${timeAgo}
</p>
</div>
</div>
</div>
`;
}).join('');
this.notificationList.innerHTML = notificationHTML;
// Event Listeners für Benachrichtigungen
this.notificationList.querySelectorAll('.notification-item').forEach(item => {
item.addEventListener('click', (e) => {
const notificationId = item.dataset.notificationId;
this.markAsRead(notificationId);
});
});
}
getNotificationIcon(type) {
const icons = {
'guest_request': `
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
`,
'job_completed': `
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
`,
'system': `
<div class="w-8 h-8 bg-gray-100 dark:bg-gray-900 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
`
};
return icons[type] || icons['system'];
}
getNotificationTitle(type) {
const titles = {
'guest_request': 'Neue Gastanfrage',
'job_completed': 'Druckauftrag abgeschlossen',
'system': 'System-Benachrichtigung'
};
return titles[type] || 'Benachrichtigung';
}
getNotificationMessage(notification) {
try {
const payload = JSON.parse(notification.payload || '{}');
switch (notification.type) {
case 'guest_request':
return `${payload.guest_name || 'Ein Gast'} hat eine neue Druckanfrage gestellt.`;
case 'job_completed':
return `Der Druckauftrag "${payload.job_name || 'Unbekannt'}" wurde erfolgreich abgeschlossen.`;
default:
return payload.message || 'Neue Benachrichtigung erhalten.';
}
} catch (error) {
return 'Neue Benachrichtigung erhalten.';
}
}
formatTimeAgo(date) {
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) {
return 'Gerade eben';
} else if (diffInSeconds < 3600) {
const minutes = Math.floor(diffInSeconds / 60);
return `vor ${minutes} Minute${minutes !== 1 ? 'n' : ''}`;
} else if (diffInSeconds < 86400) {
const hours = Math.floor(diffInSeconds / 3600);
return `vor ${hours} Stunde${hours !== 1 ? 'n' : ''}`;
} else {
const days = Math.floor(diffInSeconds / 86400);
return `vor ${days} Tag${days !== 1 ? 'en' : ''}`;
}
}
async markAsRead(notificationId) {
try {
const response = await fetch(`/api/notifications/${notificationId}/read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
// Benachrichtigung als gelesen markieren
const notification = this.notifications.find(n => n.id == notificationId);
if (notification) {
notification.read = true;
this.updateUI();
}
}
} catch (error) {
console.error('Fehler beim Markieren als gelesen:', error);
}
}
async markAllAsRead() {
try {
const response = await fetch('/api/notifications/mark-all-read', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
// Alle Benachrichtigungen als gelesen markieren
this.notifications.forEach(notification => {
notification.read = true;
});
this.updateUI();
}
} catch (error) {
console.error('Fehler beim Markieren aller als gelesen:', error);
}
}
}
// Initialisierung nach DOM-Load
document.addEventListener('DOMContentLoaded', () => {
new NotificationManager();
});

View File

@@ -252,6 +252,41 @@
</div>
{% if current_user.is_authenticated %}
<!-- Benachrichtigungen -->
<div class="relative">
<button
id="notificationToggle"
class="relative p-2 rounded-full text-slate-700 dark:text-slate-300 hover:bg-slate-100/80 dark:hover:bg-slate-800/50 transition-all duration-200"
aria-label="Benachrichtigungen anzeigen"
title="Benachrichtigungen"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
<!-- Badge für ungelesene Benachrichtigungen -->
<span id="notificationBadge" class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium hidden">
0
</span>
</button>
<!-- Benachrichtigungs-Dropdown -->
<div id="notificationDropdown" class="absolute right-0 mt-2 w-80 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-slate-200 dark:border-slate-600 z-50 hidden">
<div class="p-4 border-b border-slate-200 dark:border-slate-600">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Benachrichtigungen</h3>
</div>
<div id="notificationList" class="max-h-96 overflow-y-auto">
<div class="p-4 text-center text-slate-500 dark:text-slate-400">
Keine neuen Benachrichtigungen
</div>
</div>
<div class="p-3 border-t border-slate-200 dark:border-slate-600">
<button id="markAllRead" class="w-full text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 transition-colors">
Alle als gelesen markieren
</button>
</div>
</div>
</div>
<!-- User Profile Dropdown - neues Design -->
<div class="relative" id="user-menu-container">
<button
@@ -436,6 +471,9 @@
<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/ui-components.js') }}"></script>
<script src="{{ url_for('static', filename='js/dark-mode-fix.js') }}"></script>
{% if current_user.is_authenticated %}
<script src="{{ url_for('static', filename='js/notifications.js') }}"></script>
{% endif %}
{% block scripts %}{% endblock %}
</body>

View File

@@ -64,7 +64,19 @@ EMOJI_FALLBACK = {
'🔧': '[PRINT]',
'💥': '[ERR]',
'👤': '[USER]',
'📺': '[KIOSK]'
'📺': '[KIOSK]',
'🐞': '[BUG]',
'🚀': '[START]',
'📂': '[FOLDER]',
'📊': '[CHART]',
'💻': '[PC]',
'🌐': '[WEB]',
'📅': '[TIME]',
'📡': '[SIGNAL]',
'🧩': '[CONTENT]',
'📋': '[HEADER]',
'': '[OK]',
'📦': '[SIZE]'
}
def safe_emoji(emoji: str) -> str:
@@ -86,12 +98,22 @@ def supports_color() -> bool:
# Aktiviere VT100-Unterstützung unter Windows
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
# Setze Console-Output auf UTF-8 für bessere Emoji-Unterstützung
try:
kernel32.SetConsoleOutputCP(65001) # UTF-8
except:
pass
# Versuche UTF-8-Encoding für Emojis zu setzen
try:
import locale
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
except:
pass
try:
# Fallback für deutsche Lokalisierung
locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8')
except:
pass
return True
except:
@@ -219,7 +241,8 @@ def setup_logging(debug_mode: bool = False):
# Wenn Debug-Modus aktiv, Konfiguration loggen
if debug_mode:
root_logger.debug(f"🐞 Debug-Modus aktiviert - Ausführliche Logs werden generiert")
bug_emoji = safe_emoji("🐞")
root_logger.debug(f"{bug_emoji} Debug-Modus aktiviert - Ausführliche Logs werden generiert")
def get_logger(category: str) -> logging.Logger:
"""
@@ -280,13 +303,20 @@ def get_logger(category: str) -> logging.Logger:
def log_startup_info():
"""Loggt Startup-Informationen."""
app_logger = get_logger("app")
rocket_emoji = safe_emoji("🚀")
folder_emoji = safe_emoji("📂")
chart_emoji = safe_emoji("📊")
computer_emoji = safe_emoji("💻")
globe_emoji = safe_emoji("🌐")
calendar_emoji = safe_emoji("📅")
app_logger.info("=" * 50)
app_logger.info("🚀 MYP (Manage Your Printers) wird gestartet...")
app_logger.info(f"📂 Log-Verzeichnis: {LOG_DIR}")
app_logger.info(f"📊 Log-Level: {LOG_LEVEL}")
app_logger.info(f"💻 Betriebssystem: {platform.system()} {platform.release()}")
app_logger.info(f"🌐 Hostname: {socket.gethostname()}")
app_logger.info(f"📅 Startzeit: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
app_logger.info(f"{rocket_emoji} MYP (Manage Your Printers) wird gestartet...")
app_logger.info(f"{folder_emoji} Log-Verzeichnis: {LOG_DIR}")
app_logger.info(f"{chart_emoji} Log-Level: {LOG_LEVEL}")
app_logger.info(f"{computer_emoji} Betriebssystem: {platform.system()} {platform.release()}")
app_logger.info(f"{globe_emoji} Hostname: {socket.gethostname()}")
app_logger.info(f"{calendar_emoji} Startzeit: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}")
app_logger.info("=" * 50)
# Hilfsfunktionen für das Debugging
@@ -302,22 +332,28 @@ def debug_request(logger: logging.Logger, request):
if logger.level > logging.DEBUG:
return
logger.debug(f"🌐 HTTP-Anfrage: {request.method} {request.path}")
logger.debug(f"📡 Remote-Adresse: {request.remote_addr}")
logger.debug(f"🧩 Inhaltstyp: {request.content_type}")
web_emoji = safe_emoji("🌐")
signal_emoji = safe_emoji("📡")
puzzle_emoji = safe_emoji("🧩")
clipboard_emoji = safe_emoji("📋")
search_emoji = safe_emoji("🔍")
logger.debug(f"{web_emoji} HTTP-Anfrage: {request.method} {request.path}")
logger.debug(f"{signal_emoji} Remote-Adresse: {request.remote_addr}")
logger.debug(f"{puzzle_emoji} Inhaltstyp: {request.content_type}")
# Nur relevante Headers ausgeben
important_headers = ['User-Agent', 'Referer', 'X-Forwarded-For', 'Authorization']
headers = {k: v for k, v in request.headers.items() if k in important_headers}
if headers:
logger.debug(f"📋 Wichtige Headers: {headers}")
logger.debug(f"{clipboard_emoji} Wichtige Headers: {headers}")
# Request-Parameter (max. 1000 Zeichen)
if request.args:
args_str = str(request.args)
if len(args_str) > 1000:
args_str = args_str[:997] + "..."
logger.debug(f"🔍 URL-Parameter: {args_str}")
logger.debug(f"{search_emoji} URL-Parameter: {args_str}")
def debug_response(logger: logging.Logger, response, duration_ms: float = None):
"""
@@ -331,16 +367,18 @@ def debug_response(logger: logging.Logger, response, duration_ms: float = None):
if logger.level > logging.DEBUG:
return
status_emoji = "" if response.status_code < 400 else ""
status_emoji = safe_emoji("") if response.status_code < 400 else safe_emoji("")
logger.debug(f"{status_emoji} HTTP-Antwort: {response.status_code}")
if duration_ms is not None:
logger.debug(f"⏱️ Verarbeitungsdauer: {duration_ms:.2f} ms")
timer_emoji = safe_emoji("⏱️")
logger.debug(f"{timer_emoji} Verarbeitungsdauer: {duration_ms:.2f} ms")
content_length = response.content_length or 0
if content_length > 0:
size_str = f"{content_length / 1024:.1f} KB" if content_length > 1024 else f"{content_length} Bytes"
logger.debug(f"📦 Antwortgröße: {size_str}")
package_emoji = safe_emoji("📦")
logger.debug(f"{package_emoji} Antwortgröße: {size_str}")
def measure_execution_time(func=None, logger=None, task_name=None):
"""
@@ -367,10 +405,11 @@ def measure_execution_time(func=None, logger=None, task_name=None):
name = task_name or f.__name__
if logger:
timer_emoji = safe_emoji('⏱️')
if duration_ms > 1000: # Länger als 1 Sekunde
logger.warning(f"⏱️ Langsame Ausführung: {name} - {duration_ms:.2f} ms")
logger.warning(f"{timer_emoji} Langsame Ausführung: {name} - {duration_ms:.2f} ms")
else:
logger.debug(f"⏱️ Ausführungszeit: {name} - {duration_ms:.2f} ms")
logger.debug(f"{timer_emoji} Ausführungszeit: {name} - {duration_ms:.2f} ms")
return result
return wrapper