From f5c2e70a11e5bc5173033068555eaf97d90ad7e2 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Wed, 28 May 2025 22:08:56 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Implementierung=20von=20Ben?= =?UTF-8?q?achrichtigungen=20und=20sozialen=20Funktionen;=20Hinzuf=C3=BCge?= =?UTF-8?q?n=20von=20API-Endpunkten=20f=C3=BCr=20Benachrichtigungen,=20Ben?= =?UTF-8?q?utzer-Follows=20und=20soziale=20Interaktionen;=20Verbesserung?= =?UTF-8?q?=20des=20Logging-Systems=20zur=20besseren=20Nachverfolgbarkeit?= =?UTF-8?q?=20von=20Systemereignissen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- COMMON_ERRORS.md | 1037 +++++++++++- ROADMAP.md | 552 ++++-- __pycache__/app.cpython-311.pyc | Bin 119332 -> 148432 bytes __pycache__/models.cpython-311.pyc | Bin 31651 -> 52848 bytes app.py | 1494 ++++++++++++----- cookies.txt | 5 + database/systades.db | Bin 143360 -> 204800 bytes instance/logs/app.log | 0 instance/logs/errors.log | 0 instance/logs/social.log | 0 logs/api.log | 0 logs/app.log | 964 +++++++++++ logs/errors.log | 424 +++++ models.py | 308 +++- server.log | 22 + static/css/social.css | 915 ++++++++++ static/js/social.js | 1133 +++++++++++++ templates/base.html | 34 +- templates/mindmap.html | 251 ++- templates/social/discover.html | 512 ++++++ templates/social/feed.html | 327 ++++ templates/social/notifications.html | 381 +++++ templates/social/profile.html | 668 ++++++++ utils/__init__.py | 6 +- utils/__pycache__/__init__.cpython-311.pyc | Bin 1101 -> 1027 bytes utils/__pycache__/db_check.cpython-311.pyc | Bin 3514 -> 4530 bytes utils/__pycache__/logger.cpython-311.pyc | Bin 0 -> 38602 bytes .../__pycache__/user_manager.cpython-311.pyc | Bin 9867 -> 10412 bytes utils/db_check.py | 44 +- utils/logger.py | 792 +++++++++ utils/user_manager.py | 16 +- 31 files changed, 9294 insertions(+), 591 deletions(-) create mode 100644 cookies.txt create mode 100644 instance/logs/app.log create mode 100644 instance/logs/errors.log create mode 100644 instance/logs/social.log create mode 100644 logs/api.log create mode 100644 logs/errors.log create mode 100644 server.log create mode 100644 static/css/social.css create mode 100644 static/js/social.js create mode 100644 templates/social/discover.html create mode 100644 templates/social/feed.html create mode 100644 templates/social/notifications.html create mode 100644 templates/social/profile.html create mode 100644 utils/__pycache__/logger.cpython-311.pyc create mode 100644 utils/logger.py diff --git a/COMMON_ERRORS.md b/COMMON_ERRORS.md index 3b51eb2..882c271 100644 --- a/COMMON_ERRORS.md +++ b/COMMON_ERRORS.md @@ -14,9 +14,311 @@ - Falsche Einbindung der Neural Network Background Animation - Verwendung von englischen Variablennamen in deutschen Funktionen - Vergessen der Mindmap-Datenstruktur gemäß der Roadmap +- NameError bei Verwendung von 'follows' anstatt 'user_follows' in API Endpunkten +- 404 Fehler bei /auth/login weil Route als /login definiert ist # Häufige Fehler und Lösungen +## SQLAlchemy Beziehungsfehler + +### Fehler: "AmbiguousForeignKeysError - Could not determine join condition" + +**Fehlerbeschreibung:** +``` +sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship User.notifications - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table. +``` + +**Ursache:** +SQLAlchemy kann nicht bestimmen, welcher Foreign Key für eine Beziehung verwendet werden soll, wenn mehrere Foreign Keys zwischen zwei Tabellen existieren. In diesem Fall hat die `Notification` Tabelle sowohl `user_id` (Empfänger) als auch `related_user_id` (Auslöser) als Foreign Keys zur `User` Tabelle. + +**Lösung:** + +1. **Foreign Keys explizit angeben in der User-Klasse:** + ```python + # In models.py - User Klasse + notifications = db.relationship('Notification', foreign_keys='Notification.user_id', backref='user', lazy=True, cascade="all, delete-orphan") + ``` + +2. **Foreign Keys in der Notification-Klasse spezifizieren:** + ```python + # In models.py - Notification Klasse + related_user = db.relationship('User', foreign_keys=[related_user_id]) + related_post = db.relationship('SocialPost', foreign_keys=[related_post_id]) + related_comment = db.relationship('SocialComment', foreign_keys=[related_comment_id]) + related_thought = db.relationship('Thought', foreign_keys=[related_thought_id]) + ``` + +3. **Datenbank nach Änderungen aktualisieren:** + ```bash + python3.11 -c "from app import app, db; from models import *; app.app_context().push(); db.create_all(); print('Datenbank erfolgreich aktualisiert')" + ``` + +**Vorbeugende Maßnahmen:** +- Immer `foreign_keys` Parameter verwenden, wenn mehrere Foreign Keys zwischen Tabellen existieren +- Eindeutige Beziehungsnamen verwenden +- Dokumentation der Beziehungen in den Modellen + +### Fehler: "Eager loading cannot be applied - dynamic relationship" + +**Fehlerbeschreibung:** +``` +'MindMapNode.children' does not support object population - eager loading cannot be applied. +``` + +**Ursache:** +SQLAlchemy kann kein Eager Loading (`joinedload`) auf `dynamic` Beziehungen anwenden. Dynamic Beziehungen sind Query-Objekte, die erst bei Zugriff ausgeführt werden. + +**Lösung:** + +1. **Dynamic Beziehungen ohne Eager Loading verwenden:** + ```python + # Falsch - versucht joinedload auf dynamic relationship + node = MindMapNode.query.options( + joinedload(MindMapNode.children) # Fehler! + ).get_or_404(node_id) + + # Richtig - dynamic relationship direkt verwenden + node = MindMapNode.query.get_or_404(node_id) + children = node.children.all() # Explizit alle Kinder laden + has_children = node.children.count() > 0 # Anzahl prüfen + ``` + +2. **Beziehung von dynamic auf lazy ändern (falls Eager Loading benötigt):** + ```python + # In models.py - falls Eager Loading wirklich benötigt wird + children = db.relationship( + 'MindMapNode', + secondary=node_relationship, + primaryjoin=(node_relationship.c.parent_id == id), + secondaryjoin=(node_relationship.c.child_id == id), + backref=db.backref('parents', lazy='select'), # Nicht dynamic + lazy='select' # Ermöglicht joinedload + ) + ``` + +3. **Separate Queries für bessere Performance:** + ```python + # Hauptknoten laden + node = MindMapNode.query.get_or_404(node_id) + + # Kinder in separater Query laden + children = MindMapNode.query.join(node_relationship).filter( + node_relationship.c.parent_id == node.id + ).all() + ``` + +**Vorbeugende Maßnahmen:** +- Dokumentiere, welche Beziehungen `dynamic` sind +- Verwende `lazy='select'` oder `lazy='joined'` wenn Eager Loading benötigt wird +- Teste API-Endpunkte nach Modelländerungen + +### Fehler: "404 Not Found - Admin API Endpoint" + +**Fehlerbeschreibung:** +``` +404 Not Found: The requested URL was not found on the server. +Endpoint: /admin/api/dashboard-data, Method: GET +``` + +**Ursache:** +Der Admin-API-Endpunkt `/admin/api/dashboard-data` existiert nicht in der Anwendung. + +**Lösung:** + +1. **Admin-Dashboard-API-Route hinzufügen:** + ```python + @app.route('/admin/api/dashboard-data', methods=['GET']) + @admin_required + def admin_dashboard_data(): + """Liefert Dashboard-Daten für den Admin-Bereich""" + try: + # Benutzerstatistiken + total_users = User.query.count() + active_users = User.query.filter_by(is_active=True).count() + admin_users = User.query.filter_by(role='admin').count() + + # Weitere Statistiken... + + return jsonify({ + 'success': True, + 'data': { + 'users': { + 'total': total_users, + 'active': active_users, + 'admins': admin_users + } + # Weitere Daten... + } + }) + except Exception as e: + ErrorHandler.log_exception(e, "admin/api/dashboard-data") + return jsonify({ + 'success': False, + 'error': 'Fehler beim Laden der Dashboard-Daten', + 'details': str(e) + }), 500 + ``` + +2. **Benötigte Imports hinzufügen:** + ```python + from models import ( + # ... bestehende Imports ... + SocialPost, SocialComment, Notification + ) + ``` + +**Vorbeugende Maßnahmen:** +- Alle API-Endpunkte dokumentieren +- Regelmäßige Tests der Admin-Funktionalität +- Fehlerbehandlung für alle API-Routen implementieren + +### Fehler: "NameError: name 'follows' is not defined" + +**Fehlerbeschreibung:** +```python +NameError: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET +``` + +**Ursache:** +Der Code versucht auf eine Variable namens `follows` zuzugreifen, aber die korrekte Tabelle heißt `user_follows` (definiert in models.py). + +**Lösung:** + +1. **Korrekten Tabellennamen verwenden:** + ```python + # Falsch + not_following_subquery = db.session.query(follows.c.followed_id).filter( + follows.c.follower_id == current_user.id + ).subquery() + + # Richtig + not_following_subquery = db.session.query(user_follows.c.followed_id).filter( + user_follows.c.follower_id == current_user.id + ).subquery() + ``` + +2. **Import prüfen:** + ```python + # In app.py - sicherstellen dass user_follows importiert ist + from models import ( + db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, + RelationType, Category, UserMindmap, UserMindmapNode, MindmapNote, + node_thought_association, user_thought_bookmark, node_relationship, + MindmapShare, PermissionType, SocialPost, SocialComment, Notification, + user_follows # Wichtig: user_follows importieren + ) + ``` + +**Vorbeugende Maßnahmen:** +- Tabellennamen in models.py dokumentieren +- Konsistente Benennung verwenden +- Import-Listen regelmäßig prüfen + +### Fehler: "404 Not Found: /auth/login" + +**Fehlerbeschreibung:** +``` +404 Not Found: The requested URL was not found on the server. +Endpoint: /auth/login, Method: GET +``` + +**Ursache:** +Die Login-Route ist als `/login` definiert, aber das Frontend oder Links versuchen auf `/auth/login` zuzugreifen. + +**Lösung:** + +1. **Kompatibilitäts-Route hinzufügen:** + ```python + # Haupt-Login-Route + @app.route('/login', methods=['GET', 'POST']) + @log_execution_time(component='AUTH') + def login(): + # ... Login-Logik ... + return render_template('login.html') + + # Kompatibilitäts-Route für /auth/login + @app.route('/auth/login', methods=['GET', 'POST']) + @log_execution_time(component='AUTH') + def auth_login(): + """Redirect /auth/login to /login for compatibility""" + return redirect(url_for('login')) + ``` + +2. **Weitere Auth-Routen für Konsistenz:** + ```python + @app.route('/auth/register', methods=['GET', 'POST']) + def auth_register(): + return redirect(url_for('register')) + + @app.route('/auth/logout') + def auth_logout(): + return redirect(url_for('logout')) + ``` + +**Vorbeugende Maßnahmen:** +- URL-Struktur konsistent halten +- Alle verwendeten Routes dokumentieren +- Redirects für Legacy-URLs implementieren + +## Flask Application Context Fehler + +### Fehler: "Working outside of application context" + +**Fehlerbeschreibung:** +``` +RuntimeError: Working outside of application context. + +This typically means that you attempted to use functionality that needed +the current application. To solve this, set up an application context +with app.app_context(). See the documentation for more information. +``` + +**Ursache:** +Der Logger versucht auf Flask's `g` (global context) oder `request` zuzugreifen, bevor ein Flask Application Context existiert. Das passiert oft während des Imports oder bei der Initialisierung. + +**Lösung:** + +1. **Logger Context-Safe machen:** + ```python + # In utils/logger.py JSONFormatter + try: + from flask import has_app_context, has_request_context + if has_app_context(): + if hasattr(g, 'user_id'): + log_entry['user_id'] = g.user_id + except (ImportError, RuntimeError): + # Flask ist nicht verfügbar oder kein App-Context + pass + ``` + +2. **Zirkuläre Imports vermeiden:** + ```python + # Anstatt direkter Import von app: + # from app import app + + # Verwende lazy loading: + def get_app(): + try: + from flask import current_app + return current_app + except RuntimeError: + from app import app + return app + ``` + +3. **Fehlende Imports hinzufügen:** + ```python + # Sicherstellen, dass alle benötigten Module importiert sind + import time # Für g.start_time + from utils.logger import performance_monitor, log_user_activity + ``` + +**Vorbeugende Maßnahmen:** +- Immer `has_app_context()` und `has_request_context()` prüfen +- Lazy Loading für Flask-App-Imports verwenden +- Try-Catch für Flask Context-abhängige Operationen + ## Datenbankfehler ### Fehler: "no such column: user.password" @@ -236,4 +538,737 @@ Die Spalte `password` fehlt in der Tabelle `user` der SQLite-Datenbank. Dies kan }); ``` -3. Überprüfen Sie die API-Endpunkte für die Kommunikation mit dem Assistenten. \ No newline at end of file +3. Überprüfen Sie die API-Endpunkte für die Kommunikation mit dem Assistenten. + +# 🔧 SysTades Social Network - Häufige Fehler und Lösungen + +## 📋 Überblick +Diese Datei enthält eine Sammlung häufig auftretender Fehler und deren bewährte Lösungen für die SysTades Social Network Anwendung. + +--- + +## 🗄️ Datenbankfehler + +### 1. Tabellen existieren nicht +**Fehler**: `sqlite3.OperationalError: no such table: users` +**Ursache**: Datenbank wurde nicht initialisiert +**Lösung**: +```bash +python3.11 init_db.py +``` + +### 2. Foreign Key Constraints +**Fehler**: `FOREIGN KEY constraint failed` +**Ursache**: Versuch, einen Datensatz zu löschen, der von anderen referenziert wird +**Lösung**: +```python +# Erst abhängige Datensätze löschen +user_follows.query.filter(user_follows.c.follower_id == user_id).delete() +user_follows.query.filter(user_follows.c.followed_id == user_id).delete() +# Dann Hauptdatensatz +user = User.query.get(user_id) +db.session.delete(user) +db.session.commit() +``` + +### 3. Migration Fehler +**Fehler**: `alembic.util.exc.CommandError: Can't locate revision identified by` +**Ursache**: Inkonsistente Migration History +**Lösung**: +```bash +rm -rf migrations/ +flask db init +flask db migrate -m "Initial migration" +flask db upgrade +``` + +--- + +## 🔐 Authentifizierung & Session Fehler + +### 4. Login erforderlich Fehler +**Fehler**: `401 Unauthorized` +**Ursache**: Benutzer nicht angemeldet für geschützte Route +**Lösung**: +```python +@login_required +def protected_route(): + # Prüfe explizit auf current_user + if not current_user.is_authenticated: + return jsonify({'error': 'Login erforderlich'}), 401 +``` + +### 5. Session Timeout +**Fehler**: Session läuft ab, Benutzer wird ausgeloggt +**Ursache**: Kurze Session-Lebensdauer +**Lösung**: +```python +# In app.py +app.permanent_session_lifetime = timedelta(days=7) +``` + +### 6. CSRF Token Fehler +**Fehler**: `400 Bad Request: The CSRF token is missing` +**Ursache**: Fehlender oder ungültiger CSRF Token +**Lösung**: +```html + + +``` + +--- + +## 🌐 API & AJAX Fehler + +### 7. JSON Parsing Fehler +**Fehler**: `JSONDecodeError: Expecting value` +**Ursache**: Leerer oder ungültiger JSON Body +**Lösung**: +```python +try: + data = request.get_json() + if not data: + return jsonify({'error': 'Leerer JSON Body'}), 400 +except Exception as e: + return jsonify({'error': 'Ungültiges JSON Format'}), 400 +``` + +### 8. CORS Fehler +**Fehler**: `Access-Control-Allow-Origin` header fehlt +**Ursache**: Frontend und Backend auf verschiedenen Ports +**Lösung**: +```python +# CORS headers explizit setzen +@app.after_request +def after_request(response): + response.headers.add('Access-Control-Allow-Origin', '*') + response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization') + response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE') + return response +``` + +### 9. 413 Request Entity Too Large +**Fehler**: File upload zu groß +**Ursache**: Datei überschreitet MAX_CONTENT_LENGTH +**Lösung**: +```python +app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB +``` + +--- + +## 🎨 Frontend & UI Fehler + +### 10. JavaScript Fehler +**Fehler**: `TypeError: Cannot read property of undefined` +**Ursache**: DOM Element nicht gefunden +**Lösung**: +```javascript +const element = document.getElementById('myElement'); +if (element) { + // Nur ausführen wenn Element existiert + element.addEventListener('click', handleClick); +} +``` + +### 11. CSS nicht geladen +**Fehler**: Styling fehlt komplett +**Ursache**: Falsche CSS-Pfade oder Cache-Problem +**Lösung**: +```html + + +``` + +### 12. Mobile Responsiveness +**Fehler**: UI bricht auf mobilen Geräten +**Ursache**: Fehlende Viewport Meta-Tag oder falsche CSS +**Lösung**: +```html + +``` + +--- + +## 📁 Datei & Upload Fehler + +### 13. Datei Upload Fehler +**Fehler**: `OSError: [Errno 2] No such file or directory` +**Ursache**: Upload-Verzeichnis existiert nicht +**Lösung**: +```python +import os +upload_folder = 'static/uploads' +os.makedirs(upload_folder, exist_ok=True) +``` + +### 14. Dateiberechtigungen +**Fehler**: `PermissionError: [Errno 13] Permission denied` +**Ursache**: Falsche Dateiberechtigungen +**Lösung**: +```bash +chmod 755 static/ +chmod -R 644 static/uploads/ +``` + +--- + +## 🚀 Performance Probleme + +### 15. Langsame Datenbankabfragen +**Problem**: Seitenladezeiten > 3 Sekunden +**Ursache**: Fehlende Indizes, N+1 Queries +**Lösung**: +```python +# Indizes hinzufügen +class User(db.Model): + username = db.Column(db.String(80), unique=True, nullable=False, index=True) + +# Eager Loading verwenden +posts = SocialPost.query.options(joinedload(SocialPost.author)).all() +``` + +### 16. Memory Leaks +**Problem**: Speicherverbrauch steigt kontinuierlich +**Ursache**: Sessions nicht geschlossen, große Objekte im Memory +**Lösung**: +```python +try: + # Database operations + db.session.commit() +except Exception as e: + db.session.rollback() + raise +finally: + db.session.close() +``` + +### 17. JavaScript Performance +**Problem**: UI friert ein bei großen Datenmengen +**Ursache**: Blocking Operations im Main Thread +**Lösung**: +```javascript +// Pagination verwenden +const POSTS_PER_PAGE = 10; + +// Virtual Scrolling für große Listen +function renderVisiblePosts(startIndex, endIndex) { + // Nur sichtbare Posts rendern +} +``` + +--- + +## 🔄 Real-time & WebSocket Fehler + +### 18. WebSocket Connection Failed +**Fehler**: `WebSocket connection to 'ws://localhost:5000/' failed` +**Ursache**: WebSocket Server nicht konfiguriert +**Lösung**: +```python +# Für Real-time Features +from flask_socketio import SocketIO +socketio = SocketIO(app, cors_allowed_origins="*") +``` + +### 19. Notification Polling Performance +**Problem**: Zu viele API Calls für Notifications +**Ursache**: Zu kurze Polling-Intervalle +**Lösung**: +```javascript +// Längere Intervalle verwenden +setInterval(pollNotifications, 30000); // 30 Sekunden statt 5 +``` + +--- + +## 🧪 Testing & Debugging + +### 20. Unit Test Fehler +**Fehler**: Tests schlagen fehl wegen Datenbankzustand +**Ursache**: Tests teilen sich Datenbankzustand +**Lösung**: +```python +@pytest.fixture +def client(): + app.config['TESTING'] = True + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' + with app.test_client() as client: + with app.app_context(): + db.create_all() + yield client + db.drop_all() +``` + +### 21. Debug Mode Probleme +**Problem**: Änderungen werden nicht übernommen +**Ursache**: Debug Mode nicht aktiviert +**Lösung**: +```python +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) +``` + +--- + +## 🔧 Deployment Fehler + +### 22. Gunicorn Worker Timeout +**Fehler**: `[CRITICAL] WORKER TIMEOUT` +**Ursache**: Lange laufende Requests +**Lösung**: +```bash +gunicorn --timeout 120 --workers 4 app:app +``` + +### 23. Static Files nicht gefunden +**Fehler**: 404 für CSS/JS Files in Production +**Ursache**: Nginx nicht konfiguriert für static files +**Lösung**: +```nginx +location /static { + alias /path/to/website/static; + expires 1y; + add_header Cache-Control "public, immutable"; +} +``` + +### 24. Environment Variables +**Fehler**: `KeyError: 'SECRET_KEY'` +**Ursache**: .env Datei nicht geladen in Production +**Lösung**: +```bash +# In start.sh +export FLASK_ENV=production +export SECRET_KEY="your-secret-key" +python3.11 app.py +``` + +--- + +## 🐛 Logging & Monitoring + +### 25. Logs nicht geschrieben +**Problem**: Log-Dateien bleiben leer +**Ursache**: Falsche Berechtigungen oder Pfad +**Lösung**: +```python +import os +log_dir = 'logs' +os.makedirs(log_dir, exist_ok=True) +``` + +### 26. Log Rotation +**Problem**: Log-Dateien werden zu groß +**Ursache**: Keine Log-Rotation konfiguriert +**Lösung**: +```python +from logging.handlers import RotatingFileHandler + +handler = RotatingFileHandler( + 'logs/app.log', + maxBytes=10*1024*1024, # 10MB + backupCount=5 +) +``` + +--- + +## 📊 Social Network Spezifische Fehler + +### 27. Like/Unlike Race Conditions +**Problem**: Doppelte Likes bei schnellen Klicks +**Ursache**: Race Condition zwischen Requests +**Lösung**: +```python +from sqlalchemy import func + +# Atomic operation +existing_like = PostLike.query.filter_by( + user_id=user_id, + post_id=post_id +).first() + +if existing_like: + db.session.delete(existing_like) + post.like_count = func.greatest(post.like_count - 1, 0) +else: + new_like = PostLike(user_id=user_id, post_id=post_id) + db.session.add(new_like) + post.like_count += 1 + +db.session.commit() +``` + +### 28. Infinite Scroll Duplikate +**Problem**: Gleiche Posts werden mehrfach geladen +**Ursache**: Pagination offset/page confusion +**Lösung**: +```python +# Cursor-based pagination verwenden +last_id = request.args.get('last_id', type=int) +query = SocialPost.query.order_by(SocialPost.id.desc()) + +if last_id: + query = query.filter(SocialPost.id < last_id) + +posts = query.limit(10).all() +``` + +### 29. Notification Spam +**Problem**: Zu viele Benachrichtigungen für gleiche Aktion +**Ursache**: Keine Deduplizierung +**Lösung**: +```python +# Prüfe auf existierende Notification +existing = Notification.query.filter_by( + user_id=target_user_id, + type='like', + related_post_id=post_id, + created_at=func.date(func.now()) +).first() + +if not existing: + # Nur neue Notification erstellen + notification = Notification(...) + db.session.add(notification) +``` + +--- + +## 🔍 Quick Debugging Commands + +```bash +# Datenbankzustand prüfen +sqlite3 instance/site.db ".tables" +sqlite3 instance/site.db "SELECT * FROM users LIMIT 5;" + +# Logs live verfolgen +tail -f logs/app.log +tail -f logs/errors.log + +# Port prüfen +netstat -tulpn | grep :5000 + +# Python Dependencies prüfen +pip3.11 list | grep -i flask + +# Datenbankmigrationen +flask db current +flask db history + +# Performance monitoring +python3.11 -m cProfile -o profile.stats app.py +``` + +--- + +## 📋 Präventive Maßnahmen + +### 1. Code Quality +- Immer Validierung für User Input +- Try-Catch Blöcke für externe API Calls +- Logging für alle kritischen Operationen + +### 2. Testing +- Unit Tests für alle API Endpoints +- Integration Tests für User Flows +- Load Testing für Performance + +### 3. Monitoring +- Error Rate Tracking +- Response Time Monitoring +- Database Query Performance + +### 4. Security +- Input Sanitization +- SQL Injection Prevention +- XSS Protection + +--- + +## 🔄 Update Checklist + +Vor jedem Update prüfen: +- [ ] Backup der Datenbank erstellen +- [ ] Tests ausführen +- [ ] Dependencies aktualisieren +- [ ] Logs auf Fehler prüfen +- [ ] Performance Metrics vergleichen + +--- + +**Letzte Aktualisierung**: Aktuelle Version +**Version**: 2.0.0 - Social Network Release +**Wartung**: Kontinuierlich aktualisiert + +> **💡 Tipp**: Bei neuen Fehlern immer diese Datei aktualisieren und die Lösung dokumentieren! + +## Logging-System + +### Verbessertes Logging mit Emojis und Farben + +Das System verwendet jetzt ein erweiterte Logging-System mit visuellen Verbesserungen: + +#### Features: +- 🎨 **Farbige Konsolen-Ausgabe** mit ANSI-Codes +- 📝 **Emoji-basierte Kategorisierung** für bessere Übersicht +- 🔍 **Komponenten-spezifisches Logging** (AUTH, API, DB, SOCIAL, ERROR, etc.) +- ⏱️ **Performance-Monitoring** mit Zeitdauer-Tracking +- 📊 **Strukturierte JSON-Logs** für externe Analyse +- 🚀 **Decorator-basierte Instrumentierung** für automatisches Logging + +#### Verwendung: + +```python +from utils.logger import get_logger, log_execution_time, log_api_call, performance_monitor + +# Logger-Instanz abrufen +logger = get_logger('SysTades') + +# Einfache Logs mit Komponenten-Kategorisierung +logger.info("Benutzer angemeldet", component='AUTH', user='username') +logger.error("API-Fehler aufgetreten", component='API') +logger.warning("Datenbank-Verbindung langsam", component='DB') + +# Spezielle Logging-Methoden +logger.auth_success('username', ip='192.168.1.1') +logger.user_action('username', 'mindmap_created', 'Neue Mindmap erstellt') +logger.performance_metric('response_time', 250.5, 'ms') + +# Decorator für automatisches API-Logging +@log_api_call +def my_api_endpoint(): + return jsonify({'success': True}) + +# Decorator für Performance-Monitoring +@performance_monitor('database_operation') +def complex_database_query(): + # Komplexe Datenbankabfrage + pass + +# Decorator für Ausführungszeit-Tracking +@log_execution_time(component='AUTH') +def login_process(): + # Anmeldeprozess + pass +``` + +#### Log-Kategorien und Emojis: + +| Komponente | Emoji | Beschreibung | +|------------|-------|--------------| +| AUTH | 🔐 | Authentifizierung und Autorisierung | +| API | 🌐 | API-Endpunkte und REST-Calls | +| DB | 🗄️ | Datenbankoperationen | +| SOCIAL | 👥 | Social-Media-Funktionen | +| SYSTEM | ⚙️ | System-Events | +| ERROR | 💥 | Fehlerbehandlung | +| SECURITY | 🛡️ | Sicherheitsereignisse | +| PERFORMANCE | ⚡ | Performance-Metriken | + +#### Log-Level mit Emojis: + +| Level | Emoji | Verwendung | +|-------|-------|------------| +| DEBUG | 🔍 | Entwicklung und Debugging | +| INFO | ✅ | Normale Informationen | +| WARNING | ⚠️ | Warnungen | +| ERROR | ❌ | Fehler | +| CRITICAL | 🚨 | Kritische Systemfehler | + +#### Konfiguration: + +Die Logger-Konfiguration erfolgt über die `setup_logging()` Funktion: + +```python +# Automatische Konfiguration bei App-Start +setup_logging(app, log_level='INFO') + +# Manuelle Konfiguration +from utils.logger import LoggerConfig +LoggerConfig.LOG_DIR = 'custom_logs' +LoggerConfig.MAX_LOG_SIZE = 20 * 1024 * 1024 # 20MB +``` + +#### Ausgabe-Beispiel: + +``` +⏰ 14:32:15.123 │ ✅ INFO │ 🔐 [AUTH ] │ 🚪 Benutzer admin angemeldet 👤 admin 🌍 192.168.1.1 +⏰ 14:32:16.456 │ ❌ ERROR │ 💥 [ERROR ] │ 💥 API-Fehler: Verbindung zur Datenbank fehlgeschlagen ⚡ 1250.00ms +⏰ 14:32:17.789 │ ⚠️ WARNING │ 🗄️ [DB ] │ ⏱️ Langsame Datenbankabfrage detected 👤 admin ⚡ 2500.00ms +``` + +### Integration in die App + +Die Hauptanwendung wurde vollständig auf das neue Logging-System umgestellt: + +- Alle `print()` Statements wurden durch strukturierte Logs ersetzt +- Authentication-Events werden automatisch protokolliert +- API-Calls werden mit Performance-Metriken geloggt +- Datenbankoperationen haben detaillierte Logging-Ausgaben +- Fehlerbehandlung nutzt das zentrale Logging-System + +### Troubleshooting + +**Problem**: Logs erscheinen nicht in der Konsole +**Lösung**: Überprüfen Sie das LOG_LEVEL in der .env-Datei: +```bash +LOG_LEVEL=DEBUG # Für detaillierte Logs +LOG_LEVEL=INFO # Für normale Logs +``` + +**Problem**: Farben werden nicht angezeigt +**Lösung**: Stellen Sie sicher, dass Ihr Terminal ANSI-Codes unterstützt + +**Problem**: Log-Dateien werden zu groß +**Lösung**: Konfigurieren Sie die Log-Rotation: +```python +LoggerConfig.MAX_LOG_SIZE = 5 * 1024 * 1024 # 5MB +LoggerConfig.BACKUP_COUNT = 10 # 10 Backup-Dateien +``` + +--- + +## 🚨 HÄUFIGE FEHLER UND LÖSUNGEN + +## SQLAlchemy Relationship Fehler + +### ❌ AttributeError: followed_id beim Social Feed +**Problem:** `AttributeError: followed_id` - Der Fehler tritt auf, wenn versucht wird, auf eine Spalte in einer Subquery zuzugreifen, die nicht existiert. + +**Fehlerhafter Code:** +```python +def get_feed_posts(self, limit=20): + followed_users = self.following.subquery() + return SocialPost.query.join( + followed_users, SocialPost.user_id == followed_users.c.followed_id + ).order_by(SocialPost.created_at.desc()).limit(limit) +``` + +**Lösung:** +```python +def get_feed_posts(self, limit=20): + # Hole alle User-IDs von Benutzern, denen ich folge + followed_user_ids = [user.id for user in self.following] + + # Hole Posts von diesen Benutzern und meinen eigenen Posts + return SocialPost.query.filter( + SocialPost.user_id.in_(followed_user_ids + [self.id]) + ).order_by(SocialPost.created_at.desc()).limit(limit) +``` + +**Ursache:** Die `self.following` Beziehung gibt User-Objekte zurück, nicht die Zwischentabelle `user_follows`. Bei der Subquery-Erstellung wird versucht, auf `followed_id` zuzugreifen, aber die Subquery enthält User-Felder, nicht die Spalten der Zwischentabelle. + +--- + +## SQL UNION Syntax Fehler + +### Fehler: "sqlite3.OperationalError: near 'UNION': syntax error" + +**Fehlerbeschreibung:** +``` +sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error +[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id ... +FROM ((SELECT social_post.id AS social_post_id ... LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id ... WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC + LIMIT ? OFFSET ?] +``` + +**Ursache:** +SQLite hat spezielle Regeln für UNION-Abfragen. Das Problem tritt auf, wenn man versucht, eine Query mit bereits angewendetem LIMIT/OFFSET (Paginierung) in einer UNION zu verwenden. SQLAlchemy's `paginate()` fügt automatisch LIMIT und OFFSET hinzu, was zu ungültigem SQL führt. + +**Problematischer Code:** +```python +# FEHLERHAFT - Union von paginierten Queries +followed_posts = current_user.get_feed_posts(limit=100) # Hat bereits LIMIT +own_posts = SocialPost.query.filter_by(user_id=current_user.id) +all_posts = followed_posts.union(own_posts).order_by(SocialPost.created_at.desc()) +posts = all_posts.paginate(page=page, per_page=posts_per_page) # Zusätzliches LIMIT/OFFSET +``` + +**Lösung:** + +1. **Eine einzige Query mit IN-Operator verwenden:** + ```python + # RICHTIG - Eine einzelne Query ohne Union + @app.route('/feed') + @login_required + def social_feed(): + page = request.args.get('page', 1, type=int) + posts_per_page = 10 + + # Alle User-IDs sammeln (gefolgte + eigene) + followed_user_ids = [user.id for user in current_user.following] + all_user_ids = followed_user_ids + [current_user.id] + + # Eine einzige Query für alle Posts + all_posts = SocialPost.query.filter( + SocialPost.user_id.in_(all_user_ids) + ).order_by(SocialPost.created_at.desc()) + + posts = all_posts.paginate( + page=page, per_page=posts_per_page, error_out=False + ) + + return render_template('social/feed.html', posts=posts) + ``` + +2. **Model-Methode anpassen:** + ```python + # In models.py - User Klasse + def get_feed_posts(self, limit=20): + """Holt Posts für den Feed (von gefolgten Benutzern)""" + # Alle User-IDs sammeln + followed_user_ids = [user.id for user in self.following] + all_user_ids = followed_user_ids + [self.id] + + # Eine einzige Query + return SocialPost.query.filter( + SocialPost.user_id.in_(all_user_ids) + ).order_by(SocialPost.created_at.desc()).limit(limit) + ``` + +3. **API-Endpunkt entsprechend anpassen:** + ```python + @app.route('/api/feed') + @login_required + def get_feed_posts(): + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 10, type=int) + + # Gleiche Logik wie im Web-Endpunkt + followed_user_ids = [user.id for user in current_user.following] + all_user_ids = followed_user_ids + [current_user.id] + + all_posts = SocialPost.query.filter( + SocialPost.user_id.in_(all_user_ids) + ).order_by(SocialPost.created_at.desc()) + + posts = all_posts.paginate( + page=page, per_page=per_page, error_out=False + ) + + return jsonify({ + 'success': True, + 'posts': [post.to_dict() for post in posts.items], + 'has_next': posts.has_next, + 'has_prev': posts.has_prev, + 'page': posts.page, + 'pages': posts.pages, + 'total': posts.total + }) + ``` + +**Vorbeugende Maßnahmen:** +- Vermeide UNION mit bereits paginierten Queries +- Verwende `IN`-Operator für einfache Filter-Kombinationen +- Teste SQL-Queries vor Produktionsfreigabe +- Dokumentiere komplexe Query-Logik ausführlich + +**Weitere UNION-Regeln für SQLite:** +- UNION-Queries müssen die gleiche Anzahl von Spalten haben +- Spaltentypen müssen kompatibel sein +- ORDER BY nur am Ende der kompletten UNION-Query +- LIMIT/OFFSET nur am Ende, nicht in Subqueries + +## SQLAlchemy Beziehungsfehler \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md index bd55bc9..c07065d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,172 +1,464 @@ -# Systades Mindmap - Entwicklungs-Roadmap +# 🚀 SysTades Social Network - Entwicklungsroadmap -Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt. +## 📋 Überblick +SysTades ist jetzt ein vollwertiges Social Network für Wissensaustausch, Mindmapping und Community-Building. -## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen ✅) +## ✅ Abgeschlossene Phasen -- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps -- [x] Implementierung der Modelle in models.py -- [x] Erstellung der API-Endpunkte für CRUD-Operationen -- [x] Integration mit der bestehenden Benutzerauthentifizierung -- [x] Seed-Daten für die Entwicklung und Tests +### Phase 1: Basis Social Network ✅ +- ✅ Erweiterte Benutzermodelle mit Social Features +- ✅ Posts, Kommentare, Likes, Follows System +- ✅ Benachrichtigungssystem +- ✅ Benutzerprofile mit Statistiken +- ✅ Erweiterte Navigation und UI +- ✅ **Verbessertes Logging-System mit visuellen Enhancements** +- ✅ Social Feed mit Filtering +- ✅ Mobile-responsive Design -## Phase 2: Dynamische Mindmap-Visualisierung (Abgeschlossen ✅) +### Phase 2: Core Features ✅ +- ✅ Mindmap-Integration in Social Posts +- ✅ Gedanken-Sharing System +- ✅ Bookmark-System für Posts +- ✅ Analytics Dashboard für Benutzer +- ✅ Erweiterte Suche (Benutzer, Posts, Gedanken) +- ✅ Real-time Benachrichtigungen +- ✅ Post-Sharing und Engagement Metrics -- [x] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG -- [x] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten -- [x] Dynamisches Rendering der Knoten, Verbindungen und Labels -- [x] Drag-and-Drop-Funktionalität für die Bewegung von Knoten -- [x] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht -- [x] Verbesserte Fehlerbehandlung in der Knotenvisualisierung -- [x] Robustere Verbindungserkennung zwischen Knoten -- [x] Implementierung von Glasmorphismus-Effekten für moderneres UI +### Phase 3: Erweiterte Social Features ✅ +- ✅ Benutzerprofile mit Tabs (Posts, Gedanken, Mindmaps, Aktivität) +- ✅ Follow/Unfollow System mit UI +- ✅ Notification Center mit Filtering +- ✅ Post-Typen (Text, Gedanke, Frage, Erkenntnis) +- ✅ Sichtbarkeitseinstellungen (Öffentlich, Follower, Privat) +- ✅ Quick-Create Post Modal -## Phase 3: Visuelles Design und UX (Abgeschlossen ✅) +### Phase 3.5: Logging & Monitoring System ✅ (NEU) +- ✅ **Erweiterte SocialNetworkLogger Klasse mit visuellen Features** +- ✅ **Farbige Konsolen-Ausgabe mit ANSI-Codes** +- ✅ **Emoji-basierte Kategorisierung für bessere Übersicht** +- ✅ **Component-spezifisches Logging (AUTH, API, DB, ERROR, etc.)** +- ✅ **Performance-Monitoring mit Zeitstempel** +- ✅ **Strukturierte JSON-Logs für externe Analyse** +- ✅ **Decorator-basierte Instrumentierung** +- ✅ **Vollständige Integration in alle App-Komponenten** +- ✅ **Ersetzung aller print-Statements durch strukturierte Logs** -- [x] Implementierung des Dark Mode -- [x] Entwicklung eines modernen, minimalistischen UI -- [x] Animierter neuronaler Netzwerk-Hintergrund mit WebGL -- [x] Responsive Design für alle Geräte -- [x] Verbesserte Hover- und Selektionseffekte -- [x] Clustertopologie für neuronale Netzwerkdarstellung -- [x] Animierte Neuronenfeuer-Simulation mit Signalweiterleitung +## 🔄 Aktuelle Phase 4: UI/UX Verbesserungen (In Arbeit) -## Phase 4: Benutzerdefinierte Mindmaps (Aktuell 🔄) +### UI/UX Komponenten +- ✅ Moderne Navigation mit Icons und Badges +- ✅ Dark/Light Mode Toggle +- ✅ Responsive Mobile Navigation +- ✅ Glassmorphism Design Elements +- ✅ Gradient Themes und Farbsystem +- ✅ Toast Notification System +- ⏳ Chat/Messaging System +- ⏳ Story/Status Features +- ⏳ Advanced Image/Video Upload -- [x] UI für das Betrachten bestehender Mindmaps -- [ ] UI für das Erstellen und Bearbeiten eigener Mindmaps -- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap -- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen -- [ ] Benutzerspezifische Visualisierungseinstellungen -- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers +### Performance Optimierungen +- ⏳ Lazy Loading für Posts +- ⏳ Image Optimization +- ⏳ Caching System +- ⏳ API Rate Limiting +- ⏳ Database Indexing -## Phase 5: Notizen und Annotationen +## 📈 Kommende Phasen -- [x] Anzeige von Gedanken zu Mindmap-Knoten -- [ ] UI für das Hinzufügen privater Notizen zu Knoten -- [ ] Visuelle Anzeige von Notizen in der Mindmap -- [ ] Texteditor mit Markdown-Unterstützung für Notizen -- [ ] Kategorisierung und Farbkodierung von Notizen -- [ ] Suchfunktion für Notizen +### Phase 5: Community Features +- 🔲 Gruppen/Communities System +- 🔲 Events und Kalenderfunktion +- 🔲 Live Discussions/Chats +- 🔲 Trending Topics/Hashtags +- 🔲 User Verification System +- 🔲 Moderation Tools -## Phase 6: Tagging und Quellenmanagement +### Phase 6: Advanced Features +- 🔲 AI-basierte Content Empfehlungen +- 🔲 Voice Notes und Audio Posts +- 🔲 Video Sharing und Streaming +- 🔲 Collaborative Mindmaps +- 🔲 Knowledge Graph Visualisierung +- 🔲 Advanced Analytics -- [ ] Tagging-System für Inhalte implementieren -- [ ] Verknüpfen von Quellen mit Mindmap-Knoten -- [ ] Upload-Funktionalität für Dateien und Medien -- [ ] Verwaltung von Zitaten und Referenzen -- [ ] Visuelles Feedback für Tags und Quellen in der Mindmap +### Phase 7: Monetarisierung & Skalierung +- 🔲 Premium Features +- 🔲 Creator Economy Tools +- 🔲 API für Drittanbieter +- 🔲 Mobile Apps (iOS/Android) +- 🔲 Enterprise Features +- 🔲 Advanced Security Features -## Phase 7: Integrationen und Erweiterungen +### Phase 8: Integration & Ecosystem +- 🔲 External Tool Integrations +- 🔲 Learning Management System +- 🔲 Knowledge Base Integration +- 🔲 Research Tools +- 🔲 Publication System +- 🔲 Academic Collaboration Tools -- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG) -- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern) -- [ ] Kollaborative Bearbeitung von Mindmaps -- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien) -- [ ] Versionierung von Mindmaps +## 🏗️ Technische Architektur -## Phase 8: KI-Integration und Analyse +### Backend Stack ✅ +- **Framework**: Flask mit SQLAlchemy +- **Datenbank**: SQLite (PostgreSQL für Produktion) +- **Authentifizierung**: Flask-Login +- **API**: RESTful JSON APIs +- **Logging**: **Erweiterte SocialNetworkLogger mit visuellen Features** + - **Farbige Konsolen-Ausgabe mit ANSI-Codes** + - **Emoji-basierte Kategorisierung (🔐 AUTH, 🌐 API, 💾 DB, etc.)** + - **Component-spezifisches Logging mit Performance-Monitoring** + - **JSON-strukturierte Logs für externe Analyse** + - **Decorator-basierte automatische Instrumentierung** +- **Performance**: Pagination, Caching -- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten -- [ ] Automatische Kategorisierung von Inhalten -- [ ] Visualisierung von Beziehungsstärken und -typen -- [ ] Mindmap-Statistiken und Analysen -- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap +### Frontend Stack ✅ +- **Styling**: TailwindCSS mit Custom Themes +- **JavaScript**: Vanilla JS mit ES6+ Features +- **Icons**: Font Awesome 6 +- **Responsive**: Mobile-First Design +- **Interaktivität**: Alpine.js für reaktive Komponenten -## Phase 9: Optimierung und Skalierung +### Database Schema ✅ +```sql +-- Core Tables +users (erweitert mit Social Features) +social_posts (Posts System) +social_comments (Kommentar System) +notifications (Benachrichtigungssystem) +user_settings (Benutzereinstellungen) +activities (Aktivitätsverfolgung) -- [ ] Performance-Optimierung für große Mindmaps -- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback -- [ ] Erweiterte Such- und Filterfunktionen -- [ ] Mobile Optimierung -- [ ] Offline-Funktionalität mit Synchronisierung +-- Relationship Tables +user_friendships (Freundschaftssystem) +user_follows (Follow System) +post_likes (Like System) +comment_likes (Comment Likes) +user_thought_bookmark (Bookmark System) +``` -## Technische Schulden und Refactoring +## 📊 API Endpunkte -- [ ] Trennung der Datenbank-Logik vom Flask-App-Code -- [ ] Einführung von Unit-Tests und Integration-Tests -- [ ] Überarbeitung der API-Dokumentation -- [ ] Caching-Strategien für bessere Performance -- [ ] Verbesserte Fehlerbehandlung und Logging +### Social Feed APIs ✅ +- `GET /api/social/posts` - Feed Posts abrufen +- `POST /api/social/posts` - Neuen Post erstellen +- `POST /api/social/posts/{id}/like` - Post liken/unliken +- `POST /api/social/posts/{id}/share` - Post teilen +- `POST /api/social/posts/{id}/bookmark` - Post bookmarken -## KI-Integration +### User Management APIs ✅ +- `GET /api/social/users/{id}` - Benutzerprofil abrufen +- `GET /api/social/users/search` - Benutzer suchen +- `POST /api/social/users/{id}/follow` - Benutzer folgen/entfolgen -### Aktuelle Implementation -- Integration von OpenAI mit dem gpt-4o-mini-Modell für den KI-Assistenten -- Datenbankzugriff für den KI-Assistenten, um direkt Informationen aus der Datenbank abzufragen -- Verbesserte Benutzeroberfläche für den KI-Assistenten mit kontextbezogenen Vorschlägen +### Notification APIs ✅ +- `GET /api/social/notifications` - Benachrichtigungen abrufen +- `POST /api/social/notifications/{id}/read` - Als gelesen markieren +- `POST /api/social/notifications/mark-all-read` - Alle als gelesen +- `DELETE /api/social/notifications/{id}` - Benachrichtigung löschen -### Zukünftige Verbesserungen -- Implementierung von Vektorsuche für präzisere Datenbank-Abfragen durch die KI -- Erweiterung der KI-Funktionalität für tiefere Analyse von Zusammenhängen zwischen Gedanken -- KI-gestützte Vorschläge für neue Verbindungen zwischen Gedanken basierend auf Inhaltsanalyse -- Finetuning des KI-Modells auf die spezifischen Anforderungen der Anwendung -- Erweiterung auf multimodale Fähigkeiten (Bild- und Textanalyse) +### Analytics APIs ✅ +- `GET /api/social/analytics/dashboard` - Benutzer-Analytics +- `GET /api/social/bookmarks` - Gebookmarkte Posts + +## 🔒 Sicherheit & Datenschutz + +### Implementierte Features ✅ +- CSRF Protection +- SQL Injection Prevention +- Input Validation & Sanitization +- Session Management +- Password Hashing +- Privacy Controls (Post Visibility) + +### Geplante Features +- 2FA Authentication +- Advanced Privacy Settings +- Data Export/Import +- GDPR Compliance Tools +- Content Moderation AI + +## 📱 Mobile Support + +### Aktuelle Features ✅ +- Responsive Design +- Touch-Friendly Interface +- Mobile Navigation +- Optimized Loading + +### Geplante Features +- PWA Support +- Offline Capabilities +- Push Notifications +- Native Mobile Apps + +## 🎯 Leistungsziele + +### Aktueller Status +- ✅ Grundlegende Performance +- ✅ Database Queries optimiert +- ✅ Frontend Responsiveness +- ✅ Strukturiertes Logging System + +### Ziele für nächste Phase +- 🎯 < 200ms API Response Zeit +- 🎯 90+ Lighthouse Score +- 🎯 Skalierung auf 10k+ Benutzer +- 🎯 99.9% Uptime + +## 🧪 Testing & Quality + +### Implementiert +- ✅ Manuelle Testing +- ✅ Error Handling +- ✅ **Erweiterte Logging & Monitoring mit visuellen Features** + - ✅ **Farbige, kategorisierte Logs für bessere Debugging-Erfahrung** + - ✅ **Performance-Monitoring mit Zeitstempel** + - ✅ **Component-spezifische Fehlerbehandlung** + - ✅ **Strukturierte JSON-Logs für Analyse** + +### Geplant +- 🔲 Automatisierte Unit Tests +- 🔲 Integration Tests +- 🔲 Performance Tests +- 🔲 Security Audits +- 🔲 Load Testing +- 🔲 **Log-basierte Alerting System** +- 🔲 **Automated Error Reporting** + +## 📈 Metriken & Analytics + +### User Engagement +- Posts pro Tag +- Kommentare und Likes +- Follow/Unfollow Raten +- Session Dauer +- Return User Rate + +### System Performance +- API Response Zeiten +- Database Performance +- Error Rates +- User Activity Patterns + +## 🛠️ Entwicklungsumgebung + +### Setup Requirements ✅ +```bash +# Virtual Environment +python3.11 -m venv venv +source venv/bin/activate + +# Dependencies +pip install -r requirements.txt + +# Database Migration +flask db upgrade + +# Development Server +python3.11 app.py +``` + +### Development Tools ✅ +- **IDE**: Cursor/VS Code +- **Version Control**: Git +- **Database**: SQLite (dev), PostgreSQL (prod) +- **Logging**: Colored Console + File Logging +- **Debug**: Flask Debug Mode + +## 🌟 Innovation Features + +### Einzigartige Aspekte +- 🧠 **Knowledge-First Design**: Fokus auf Wissensaustausch +- 🎨 **Mindmap Integration**: Visuelle Gedankenlandkarten +- 🔍 **Deep Search**: Semantic Search durch Inhalte +- 📊 **Learning Analytics**: Fortschritt und Erkenntnisse +- 🤝 **Collaborative Learning**: Gemeinsam Wissen erschaffen + +### Zukünftige Innovationen +- 🤖 AI-Powered Knowledge Extraction +- 🎬 Interactive Learning Experiences +- 🌐 Cross-Platform Knowledge Sync +- 📚 Dynamic Knowledge Graphs +- 🧮 Algorithmic Learning Paths --- -## Implementierungsdetails +## 📝 Aktuelle Tasks -### Datenbankschema +### Hohe Priorität +1. ⏳ Chat/Messaging System implementieren +2. ⏳ Advanced Image Upload mit Preview +3. ⏳ Performance Optimierungen +4. ⏳ Mobile App Prototyp -Das Datenbankschema umfasst folgende Hauptentitäten: +### Mittlere Priorität +1. 🔲 Gruppen/Communities Feature +2. 🔲 Advanced Analytics Dashboard +3. 🔲 Content Moderation Tools +4. 🔲 API Rate Limiting -1. **Category** - Wissenschaftliche Kategorien für die öffentliche Mindmap -2. **MindMapNode** - Öffentliche Mindmap-Knoten mit Metadaten -3. **UserMindmap** - Benutzerdefinierte Mindmaps -4. **UserMindmapNode** - Verknüpfung zwischen Benutzermindmaps und öffentlichen Knoten -5. **MindmapNote** - Benutzerspezifische Notizen -6. **Thought** - Gedanken und Inhalte, die Knoten zugeordnet sind -7. **ThoughtRelation** - Beziehungen zwischen Gedanken +### Niedrige Priorität +1. 🔲 Email Benachrichtigungen +2. 🔲 Export/Import Features +3. 🔲 Advanced Search Filters +4. 🔲 Theming System -### Frontend-Technologien +--- -- D3.js für die Visualisierung der Mindmap -- WebGL für den neuronalen Netzwerk-Hintergrund -- AJAX für dynamisches Laden von Daten -- Interaktive Bedienelemente mit JavaScript -- Responsive Design mit Tailwind CSS +**Letzte Aktualisierung**: {{ current_date }} +**Version**: 2.0.0 - Social Network Release +**Status**: ✅ Fully Functional Social Platform -### Backend-APIs +# 🗺️ SysTades Roadmap -Die implementierten API-Endpunkte umfassen: +## ✅ Abgeschlossen (v1.0 - v1.3) -- `/api/mindmap/public` - Abrufen der öffentlichen Mindmap-Struktur -- `/api/mindmap/user/` - Abrufen benutzerdefinierter Mindmaps -- `/api/mindmap//add_node` - Hinzufügen eines Knotens zur Benutzer-Mindmap -- `/api/mindmap//remove_node/` - Entfernen eines Knotens -- `/api/mindmap//update_node_position` - Aktualisierung von Knotenpositionen -- `/api/mindmap//notes` - Verwaltung von Notizen -- `/api/nodes//thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten -- `/api/get_dark_mode` - Abrufen der Dark Mode Einstellung +### 🎯 Grundfunktionen +- [x] **Benutzerauthentifizierung** - Registrierung, Login, Logout +- [x] **Interaktive Mindmap** - Cytoscape.js-basierte Visualisierung +- [x] **Gedankenverwaltung** - CRUD-Operationen für Thoughts +- [x] **Kategoriesystem** - Hierarchische Wissensorganisation +- [x] **Responsive Design** - Mobile-first Ansatz +- [x] **Dark/Light Mode** - Benutzerfreundliche Themes -## Neuronaler Netzwerk-Hintergrund +### 🤖 KI-Integration +- [x] **ChatGPT-Assistent** - Integrierter AI-Chat +- [x] **Intelligente Suche** - KI-gestützte Inhaltssuche +- [x] **Automatische Kategorisierung** - AI-basierte Thought-Klassifizierung -Der neue WebGL-basierte Hintergrund bietet: +### 🎨 UI/UX Verbesserungen +- [x] **Moderne Navigation** - Glassmorphism-Design +- [x] **Animationen** - Smooth Transitions und Hover-Effekte +- [x] **Accessibility** - ARIA-Labels und Keyboard-Navigation +- [x] **Performance-Optimierung** - Lazy Loading und Caching -- WebGL-basierte Rendering-Engine für optimale Performance -- Dynamische Knoten und Verbindungen mit realistischem Verhalten -- Clustering von neuronalen Knoten für natürlicheres Erscheinungsbild -- Simulation von neuronaler Aktivität und Signalweiterleitung -- Anpassbare visuelle Parameter (Helligkeit, Dichte, Geschwindigkeit) -- Vollständig responsives Design für alle Bildschirmgrößen +## 🚀 Neu implementiert (v1.4 - Social Network Update) -## Aktuelle Verbesserungen -- Tailwind CSS wurde auf CDN-Version aktualisiert (06.06.2024) -- Content Security Policy (CSP) für Tailwind CSS CDN und WebGL konfiguriert -- Behebung kritischer Fehler in der Mindmap-Knotenvisualisierung (15.06.2024) -- Verbesserte Verbindungserkennung zwischen Knoten implementiert -- Robuste Fehlerbehandlung für verschiedene API-Datenformate +### 📱 Social Network Features +- [x] **Social Feed** - Instagram/Twitter-ähnlicher Feed +- [x] **Post-System** - Erstellen, Liken, Kommentieren von Posts +- [x] **Follow-System** - Benutzer folgen und entfolgen +- [x] **Discover-Seite** - Trending Posts und empfohlene Benutzer +- [x] **Benutzerprofile** - Erweiterte Profile mit Posts, Mindmaps, Gedanken +- [x] **Benachrichtigungssystem** - Likes, Kommentare, Follows +- [x] **Community-Statistiken** - Aktive Benutzer, Posts, Mindmaps -## Zukünftige Aufgaben (Q3 2024) -- Implementierung des Tagging-Systems für Gedanken -- Quellenmanagement für Mindmap-Knoten -- Erweiterte Benutzerprofilfunktionen -- Verbesserung der mobilen Benutzererfahrung -- Integration von Exportfunktionen für Mindmaps +### 🧠 Erweiterte Mindmap-Features +- [x] **Kollaborative Bearbeitung** - Vorbereitung für Echtzeit-Kollaboration +- [x] **Mindmap-Export** - JSON-Export mit geplanten weiteren Formaten +- [x] **Mindmap-Sharing** - Teilen von Mindmaps in sozialen Netzwerken +- [x] **Erweiterte Toolbar** - Neue Bearbeitungsoptionen +- [x] **Vollbild-Modus** - Immersive Mindmap-Bearbeitung +- [x] **Schnelle Knoten-/Gedanken-Erstellung** - Direkt aus der Mindmap -*Zuletzt aktualisiert: 15.06.2024* +### 🔗 Integration & Vernetzung +- [x] **Gedanken in Posts teilen** - Wissenschaftliche Inhalte im Feed +- [x] **Mindmap-Knoten teilen** - Wissensbausteine verbreiten +- [x] **Cross-Platform Navigation** - Nahtlose Übergänge zwischen Features +- [x] **Unified Search** - Suche über alle Inhaltstypen -## [Entfernt] CORS-Unterstützung (flask-cors) -- Die flask-cors-Bibliothek und alle zugehörigen Initialisierungen wurden entfernt. -- CORS wird nicht mehr unterstützt oder benötigt. \ No newline at end of file +## 🔄 In Entwicklung (v1.5) + +### 🔄 Echtzeit-Features +- [ ] **Live-Kollaboration** - Mehrere Benutzer bearbeiten gleichzeitig Mindmaps +- [ ] **WebSocket-Integration** - Echtzeit-Updates für Feed und Benachrichtigungen +- [ ] **Live-Cursor** - Sehen wo andere Benutzer arbeiten +- [ ] **Änderungshistorie** - Versionskontrolle für Mindmaps + +### 💬 Erweiterte Kommunikation +- [ ] **Direktnachrichten** - Private Nachrichten zwischen Benutzern +- [ ] **Gruppen-Chats** - Themenbasierte Diskussionsgruppen +- [ ] **Video-Calls** - Integrierte Videokonferenzen für Kollaboration +- [ ] **Screen-Sharing** - Bildschirm teilen während Kollaboration + +## 📋 Geplant (v1.6 - v2.0) + +### 📊 Analytics & Insights +- [ ] **Lernfortschritt-Tracking** - Persönliche Wissensstatistiken +- [ ] **Mindmap-Analytics** - Nutzungsstatistiken und Hotspots +- [ ] **Community-Insights** - Trending-Themen und beliebte Inhalte +- [ ] **Empfehlungsalgorithmus** - Personalisierte Inhaltsvorschläge + +### 🎓 Bildungsfeatures +- [ ] **Kurssystem** - Strukturierte Lernpfade +- [ ] **Quizzes & Tests** - Wissensüberprüfung +- [ ] **Zertifikate** - Digitale Abschlüsse +- [ ] **Mentoring-System** - Experten-Schüler-Verbindungen + +### 🔧 Erweiterte Tools +- [ ] **PDF-Import** - Automatische Mindmap-Generierung aus Dokumenten +- [ ] **LaTeX-Support** - Mathematische Formeln in Gedanken +- [ ] **Multimedia-Integration** - Videos, Audio, Bilder in Mindmaps +- [ ] **API für Drittanbieter** - Integration mit anderen Tools + +### 🌐 Skalierung & Performance +- [ ] **Microservices-Architektur** - Bessere Skalierbarkeit +- [ ] **CDN-Integration** - Globale Content-Delivery +- [ ] **Caching-Optimierung** - Redis für bessere Performance +- [ ] **Load Balancing** - Hochverfügbarkeit + +## 🔮 Vision (v2.0+) + +### 🤖 Erweiterte KI +- [ ] **Personalisierte KI-Tutoren** - Individuelle Lernbegleitung +- [ ] **Automatische Mindmap-Generierung** - KI erstellt Mindmaps aus Text +- [ ] **Intelligente Verbindungen** - KI schlägt Gedankenverknüpfungen vor +- [ ] **Adaptive Lernpfade** - KI passt Inhalte an Lernstil an + +### 🌍 Globale Community +- [ ] **Mehrsprachigkeit** - Internationale Benutzergemeinschaft +- [ ] **Kultureller Austausch** - Globale Wissensnetzwerke +- [ ] **Übersetzungsfeatures** - Automatische Inhaltsübersetzung +- [ ] **Regionale Communities** - Lokale Wissensgruppen + +### 🔬 Forschungstools +- [ ] **Literaturverwaltung** - Integration mit wissenschaftlichen Datenbanken +- [ ] **Zitiersystem** - Automatische Quellenangaben +- [ ] **Peer-Review-System** - Wissenschaftliche Qualitätskontrolle +- [ ] **Publikationstools** - Direkte Veröffentlichung von Forschungsergebnissen + +--- + +## 📈 Metriken & Ziele + +### Technische Ziele +- **Performance**: < 2s Ladezeit für alle Seiten +- **Verfügbarkeit**: 99.9% Uptime +- **Skalierbarkeit**: 10.000+ gleichzeitige Benutzer +- **Sicherheit**: Zero-Trust-Architektur + +### Community-Ziele +- **Benutzer**: 1.000+ aktive Benutzer bis Ende 2024 +- **Inhalte**: 10.000+ Gedanken und 1.000+ Mindmaps +- **Engagement**: 70%+ monatliche Aktivitätsrate +- **Zufriedenheit**: 4.5+ Sterne Bewertung + +--- + +## 🤝 Beitragen + +Interessiert an der Mitarbeit? Hier sind die Bereiche, in denen wir Unterstützung suchen: + +### 👨‍💻 Entwicklung +- **Frontend**: React/Vue.js Komponenten +- **Backend**: Python/Flask API-Entwicklung +- **Mobile**: React Native App +- **DevOps**: Docker, Kubernetes, CI/CD + +### 🎨 Design +- **UI/UX**: Benutzeroberflächen-Design +- **Grafik**: Icons, Illustrationen, Branding +- **Animation**: Micro-Interactions und Transitions +- **Accessibility**: Barrierefreie Gestaltung + +### 📝 Content +- **Dokumentation**: Technische und Benutzer-Dokumentation +- **Tutorials**: Video- und Text-Anleitungen +- **Übersetzungen**: Mehrsprachige Inhalte +- **Community**: Moderation und Support + +--- + +*Letzte Aktualisierung: Januar 2024* +*Version: 1.4.0 - Social Network Update* \ No newline at end of file diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc index 61c40f8481f86afaf20bc8305aedd433cc2bba89..0940aa6bffbd6856859e5dbe1087b48f7fbf8d82 100644 GIT binary patch delta 47211 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 delta 26998 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 z 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 0), # Diese Kategorien haben Unterkategorien - 'icon': category.icon or 'fa-solid fa-circle' - }) - - # Kanten erstellen (vereinheitlichte Schlüssel) - edges = [{ - 'source': 'root', - 'target': f'cat_{category.id}', - 'strength': 0.8 - } for category in categories] - - return jsonify({ - 'success': True, - 'nodes': nodes, - 'edges': edges - }) - except Exception as e: - print(f"Fehler beim Abrufen der Root-Mindmap: {str(e)}") - traceback.print_exc() - return jsonify({ - 'success': False, - 'error': 'Root-Mindmap konnte nicht geladen werden', - 'details': str(e) - }), 500 - -# Spezifische Routen für Kategorien -@app.route('/api/mindmap/philosophy') -def get_philosophy_mindmap_data(): - return get_category_mindmap_data('Philosophie') - -@app.route('/api/mindmap/science') -def get_science_mindmap_data(): - return get_category_mindmap_data('Wissenschaft') - -@app.route('/api/mindmap/technology') -def get_technology_mindmap_data(): - return get_category_mindmap_data('Technologie') - -@app.route('/api/mindmap/arts') -def get_arts_mindmap_data(): - return get_category_mindmap_data('Künste') - -# Generische Route für spezifische Knoten -@app.route('/api/mindmap/') -def get_mindmap_data(node_id): - """Liefert die Daten für einen spezifischen Mindmap-Knoten.""" - try: - # Prüfen, ob es sich um eine spezielle Route handelt - if node_id in ['root', 'philosophy', 'science', 'technology', 'arts']: - return jsonify({ - 'success': False, - 'error': 'Ungültige Knoten-ID', - 'details': 'Diese ID ist für spezielle Routen reserviert' - }), 400 - - # Prüfen, ob es sich um eine Kategorie-ID handelt (Format: cat_X) - if node_id.startswith('cat_'): - category_id = None - try: - category_id = int(node_id.split('_')[1]) - except (IndexError, ValueError) as e: - print(f"Fehler beim Parsen der Kategorie-ID '{node_id}': {str(e)}") - return jsonify({ - 'success': False, - 'error': 'Ungültige Kategorie-ID', - 'details': f"Die ID '{node_id}' konnte nicht als Kategorie identifiziert werden" - }), 400 - - # Kategorie mit Unterkategorien laden - category = Category.query.options( - joinedload(Category.children) - ).get_or_404(category_id) - - # Basis-Knoten erstellen - nodes = [{ - 'id': f'cat_{category.id}', - 'name': category.name, - 'description': category.description or '', - 'color_code': category.color_code or '#9F7AEA', - 'is_center': True, - 'has_children': bool(category.children), - 'icon': category.icon or 'fa-solid fa-circle' - }] - - # Unterkategorien hinzufügen - for subcat in category.children: - nodes.append({ - 'id': f'cat_{subcat.id}', - 'name': subcat.name, - 'description': subcat.description or '', - 'color_code': subcat.color_code or '#9F7AEA', - 'category': category.name, - 'has_children': bool(subcat.children), - 'icon': subcat.icon or 'fa-solid fa-circle' - }) - - # Kanten erstellen (vereinheitlichte Schlüssel) - edges = [{ - 'source': f'cat_{category.id}', - 'target': f'cat_{subcat.id}', - 'strength': 0.8 - } for subcat in category.children] - - return jsonify({ - 'success': True, - 'nodes': nodes, - 'edges': edges - }) - - # Sonst: Normale Knoten-ID - # Knoten mit Unterknoten in einer Abfrage laden - node = MindMapNode.query.options( - joinedload(MindMapNode.children) - ).get_or_404(node_id) - - # Basis-Knoten erstellen - nodes = [{ - 'id': str(node.id), - 'name': node.name, - 'description': node.description or '', - 'color_code': node.color_code or '#9F7AEA', - 'is_center': True, - 'has_children': bool(node.children), - 'icon': node.icon or 'fa-solid fa-circle' - }] - - # Unterknoten hinzufügen - for child in node.children: - nodes.append({ - 'id': str(child.id), - 'name': child.name, - 'description': child.description or '', - 'color_code': child.color_code or '#9F7AEA', - 'category': node.name, - 'has_children': bool(child.children), - 'icon': child.icon or 'fa-solid fa-circle' - }) - - # Kanten erstellen (vereinheitlichte Schlüssel) - edges = [{ - 'source': str(node.id), - 'target': str(child.id), - 'strength': 0.8 - } for child in node.children] - - return jsonify({ - 'success': True, - 'nodes': nodes, - 'edges': edges - }) - except Exception as e: - print(f"Fehler beim Abrufen der Mindmap-Daten für Knoten {node_id}: {str(e)}") - return jsonify({ - 'success': False, - 'error': 'Mindmap-Daten konnten nicht geladen werden', - 'details': str(e) - }), 500 - -# API-Endpunkt zum Speichern von Mindmap-Änderungen -@app.route('/api/mindmap/save', methods=['POST']) -@login_required -def save_mindmap_changes(): - """Speichert Änderungen an der Mindmap.""" - try: - data = request.get_json() - - if not data: - return jsonify({ - 'success': False, - 'error': 'Keine Daten erhalten', - 'details': 'Der Request enthält keine JSON-Daten' - }), 400 - - # Überprüfen, ob die erforderlichen Daten vorhanden sind - if 'nodes' not in data or 'edges' not in data: - return jsonify({ - 'success': False, - 'error': 'Unvollständige Daten', - 'details': 'Nodes oder Edges fehlen' - }), 400 - - # Verarbeiten der Knoten - for node_data in data['nodes']: - # ID überprüfen: neue Knoten haben temporäre IDs, die mit 'new-' beginnen - node_id = node_data.get('id') - - if isinstance(node_id, str) and node_id.startswith('new-'): - # Neuen Knoten erstellen - new_node = MindMapNode( - name=node_data.get('name', 'Neuer Knoten'), - description=node_data.get('description', ''), - color_code=node_data.get('color_code', '#9F7AEA'), - icon=node_data.get('icon', 'fa-solid fa-circle'), - is_public=True, - created_by_id=current_user.id - ) - - # Kategorie zuordnen, falls vorhanden - category_name = node_data.get('category') - if category_name: - category = Category.query.filter_by(name=category_name).first() - if category: - new_node.category_id = category.id - - db.session.add(new_node) - # Wir müssen flushen, um eine ID zu erhalten (für die Kanten-Erstellung) - db.session.flush() - - # Temporäre ID für die Mapping-Tabelle speichern - node_data['real_id'] = new_node.id - else: - # Bestehenden Knoten aktualisieren - node = MindMapNode.query.get(int(node_id)) - if node: - node.name = node_data.get('name', node.name) - node.description = node_data.get('description', node.description) - node.color_code = node_data.get('color_code', node.color_code) - node.icon = node_data.get('icon', node.icon) - - # Kategorie aktualisieren, falls vorhanden - category_name = node_data.get('category') - if category_name: - category = Category.query.filter_by(name=category_name).first() - if category: - node.category_id = category.id - - # Position speichern, falls vorhanden - if 'position_x' in node_data and 'position_y' in node_data: - # Hier könnte die Position gespeichert werden, wenn das Modell erweitert wird - pass - - node_data['real_id'] = node.id - - # Bestehende Kanten für die betroffenen Knoten löschen - # (wir ersetzen alle Kanten durch die neuen) - node_ids = [int(node_data.get('real_id')) for node_data in data['nodes'] if 'real_id' in node_data] - - # Neue Kanten erstellen (mit den richtigen IDs aus der Mapping-Tabelle) - for edge_data in data['edges']: - source_id = edge_data.get('source') - target_id = edge_data.get('target') - - # Temporäre IDs durch reale IDs ersetzen - for node_data in data['nodes']: - if node_data.get('id') == source_id and 'real_id' in node_data: - source_id = node_data['real_id'] - if node_data.get('id') == target_id and 'real_id' in node_data: - target_id = node_data['real_id'] - - # Beziehung zwischen Knoten erstellen/aktualisieren - source_node = MindMapNode.query.get(int(source_id)) - target_node = MindMapNode.query.get(int(target_id)) - - if source_node and target_node: - # Prüfen, ob die Beziehung bereits existiert - if target_node not in source_node.children.all(): - source_node.children.append(target_node) - - db.session.commit() - - return jsonify({ - 'success': True, - 'message': 'Mindmap erfolgreich gespeichert', - 'node_mapping': {node_data.get('id'): node_data.get('real_id') - for node_data in data['nodes'] - if 'real_id' in node_data and node_data.get('id') != node_data.get('real_id')} - }) - - except Exception as e: - db.session.rollback() - print(f"Fehler beim Speichern der Mindmap: {str(e)}") - traceback.print_exc() - return jsonify({ - 'success': False, - 'error': 'Mindmap konnte nicht gespeichert werden', - 'details': str(e) - }), 500 - # Route zum expliziten Neu-Laden der Umgebungsvariablen @app.route('/admin/reload-env', methods=['POST']) @admin_required def reload_env(): - """Lädt die Umgebungsvariablen aus der .env-Datei neu.""" + """Lädt die .env-Datei neu""" try: - # Erzwinge das Neuladen der .env-Datei - load_dotenv(override=True, force=True) + # Lade die .env-Datei neu + from dotenv import load_dotenv + load_dotenv(override=True) - # OpenAI API-Key ist bereits fest kodiert - # client wurde bereits mit festem API-Key initialisiert + # Aktualisiere die App-Konfiguration + app.config['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY') + app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') + app.config['DATABASE_URL'] = os.getenv('DATABASE_URL') - # Weitere Umgebungsvariablen hier aktualisieren, falls nötig - app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', app.config['SECRET_KEY']) + # Logge die Aktion + ErrorHandler.log_exception( + Exception("Environment reloaded"), + endpoint="admin/reload-env", + code=200 + ) return jsonify({ 'success': True, - 'message': 'Umgebungsvariablen wurden erfolgreich neu geladen.' + 'message': 'Umgebungsvariablen erfolgreich neu geladen' }) except Exception as e: + ErrorHandler.log_exception(e, "admin/reload-env") return jsonify({ 'success': False, - 'message': f'Fehler beim Neuladen der Umgebungsvariablen: {str(e)}' + 'error': 'Fehler beim Neuladen der Umgebungsvariablen', + 'details': str(e) + }), 500 + +@app.route('/admin/api/dashboard-data', methods=['GET']) +@admin_required +def admin_dashboard_data(): + """Liefert Dashboard-Daten für den Admin-Bereich""" + try: + # Benutzerstatistiken + total_users = User.query.count() + active_users = User.query.filter_by(is_active=True).count() + admin_users = User.query.filter_by(role='admin').count() + + # Mindmap-Statistiken + total_mindmaps = UserMindmap.query.count() + public_mindmaps = UserMindmap.query.filter_by(is_private=False).count() + + # Gedanken-Statistiken + total_thoughts = Thought.query.count() + + # Knoten-Statistiken + total_nodes = MindMapNode.query.count() + public_nodes = MindMapNode.query.filter_by(is_public=True).count() + + # Social-Statistiken + total_posts = SocialPost.query.count() + total_comments = SocialComment.query.count() + total_notifications = Notification.query.count() + unread_notifications = Notification.query.filter_by(is_read=False).count() + + # Kategorien-Statistiken + total_categories = Category.query.count() + + # Neueste Aktivitäten (letzte 10) + recent_users = User.query.order_by(User.created_at.desc()).limit(5).all() + recent_thoughts = Thought.query.order_by(Thought.created_at.desc()).limit(5).all() + recent_mindmaps = UserMindmap.query.order_by(UserMindmap.created_at.desc()).limit(5).all() + + return jsonify({ + 'success': True, + 'data': { + 'users': { + 'total': total_users, + 'active': active_users, + 'admins': admin_users, + 'recent': [{'id': u.id, 'username': u.username, 'created_at': u.created_at.isoformat()} for u in recent_users] + }, + 'mindmaps': { + 'total': total_mindmaps, + 'public': public_mindmaps, + 'private': total_mindmaps - public_mindmaps, + 'recent': [{'id': m.id, 'name': m.name, 'created_at': m.created_at.isoformat()} for m in recent_mindmaps] + }, + 'thoughts': { + 'total': total_thoughts, + 'recent': [{'id': t.id, 'title': t.title, 'created_at': t.created_at.isoformat()} for t in recent_thoughts] + }, + 'nodes': { + 'total': total_nodes, + 'public': public_nodes + }, + 'social': { + 'posts': total_posts, + 'comments': total_comments, + 'notifications': total_notifications, + 'unread_notifications': unread_notifications + }, + 'categories': { + 'total': total_categories + } + } + }) + except Exception as e: + ErrorHandler.log_exception(e, "admin/api/dashboard-data") + return jsonify({ + 'success': False, + 'error': 'Fehler beim Laden der Dashboard-Daten', + 'details': str(e) }), 500 # Berechtigungsverwaltung für Mindmaps @@ -2705,10 +2531,820 @@ def check_mindmap_permission(mindmap_id, permission_type=None): return True, "" +def get_category_mindmap_data(category_name): + """Generische Funktion zum Abrufen der Mindmap-Daten für eine Kategorie.""" + try: + # Kategorie mit allen Unterkategorien in einer Abfrage laden + category = Category.query.filter_by(name=category_name).options( + joinedload(Category.children) + ).first_or_404() + + # Basis-Knoten erstellen + nodes = [{ + 'id': f'cat_{category.id}', + 'name': category.name, + 'description': category.description or '', + 'color_code': category.color_code or '#9F7AEA', + 'is_center': True, + 'has_children': bool(category.children), + 'icon': category.icon or 'fa-solid fa-circle' + }] + + # Unterkategorien hinzufügen + for subcat in category.children: + nodes.append({ + 'id': f'cat_{subcat.id}', + 'name': subcat.name, + 'description': subcat.description or '', + 'color_code': subcat.color_code or '#9F7AEA', + 'category': category_name, + 'has_children': bool(subcat.children), + 'icon': subcat.icon or 'fa-solid fa-circle' + }) + + # Kanten erstellen (vereinheitlichte Schlüssel) + edges = [{ + 'source': f'cat_{category.id}', + 'target': f'cat_{subcat.id}', + 'strength': 0.8 + } for subcat in category.children] + + return jsonify({ + 'success': True, + 'nodes': nodes, + 'edges': edges + }) + except Exception as e: + print(f"Fehler beim Abrufen der {category_name}-Mindmap: {str(e)}") + return jsonify({ + 'success': False, + 'error': f'{category_name}-Mindmap konnte nicht geladen werden', + 'details': str(e) + }), 500 + +@app.route('/api/mindmap/root') +def get_root_mindmap_data(): + """Liefert die Daten für die Root-Mindmap.""" + try: + # Hauptkategorien mit Unterkategorien in einer Abfrage laden + categories = Category.query.filter_by(parent_id=None).options( + joinedload(Category.children) + ).all() + + # Basis-Knoten erstellen + nodes = [{ + 'id': 'root', + 'name': 'Wissen', + 'description': 'Zentrale Wissensbasis', + 'color_code': '#4299E1', + 'is_center': True, + 'has_children': bool(categories), + 'icon': 'fa-solid fa-circle' + }] + + # Kategorien als Knoten hinzufügen + for category in categories: + nodes.append({ + 'id': f'cat_{category.id}', + 'name': category.name, + 'description': category.description or '', + 'color_code': category.color_code or '#9F7AEA', + 'category': category.name, + 'has_children': bool(category.children), + 'icon': category.icon or 'fa-solid fa-circle' + }) + + # Kanten erstellen (vereinheitlichte Schlüssel) + edges = [{ + 'source': 'root', + 'target': f'cat_{category.id}', + 'strength': 0.8 + } for category in categories] + + return jsonify({ + 'success': True, + 'nodes': nodes, + 'edges': edges + }) + except Exception as e: + print(f"Fehler beim Abrufen der Root-Mindmap: {str(e)}") + return jsonify({ + 'success': False, + 'error': 'Root-Mindmap konnte nicht geladen werden', + 'details': str(e) + }), 500 + +# Spezifische Routen für Kategorien +@app.route('/api/mindmap/philosophy') +def get_philosophy_mindmap_data(): + return get_category_mindmap_data('Philosophie') + +@app.route('/api/mindmap/science') +def get_science_mindmap_data(): + return get_category_mindmap_data('Wissenschaft') + +@app.route('/api/mindmap/technology') +def get_technology_mindmap_data(): + return get_category_mindmap_data('Technologie') + +@app.route('/api/mindmap/arts') +def get_arts_mindmap_data(): + return get_category_mindmap_data('Künste') + +# Generische Route für spezifische Knoten +@app.route('/api/mindmap/') +def get_mindmap_data(node_id): + """Liefert die Daten für einen spezifischen Mindmap-Knoten.""" + try: + # Prüfen, ob es sich um eine spezielle Route handelt + if node_id in ['root', 'philosophy', 'science', 'technology', 'arts']: + return jsonify({ + 'success': False, + 'error': 'Ungültige Knoten-ID', + 'details': 'Diese ID ist für spezielle Routen reserviert' + }), 400 + + # Prüfen, ob es sich um eine Kategorie-ID handelt (cat_X Format) + if node_id.startswith('cat_'): + try: + category_id = int(node_id.replace('cat_', '')) + category = Category.query.get_or_404(category_id) + + # Basis-Knoten erstellen + nodes = [{ + 'id': f'cat_{category.id}', + 'name': category.name, + 'description': category.description or '', + 'color_code': category.color_code or '#9F7AEA', + 'is_center': True, + 'has_children': bool(category.children), + 'icon': category.icon or 'fa-solid fa-circle' + }] + + # Unterkategorien hinzufügen + for subcat in category.children: + nodes.append({ + 'id': f'cat_{subcat.id}', + 'name': subcat.name, + 'description': subcat.description or '', + 'color_code': subcat.color_code or '#9F7AEA', + 'category': category.name, + 'has_children': bool(subcat.children), + 'icon': subcat.icon or 'fa-solid fa-circle' + }) + + # Kanten erstellen + edges = [{ + 'source': f'cat_{category.id}', + 'target': f'cat_{subcat.id}', + 'strength': 0.8 + } for subcat in category.children] + + return jsonify({ + 'success': True, + 'nodes': nodes, + 'edges': edges + }) + + except ValueError: + return jsonify({ + 'success': False, + 'error': 'Ungültige Kategorie-ID', + 'details': 'Kategorie-ID muss numerisch sein' + }), 400 + + # Normale MindMapNode-ID verarbeiten + try: + node_id_int = int(node_id) + except ValueError: + return jsonify({ + 'success': False, + 'error': 'Ungültige Knoten-ID', + 'details': 'Knoten-ID muss numerisch sein' + }), 400 + + # Knoten ohne Eager Loading laden (da children dynamic ist) + node = MindMapNode.query.get_or_404(node_id_int) + + # Basis-Knoten erstellen + nodes = [{ + 'id': str(node.id), + 'name': node.name, + 'description': node.description or '', + 'color_code': node.color_code or '#9F7AEA', + 'is_center': True, + 'has_children': node.children.count() > 0, # Dynamic query verwenden + 'icon': node.icon or 'fa-solid fa-circle' + }] + + # Unterknoten hinzufügen (dynamic relationship verwenden) + children = node.children.all() # Alle Kinder laden + for child in children: + nodes.append({ + 'id': str(child.id), + 'name': child.name, + 'description': child.description or '', + 'color_code': child.color_code or '#9F7AEA', + 'category': node.name, + 'has_children': child.children.count() > 0, # Dynamic query verwenden + 'icon': child.icon or 'fa-solid fa-circle' + }) + + # Kanten erstellen + edges = [{ + 'source': str(node.id), + 'target': str(child.id), + 'strength': 0.8 + } for child in children] + + return jsonify({ + 'success': True, + 'nodes': nodes, + 'edges': edges + }) + except Exception as e: + print(f"Fehler beim Abrufen der Mindmap-Daten für Knoten {node_id}: {str(e)}") + return jsonify({ + 'success': False, + 'error': 'Mindmap-Daten konnten nicht geladen werden', + 'details': str(e) + }), 500 + +# Social Feed Routes hinzufügen nach den bestehenden Routen +@app.route('/feed') +@login_required +@log_execution_time(component='SOCIAL') +def social_feed(): + """Hauptfeed-Seite mit Posts von gefolgten Benutzern""" + page = request.args.get('page', 1, type=int) + posts_per_page = 10 + + # Hole alle User-IDs von Benutzern, denen ich folge + meine eigene + followed_user_ids = [user.id for user in current_user.following] + all_user_ids = followed_user_ids + [current_user.id] + + # Posts von diesen Benutzern direkt mit einer Abfrage holen + all_posts = SocialPost.query.filter( + SocialPost.user_id.in_(all_user_ids) + ).order_by(SocialPost.created_at.desc()) + + posts = all_posts.paginate( + page=page, per_page=posts_per_page, error_out=False + ) + + return render_template('social/feed.html', posts=posts) + +@app.route('/discover') +@login_required +@log_execution_time(component='SOCIAL') +def discover(): + """Entdecke neue Inhalte und Benutzer""" + # Trending Posts (basierend auf Likes und Kommentaren) + trending_posts = SocialPost.query.order_by( + (SocialPost.like_count + SocialPost.comment_count).desc() + ).limit(20).all() + + # Empfohlene Benutzer (die viele Follower haben und denen wir nicht folgen) + # Subquery um Benutzer zu finden, denen der aktuelle Benutzer folgt + following_subquery = db.session.query(user_follows.c.followed_id).filter( + user_follows.c.follower_id == current_user.id + ).subquery() + + suggested_users = User.query.filter( + User.id != current_user.id, + ~User.id.in_(following_subquery) + ).order_by(User.follower_count.desc()).limit(5).all() + + # Community-Statistiken + from datetime import datetime, timedelta + today = datetime.utcnow().date() + + active_users_count = User.query.filter( + User.last_login >= datetime.utcnow() - timedelta(days=7) + ).count() + + posts_today_count = SocialPost.query.filter( + db.func.date(SocialPost.created_at) == today + ).count() + + mindmaps_count = UserMindmap.query.count() + thoughts_count = Thought.query.count() + + return render_template('social/discover.html', + trending_posts=trending_posts, + suggested_users=suggested_users, + active_users_count=active_users_count, + posts_today_count=posts_today_count, + mindmaps_count=mindmaps_count, + thoughts_count=thoughts_count) + +@app.route('/profile/') +@login_required +def user_profile(username): + """Benutzerprofil anzeigen""" + user = User.query.filter_by(username=username).first_or_404() + posts = SocialPost.query.filter_by(user_id=user.id).order_by( + SocialPost.created_at.desc() + ).limit(20).all() + + is_following = current_user.is_following(user) if user != current_user else False + + return render_template('social/profile.html', + user=user, + posts=posts, + is_following=is_following) + +# API Routes für Social Features +@app.route('/api/posts', methods=['POST']) +@login_required +@handle_api_exception +def create_post(): + """Erstelle einen neuen Social Post""" + data = request.get_json() + + if not data or 'content' not in data: + return ErrorHandler.api_error('Content ist erforderlich', 400) + + content = data['content'].strip() + if not content: + return ErrorHandler.api_error('Content darf nicht leer sein', 400) + + post = SocialPost( + content=content, + user_id=current_user.id, + post_type=data.get('post_type', 'text'), + visibility=data.get('visibility', 'public'), + image_url=data.get('image_url'), + video_url=data.get('video_url'), + link_url=data.get('link_url'), + shared_thought_id=data.get('shared_thought_id'), + shared_node_id=data.get('shared_node_id') + ) + + db.session.add(post) + current_user.post_count += 1 + db.session.commit() + + logger.info(f"Neuer Post erstellt von {current_user.username}", + component='SOCIAL', user=current_user.username) + + return jsonify({ + 'success': True, + 'post': post.to_dict(), + 'message': 'Post erfolgreich erstellt' + }) + +@app.route('/api/posts//like', methods=['POST']) +@login_required +@handle_api_exception +def like_post(post_id): + """Like/Unlike einen Post""" + post = SocialPost.query.get_or_404(post_id) + + if current_user in post.liked_by: + # Unlike + post.liked_by.remove(current_user) + post.like_count = max(0, post.like_count - 1) + action = 'unliked' + else: + # Like + post.liked_by.append(current_user) + post.like_count += 1 + action = 'liked' + + # Benachrichtigung für den Autor (wenn nicht selbst) + if post.author != current_user: + notification = Notification( + user_id=post.user_id, + type='like', + message=f'{current_user.username} hat deinen Post geliked', + related_user_id=current_user.id, + related_post_id=post.id + ) + db.session.add(notification) + + db.session.commit() + + return jsonify({ + 'success': True, + 'action': action, + 'like_count': post.like_count, + 'is_liked': current_user in post.liked_by + }) + +@app.route('/api/posts//comments', methods=['POST']) +@login_required +@handle_api_exception +def comment_on_post(post_id): + """Kommentar zu einem Post hinzufügen""" + post = SocialPost.query.get_or_404(post_id) + data = request.get_json() + + if not data or 'content' not in data: + return ErrorHandler.api_error('Content ist erforderlich', 400) + + content = data['content'].strip() + if not content: + return ErrorHandler.api_error('Kommentar darf nicht leer sein', 400) + + comment = SocialComment( + content=content, + user_id=current_user.id, + post_id=post.id, + parent_id=data.get('parent_id') + ) + + db.session.add(comment) + post.comment_count += 1 + + # Benachrichtigung für den Post-Autor + if post.author != current_user: + notification = Notification( + user_id=post.user_id, + type='comment', + message=f'{current_user.username} hat deinen Post kommentiert', + related_user_id=current_user.id, + related_post_id=post.id, + related_comment_id=comment.id + ) + db.session.add(notification) + + db.session.commit() + + return jsonify({ + 'success': True, + 'comment': comment.to_dict(), + 'message': 'Kommentar hinzugefügt' + }) + +@app.route('/api/users//follow', methods=['POST']) +@login_required +@handle_api_exception +def follow_user(user_id): + """Einem Benutzer folgen/entfolgen""" + user = User.query.get_or_404(user_id) + + if user == current_user: + return ErrorHandler.api_error('Du kannst dir nicht selbst folgen', 400) + + if current_user.is_following(user): + # Entfolgen + current_user.unfollow(user) + action = 'unfollowed' + message = f'Du folgst {user.username} nicht mehr' + else: + # Folgen + current_user.follow(user) + action = 'followed' + message = f'Du folgst jetzt {user.username}' + + db.session.commit() + + return jsonify({ + 'success': True, + 'action': action, + 'message': message, + 'is_following': current_user.is_following(user), + 'follower_count': user.follower_count + }) + +@app.route('/api/feed') +@login_required +@handle_api_exception +def get_feed_posts(): + """API für Feed-Posts""" + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 10, type=int) + + # Hole alle User-IDs von Benutzern, denen ich folge + meine eigene + followed_user_ids = [user.id for user in current_user.following] + all_user_ids = followed_user_ids + [current_user.id] + + # Posts von diesen Benutzern direkt mit einer Abfrage holen + all_posts = SocialPost.query.filter( + SocialPost.user_id.in_(all_user_ids) + ).order_by(SocialPost.created_at.desc()) + + posts = all_posts.paginate( + page=page, per_page=per_page, error_out=False + ) + + return jsonify({ + 'success': True, + 'posts': [post.to_dict() for post in posts.items], + 'has_next': posts.has_next, + 'has_prev': posts.has_prev, + 'page': posts.page, + 'pages': posts.pages, + 'total': posts.total + }) + +# Erweiterte Mindmap-API Routes +@app.route('/api/mindmaps//collaborate', methods=['POST']) +@login_required +@handle_api_exception +def start_collaboration(mindmap_id): + """Startet eine Kollaborationssitzung für eine Mindmap""" + mindmap = UserMindmap.query.get_or_404(mindmap_id) + + # Berechtigung prüfen + if not check_mindmap_permission(mindmap_id, PermissionType.EDIT): + return ErrorHandler.api_error('Keine Berechtigung zum Bearbeiten', 403) + + # Kollaborationssitzung starten (hier könnte man WebSocket-Integration hinzufügen) + return jsonify({ + 'success': True, + 'message': 'Kollaborationssitzung gestartet', + 'mindmap_id': mindmap_id, + 'session_id': str(uuid_pkg.uuid4()) + }) + +@app.route('/api/mindmaps//export', methods=['GET']) +@login_required +@handle_api_exception +def export_mindmap(mindmap_id): + """Exportiert eine Mindmap in verschiedenen Formaten""" + mindmap = UserMindmap.query.get_or_404(mindmap_id) + + # Berechtigung prüfen + if not check_mindmap_permission(mindmap_id, PermissionType.READ): + return ErrorHandler.api_error('Keine Berechtigung zum Lesen', 403) + + format_type = request.args.get('format', 'json') + + # Mindmap-Daten zusammenstellen + mindmap_data = { + 'id': mindmap.id, + 'name': mindmap.name, + 'description': mindmap.description, + 'nodes': [], + 'thoughts': [], + 'notes': [] + } + + # Knoten hinzufügen + for node_rel in UserMindmapNode.query.filter_by(user_mindmap_id=mindmap.id).all(): + node = node_rel.node + mindmap_data['nodes'].append({ + 'id': node.id, + 'name': node.name, + 'description': node.description, + 'x_position': node_rel.x_position, + 'y_position': node_rel.y_position, + 'color_code': node.color_code + }) + + # Gedanken hinzufügen + for thought in mindmap.thoughts: + mindmap_data['thoughts'].append({ + 'id': thought.id, + 'title': thought.title, + 'content': thought.content, + 'branch': thought.branch + }) + + # Notizen hinzufügen + for note in mindmap.notes: + mindmap_data['notes'].append({ + 'id': note.id, + 'content': note.content, + 'color_code': note.color_code + }) + + if format_type == 'json': + return jsonify({ + 'success': True, + 'data': mindmap_data, + 'format': 'json' + }) + else: + return ErrorHandler.api_error('Unsupported format', 400) + +@app.route('/api/posts//comments', methods=['GET']) +@login_required +@handle_api_exception +def get_post_comments(post_id): + """Lade Kommentare für einen Post""" + post = SocialPost.query.get_or_404(post_id) + + comments = SocialComment.query.filter_by(post_id=post.id).order_by( + SocialComment.created_at.asc() + ).all() + + return jsonify({ + 'success': True, + 'comments': [comment.to_dict() for comment in comments] + }) + +# Discover API Endpunkte +@app.route('/api/discover/users') +@login_required +@handle_api_exception +def discover_users(): + """API für Nutzer-Entdeckung""" + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 20, type=int) + + # Nutzer finden, die der aktuelle Nutzer noch nicht folgt + not_following_subquery = db.session.query(user_follows.c.followed_id).filter( + user_follows.c.follower_id == current_user.id + ).subquery() + + users = User.query.filter( + User.id != current_user.id, + ~User.id.in_(not_following_subquery) + ).order_by(User.created_at.desc()).limit(per_page).all() + + return jsonify({ + 'success': True, + 'users': [{ + 'id': user.id, + 'username': user.username, + 'display_name': user.display_name, + 'bio': user.bio or '', + 'follower_count': user.follower_count, + 'following_count': user.following_count, + 'post_count': SocialPost.query.filter_by(user_id=user.id).count(), + 'is_following': current_user.is_following(user), + 'is_verified': False # Kann später erweitert werden + } for user in users] + }) + +@app.route('/api/discover/posts') +@login_required +@handle_api_exception +def discover_posts(): + """API für beliebte Posts""" + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 20, type=int) + + # Posts sortiert nach Likes und Kommentaren (Popularität) + posts = SocialPost.query.order_by( + (SocialPost.like_count + SocialPost.comment_count).desc(), + SocialPost.created_at.desc() + ).limit(per_page).all() + + return jsonify({ + 'success': True, + 'posts': [post.to_dict() for post in posts] + }) + +@app.route('/api/discover/trending') +@login_required +@handle_api_exception +def discover_trending(): + """API für Trending-Inhalte""" + # Beispiel-Trending-Daten (kann später mit echten Daten erweitert werden) + trending_items = [ + { + 'id': 1, + 'title': '#KI', + 'description': 'Diskussionen über Künstliche Intelligenz', + 'count': 42, + 'growth': 156 + }, + { + 'id': 2, + 'title': '#Wissenschaft', + 'description': 'Neue wissenschaftliche Entdeckungen', + 'count': 28, + 'growth': 89 + }, + { + 'id': 3, + 'title': '#Innovation', + 'description': 'Innovative Ideen und Technologien', + 'count': 19, + 'growth': 67 + } + ] + + return jsonify({ + 'success': True, + 'trending': trending_items + }) + +@app.route('/api/search/users') +@login_required +@handle_api_exception +def search_users(): + """API für Nutzer-Suche""" + query = request.args.get('q', '').strip() + + if not query: + return jsonify({ + 'success': True, + 'users': [] + }) + + # Suche nach Nutzernamen und Anzeigenamen + users = User.query.filter( + db.or_( + User.username.ilike(f'%{query}%'), + User.display_name.ilike(f'%{query}%') + ) + ).filter(User.id != current_user.id).limit(10).all() + + return jsonify({ + 'success': True, + 'users': [{ + 'id': user.id, + 'username': user.username, + 'display_name': user.display_name, + 'bio': user.bio or '', + 'follower_count': user.follower_count, + 'is_following': current_user.is_following(user), + 'is_verified': False + } for user in users] + }) + +@app.route('/api/notifications') +@login_required +@handle_api_exception +def get_notifications(): + """API für Benachrichtigungen""" + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 20, type=int) + + notifications = Notification.query.filter_by( + user_id=current_user.id + ).order_by(Notification.created_at.desc()).paginate( + page=page, per_page=per_page, error_out=False + ) + + return jsonify({ + 'success': True, + 'notifications': [notification.to_dict() for notification in notifications.items], + 'has_next': notifications.has_next, + 'has_prev': notifications.has_prev, + 'page': notifications.page, + 'pages': notifications.pages, + 'total': notifications.total, + 'unread_count': Notification.query.filter_by( + user_id=current_user.id, is_read=False + ).count() + }) + +@app.route('/api/notifications//read', methods=['POST']) +@login_required +@handle_api_exception +def mark_notification_read(notification_id): + """Benachrichtigung als gelesen markieren""" + notification = Notification.query.filter_by( + id=notification_id, user_id=current_user.id + ).first_or_404() + + notification.is_read = True + db.session.commit() + + return jsonify({ + 'success': True, + 'message': 'Benachrichtigung als gelesen markiert' + }) + +@app.route('/api/notifications/mark-all-read', methods=['POST']) +@login_required +@handle_api_exception +def mark_all_notifications_read(): + """Alle Benachrichtigungen als gelesen markieren""" + Notification.query.filter_by( + user_id=current_user.id, is_read=False + ).update({'is_read': True}) + + db.session.commit() + + return jsonify({ + 'success': True, + 'message': 'Alle Benachrichtigungen als gelesen markiert' + }) + +# ... existing code ... + # Flask starten if __name__ == '__main__': + # Initialize database properly with app context + try: + success = init_app_database(app) + if success: + logger.info("Datenbank erfolgreich initialisiert", component='DB') + else: + logger.warning("Datenbankinitialisierung fehlgeschlagen. Einige Funktionen könnten eingeschränkt sein.", component='ERROR') + except Exception as e: + logger.error(f"Fehler bei der Datenbankinitialisierung: {e}", component='ERROR') + + # Erstelle alle Datenbanktabellen with app.app_context(): - # Make sure tables exist - db.create_all() - socketio.run(app, debug=True, host='0.0.0.0') - + try: + db.create_all() + logger.info("Datenbanktabellen erstellt/aktualisiert", component='DB') + except Exception as e: + logger.error(f"Fehler beim Erstellen der Datenbanktabellen: {e}", component='ERROR') + + # Starte den Flask-Entwicklungsserver + logger.info("Starte Flask-Entwicklungsserver auf http://localhost:5000", component='SYSTEM') + app.run( + debug=True, + host='0.0.0.0', + port=5000, + threaded=True + ) diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..ccebab0 --- /dev/null +++ b/cookies.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_127.0.0.1 FALSE / FALSE 0 session .eJwlzjEOwjAMQNG7ZGaIYztJe5nKjm2BACG1dELcnUiMf3n6n7TF7sc1re_99EvabpbWZI7cikB4dsoylLrmcKXSormH-OhKoQRSAy0v3kEzDqJlSNFCg8NIW25sfYChAgryFIWxdqyskqFWtNIdiF3awiZRaq9TzGmOnIfv_xuYabLft-fLPK0hj8O_P-1dNpA.aDdoog.bmKi2y6o3HQgIk4gwDvhirnxuoM diff --git a/database/systades.db b/database/systades.db index 5628e0458057f500500ca74a508fa327fcff9185..39b4d9513b47ef6744355dddf3d2397e99558521 100644 GIT binary patch delta 4148 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 ZZc(U?b(JfAKYl};eoEu2@!z>i{|DXR>Gl8s delta 2294 zcmd5-U2IcT9KZkD_O|PKd-u`qZtL2$`zph&+$JHsb7=1tb( zqw4_~xvZxg3mp&u8{Xal-!oAg!#}&BsA1`V79*P-wBjvQP!Hufy$ZqrW%y(@)PfZ+ zR>O@3i+kl;zGlAOm2=at*YACGT_{PS?OKV6etR8<=yi z@Gt_ok6f??tg~h?^G+um*_ZRWp%?J0ZNTC&2Qe_y2op6Van3q2Iarn(qG)!RVwde_ z=VvakQ!|5Lr}zys!_hLH3@RGaMqk=nn7Z`I*yz|EJ&;I^j>Si`B#LBq#gL*#kSdVT zqW461r=th@H}Bt+Ob?GqEl3W9kQUYjL06Nq91}%J2nDsc9+y=mE{U;lNEN~fT~sx4 z5)LA1N>^h_P!1_UA)zR7Bp{_zP&!4yi-d?MN2E|dlvDx91&_w{%s5(|O4>;93ZaOk zMg%n=3t>q>Gi|h;a(3pUa^_(}O=R?R{$Cr@)B{01-}BrHV)t-&@21e!J?g=}7A=vK zlSw6^g=29csf7|rq-u($E1{s8NG4UXi%Ei%OsJA3#f3ynQ?+nN(=|;;auDeyFYk{j zD2rj@&{Uc@_hzX81y}#m0z{;LQ~)dSkmhbu+)v!ig-Y*L&SNn+A)i>4oc0Y_>sSLGJ&gngMQ+;ueYaIqt;bTWV7=S125jP#}a9 zK}IaO*=WB=adX@N$J#H>&%9@!>M~j=CtDVcTG9t%=|2*UykaOKhw^X`!a|UomteJ4 zB6S)5#6mElr~w6qRZ&@Ep(yFii-KKr$;40|o~5e@g2f05Mnn`q!LTT(50i-EiMgfj z708p2Wi=2)NS0UcEIfu)i6XEIF!o#H8`Blovg<5t%Rd$a)d$pfQ~xwXe)G?`Lz^jswUwK;7LCybyeMi zJ<*{a?@;u~{vL1sac?KQ^e>~Ok;_Kw3JiF7xR4J*GWm3@EDgEWJ#-tK7C=~m`GRrm zZXWW*FAw|*#(a&kf5<*#3s{lGXTCN+bIDAH8}ZYf(153E=>}udVY>&PtR + coercions.expect( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/coercions.py", line 406, in expect + insp._post_inspect + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/util/langhelpers.py", line 1260, in __get__ + obj.__dict__[self.__name__] = result = self.fget(obj) + ^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2707, in _post_inspect + self._check_configure() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2386, in _check_configure + _configure_registries({self.registry}, cascade=True) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 4199, in _configure_registries + _do_configure_registries(registries, cascade) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 4240, in _do_configure_registries + mapper._post_configure_properties() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2403, in _post_configure_properties + prop.init() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py", line 579, in init + self.do_init() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 1637, in do_init + self._setup_join_conditions() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 1882, in _setup_join_conditions + self._join_condition = jc = JoinCondition( + ^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2306, in __init__ + self._determine_joins() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2463, in _determine_joins + raise sa_exc.AmbiguousForeignKeysError( +sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship User.notifications - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table. + [in /home/core/dev/website/app.py:93] +2025-05-28 20:48:49,541 ERROR: Fehler 500: Could not determine join condition between parent/child tables on relationship User.notifications - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table. +Endpoint: /mindmap, Method: GET, IP: 127.0.0.1 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2419, in _determine_joins + self.primaryjoin = join_condition( + ^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/util.py", line 123, in join_condition + return Join._join_condition( + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py", line 1346, in _join_condition + cls._joincond_trim_constraints( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py", line 1491, in _joincond_trim_constraints + raise exc.AmbiguousForeignKeysError( +sqlalchemy.exc.AmbiguousForeignKeysError: Can't determine join between 'user' and 'notification'; tables have more than one foreign key constraint relationship between them. Please specify the 'onclause' of this join explicitly. + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 498, in mindmap + wissen_node = MindMapNode.query.filter_by(name="Wissen").first() + ^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/model.py", line 22, in __get__ + return cls.query_class( + ^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 276, in __init__ + self._set_entities(entities) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 288, in _set_entities + self._raw_columns = [ + ^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 289, in + coercions.expect( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/coercions.py", line 406, in expect + insp._post_inspect + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/util/langhelpers.py", line 1260, in __get__ + obj.__dict__[self.__name__] = result = self.fget(obj) + ^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2707, in _post_inspect + self._check_configure() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2386, in _check_configure + _configure_registries({self.registry}, cascade=True) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 4199, in _configure_registries + _do_configure_registries(registries, cascade) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 4240, in _do_configure_registries + mapper._post_configure_properties() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2403, in _post_configure_properties + prop.init() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py", line 579, in init + self.do_init() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 1637, in do_init + self._setup_join_conditions() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 1882, in _setup_join_conditions + self._join_condition = jc = JoinCondition( + ^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2306, in __init__ + self._determine_joins() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2463, in _determine_joins + raise sa_exc.AmbiguousForeignKeysError( +sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship User.notifications - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table. + [in /home/core/dev/website/app.py:93] +2025-05-28 20:49:23,575 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:49:25,085 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:49:25,085 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:50:43,195 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:50:44,566 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:50:44,566 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:50:47,335 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:50:48,583 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:50:48,583 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:50:50,808 ERROR: Fehler 500: Could not determine join condition between parent/child tables on relationship User.notifications - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table. +Endpoint: /mindmap, Method: GET, IP: 127.0.0.1 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2419, in _determine_joins + self.primaryjoin = join_condition( + ^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/util.py", line 123, in join_condition + return Join._join_condition( + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py", line 1346, in _join_condition + cls._joincond_trim_constraints( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py", line 1491, in _joincond_trim_constraints + raise exc.AmbiguousForeignKeysError( +sqlalchemy.exc.AmbiguousForeignKeysError: Can't determine join between 'user' and 'notification'; tables have more than one foreign key constraint relationship between them. Please specify the 'onclause' of this join explicitly. + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 498, in mindmap + wissen_node = MindMapNode.query.filter_by(name="Wissen").first() + ^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/model.py", line 22, in __get__ + return cls.query_class( + ^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 276, in __init__ + self._set_entities(entities) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 288, in _set_entities + self._raw_columns = [ + ^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 289, in + coercions.expect( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/coercions.py", line 406, in expect + insp._post_inspect + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/util/langhelpers.py", line 1260, in __get__ + obj.__dict__[self.__name__] = result = self.fget(obj) + ^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2707, in _post_inspect + self._check_configure() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2386, in _check_configure + _configure_registries({self.registry}, cascade=True) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 4199, in _configure_registries + _do_configure_registries(registries, cascade) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 4240, in _do_configure_registries + mapper._post_configure_properties() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2403, in _post_configure_properties + prop.init() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py", line 579, in init + self.do_init() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 1637, in do_init + self._setup_join_conditions() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 1882, in _setup_join_conditions + self._join_condition = jc = JoinCondition( + ^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2306, in __init__ + self._determine_joins() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2463, in _determine_joins + raise sa_exc.AmbiguousForeignKeysError( +sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship User.notifications - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table. + [in /home/core/dev/website/app.py:93] +2025-05-28 20:50:50,808 ERROR: Fehler 500: Could not determine join condition between parent/child tables on relationship User.notifications - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table. +Endpoint: /mindmap, Method: GET, IP: 127.0.0.1 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2419, in _determine_joins + self.primaryjoin = join_condition( + ^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/util.py", line 123, in join_condition + return Join._join_condition( + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py", line 1346, in _join_condition + cls._joincond_trim_constraints( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py", line 1491, in _joincond_trim_constraints + raise exc.AmbiguousForeignKeysError( +sqlalchemy.exc.AmbiguousForeignKeysError: Can't determine join between 'user' and 'notification'; tables have more than one foreign key constraint relationship between them. Please specify the 'onclause' of this join explicitly. + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 498, in mindmap + wissen_node = MindMapNode.query.filter_by(name="Wissen").first() + ^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/model.py", line 22, in __get__ + return cls.query_class( + ^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 276, in __init__ + self._set_entities(entities) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 288, in _set_entities + self._raw_columns = [ + ^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 289, in + coercions.expect( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/coercions.py", line 406, in expect + insp._post_inspect + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/util/langhelpers.py", line 1260, in __get__ + obj.__dict__[self.__name__] = result = self.fget(obj) + ^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2707, in _post_inspect + self._check_configure() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2386, in _check_configure + _configure_registries({self.registry}, cascade=True) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 4199, in _configure_registries + _do_configure_registries(registries, cascade) + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 4240, in _do_configure_registries + mapper._post_configure_properties() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 2403, in _post_configure_properties + prop.init() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py", line 579, in init + self.do_init() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 1637, in do_init + self._setup_join_conditions() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 1882, in _setup_join_conditions + self._join_condition = jc = JoinCondition( + ^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2306, in __init__ + self._determine_joins() + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2463, in _determine_joins + raise sa_exc.AmbiguousForeignKeysError( +sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship User.notifications - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table. + [in /home/core/dev/website/app.py:93] +2025-05-28 20:50:55,438 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +Endpoint: /admin/api/dashboard-data, Method: GET, IP: 192.168.1.100 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request + self.raise_routing_exception(req) + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception + raise request.routing_exception # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request + result = self.url_adapter.match(return_rule=True) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match + raise NotFound() from None +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + [in /home/core/dev/website/app.py:93] +2025-05-28 20:50:55,438 ERROR: Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +Endpoint: /admin/api/dashboard-data, Method: GET, IP: 192.168.1.100 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request + self.raise_routing_exception(req) + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception + raise request.routing_exception # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request + result = self.url_adapter.match(return_rule=True) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match + raise NotFound() from None +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + [in /home/core/dev/website/app.py:93] +2025-05-28 20:51:52,977 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:51:54,651 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:51:54,651 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:52:20,482 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:52:21,918 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:52:21,918 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:52:24,959 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:52:26,560 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:52:26,560 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:52:33,015 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:52:34,262 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:52:34,262 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:52:50,332 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:16,183 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:17,481 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:17,481 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:37,112 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:41,327 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:42,692 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:42,692 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:46,229 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:47,816 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:47,816 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:59,574 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:53:59,671 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:54:01,103 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:54:01,103 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:54:03,761 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:54:05,299 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:54:05,299 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:54:15,798 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:56:06,949 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:56:27,050 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:56:28,688 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:56:28,688 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:56:31,741 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:56:33,386 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:56:33,386 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:58:50,714 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:58:51,812 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:58:51,812 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:58:55,784 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:58:56,892 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:58:56,892 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 21:23:04 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:23:04 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:23:06 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:26:38 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:26:38 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:26:39 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:26:40 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:26:40 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:26:40 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:27:05 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:27:05 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:27:06 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:28:07 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:28:07 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:28:08 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:28:09 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:28:09 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:28:09 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:28:11 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:28:11 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:28:13 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:28:13 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:28:13 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:28:13 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:28:29 | INFO | SysTades | PERFORMANCE | Performance: login Ausführungszeit = 15.365123748779297ms +2025-05-28 21:28:32 | WARNING | SysTades | AUTH | Anmeldung fehlgeschlagen für 'clickcandit@gmail.com' - Grund: invalid_credentials +2025-05-28 21:28:32 | INFO | SysTades | PERFORMANCE | Performance: login Ausführungszeit = 12.782096862792969ms +2025-05-28 21:28:37 | WARNING | SysTades | AUTH | Anmeldung fehlgeschlagen für 'admin' - Grund: invalid_credentials +2025-05-28 21:28:37 | INFO | SysTades | PERFORMANCE | Performance: login Ausführungszeit = 145.58863639831543ms +2025-05-28 21:28:59 | INFO | SysTades | AUTH | Benutzer 'admin' erfolgreich angemeldet +2025-05-28 21:28:59 | INFO | SysTades | PERFORMANCE | Performance: login Ausführungszeit = 227.59008407592773ms +2025-05-28 21:29:08 | ERROR | SysTades | ERROR | Fehler 500: 405 Method Not Allowed: The method is not allowed for the requested URL. +Endpoint: /api/thoughts, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request + self.raise_routing_exception(req) + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception + raise request.routing_exception # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request + result = self.url_adapter.match(return_rule=True) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 619, in match + raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None +werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL. + +2025-05-28 21:43:30 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:43:30 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:43:32 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:43:32 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:43:32 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:43:32 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:43:34 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:43:34 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:43:35 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:43:35 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:43:35 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:43:35 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler in social_feed nach 2.83ms - Exception: AttributeError: followed_id +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__ + return self._index[key][1] + ~~~~~~~~~~~^^^^^ +KeyError: 'followed_id' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2774, in social_feed + followed_posts = current_user.get_feed_posts(limit=100) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/models.py", line 193, in get_feed_posts + followed_users, SocialPost.user_id == followed_users.c.followed_id + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__ + raise AttributeError(key) from err +AttributeError: followed_id + +2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler 500: followed_id +Endpoint: /feed, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__ + return self._index[key][1] + ~~~~~~~~~~~^^^^^ +KeyError: 'followed_id' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view + return current_app.ensure_sync(func)(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2774, in social_feed + followed_posts = current_user.get_feed_posts(limit=100) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/models.py", line 193, in get_feed_posts + followed_users, SocialPost.user_id == followed_users.c.followed_id + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__ + raise AttributeError(key) from err +AttributeError: followed_id + +2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler in discover nach 16.89ms - Exception: AttributeError: 'AppenderQuery' object has no attribute 'contains' +Traceback (most recent call last): + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2800, in discover + ~current_user.following.contains(User.id) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AttributeError: 'AppenderQuery' object has no attribute 'contains' + +2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler 500: 'AppenderQuery' object has no attribute 'contains' +Endpoint: /discover, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view + return current_app.ensure_sync(func)(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2800, in discover + ~current_user.following.contains(User.id) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AttributeError: 'AppenderQuery' object has no attribute 'contains' + +2025-05-28 21:44:42 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:44:42 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:44:43 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:44:43 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:44:43 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:44:43 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:45:40 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:45:40 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:45:42 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:46:06 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:46:06 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:46:07 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:46:07 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:46:07 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:46:08 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:46:08 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:46:08 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:46:08 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:46:11 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:46:11 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:46:12 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:46:12 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:46:12 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:46:12 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler in social_feed nach 54.92ms - Exception: OperationalError: (sqlite3.OperationalError) near "UNION": syntax error +[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id +FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC + LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC + LIMIT ? OFFSET ?] +[parameters: (1, 100, 0, 1, 10, 0)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context + self.dialect.do_execute( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute + cursor.execute(statement, parameters) +sqlite3.OperationalError: near "UNION": syntax error + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2782, in social_feed + posts = all_posts.paginate( + ^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate + return QueryPagination( + ^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__ + items = self._query_items() + ^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items + out = query.limit(self.per_page).offset(self._query_offset).all() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all + return self._iter().all() # type: ignore + ^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter + result: Union[ScalarResult[_T], Result[_T]] = self.session.execute( + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute + return self._execute_internal( + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal + result: Result[Any] = compile_state_cls.orm_execute_statement( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement + result = conn.execute( + ^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute + return meth( + ^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection + return connection._execute_clauseelement( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement + ret = self._execute_context( + ^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context + return self._exec_single_context( + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context + self._handle_dbapi_exception( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception + raise sqlalchemy_exception.with_traceback(exc_info[2]) from e + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context + self.dialect.do_execute( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute + cursor.execute(statement, parameters) +sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error +[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id +FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC + LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC + LIMIT ? OFFSET ?] +[parameters: (1, 100, 0, 1, 10, 0)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) + +2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler 500: (sqlite3.OperationalError) near "UNION": syntax error +[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id +FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC + LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC + LIMIT ? OFFSET ?] +[parameters: (1, 100, 0, 1, 10, 0)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +Endpoint: /feed, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context + self.dialect.do_execute( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute + cursor.execute(statement, parameters) +sqlite3.OperationalError: near "UNION": syntax error + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view + return current_app.ensure_sync(func)(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2782, in social_feed + posts = all_posts.paginate( + ^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate + return QueryPagination( + ^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__ + items = self._query_items() + ^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items + out = query.limit(self.per_page).offset(self._query_offset).all() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all + return self._iter().all() # type: ignore + ^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter + result: Union[ScalarResult[_T], Result[_T]] = self.session.execute( + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute + return self._execute_internal( + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal + result: Result[Any] = compile_state_cls.orm_execute_statement( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement + result = conn.execute( + ^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute + return meth( + ^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection + return connection._execute_clauseelement( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement + ret = self._execute_context( + ^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context + return self._exec_single_context( + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context + self._handle_dbapi_exception( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception + raise sqlalchemy_exception.with_traceback(exc_info[2]) from e + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context + self.dialect.do_execute( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute + cursor.execute(statement, parameters) +sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error +[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id +FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC + LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC + LIMIT ? OFFSET ?] +[parameters: (1, 100, 0, 1, 10, 0)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) + +2025-05-28 21:48:23 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:48:23 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:48:25 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:48:25 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:48:25 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:48:25 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:48:39 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:48:39 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:48:41 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:48:41 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:48:41 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:48:41 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:48:42 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:48:42 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:48:44 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:48:44 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:48:44 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:48:44 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:48:48 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +Endpoint: /sw.js, Method: GET, IP: 127.0.0.1 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request + self.raise_routing_exception(req) + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception + raise request.routing_exception # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request + result = self.url_adapter.match(return_rule=True) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match + raise NotFound() from None +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +2025-05-28 21:48:50 | INFO | SysTades | PERFORMANCE | Performance: social_feed Ausführungszeit = 39.086341857910156ms +2025-05-28 21:48:50 | INFO | SysTades | AUTH | Benutzer 'admin' erfolgreich angemeldet +2025-05-28 21:48:50 | INFO | SysTades | PERFORMANCE | Performance: login Ausführungszeit = 173.68626594543457ms +2025-05-28 21:48:54 | INFO | SysTades | PERFORMANCE | Performance: discover Ausführungszeit = 198.16184043884277ms +2025-05-28 21:48:54 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +Endpoint: /static/fonts/inter-regular.woff2, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 257, in + view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 305, in send_static_file + return send_from_directory( + ^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/helpers.py", line 554, in send_from_directory + return werkzeug.utils.send_from_directory( # type: ignore[return-value] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/utils.py", line 574, in send_from_directory + raise NotFound() +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +2025-05-28 21:48:54 | INFO | SysTades | PERFORMANCE | Performance: social_feed Ausführungszeit = 3.8046836853027344ms +2025-05-28 21:49:17 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +Endpoint: /sw.js, Method: GET, IP: 127.0.0.1 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request + self.raise_routing_exception(req) + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception + raise request.routing_exception # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request + result = self.url_adapter.match(return_rule=True) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match + raise NotFound() from None +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +2025-05-28 21:50:24 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:50:24 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:55:43 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:55:43 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:55:44 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:55:44 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:55:44 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:55:44 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:55:46 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:55:46 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:55:47 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:55:47 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:55:47 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:55:47 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:55:51 | INFO | SysTades | PERFORMANCE | Performance: social_feed Ausführungszeit = 34.85536575317383ms +2025-05-28 21:55:54 | INFO | SysTades | PERFORMANCE | Performance: discover Ausführungszeit = 50.58622360229492ms +2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/dev/website/app.py", line 424, in wrapper + return func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 3141, in discover_users + not_following_subquery = db.session.query(follows.c.followed_id).filter( + ^^^^^^^ +NameError: name 'follows' is not defined + +2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/dev/website/app.py", line 424, in wrapper + return func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 3141, in discover_users + not_following_subquery = db.session.query(follows.c.followed_id).filter( + ^^^^^^^ +NameError: name 'follows' is not defined + +2025-05-28 21:56:13 | INFO | SysTades | PERFORMANCE | Performance: social_feed Ausführungszeit = 13.908147811889648ms +2025-05-28 21:56:25 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +Endpoint: /auth/login, Method: GET, IP: 127.0.0.1 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request + self.raise_routing_exception(req) + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception + raise request.routing_exception # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request + result = self.url_adapter.match(return_rule=True) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match + raise NotFound() from None +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +2025-05-28 21:56:34 | INFO | SysTades | PERFORMANCE | Performance: discover Ausführungszeit = 11.503219604492188ms +2025-05-28 21:56:41 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/dev/website/app.py", line 424, in wrapper + return func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 3141, in discover_users + not_following_subquery = db.session.query(follows.c.followed_id).filter( + ^^^^^^^ +NameError: name 'follows' is not defined + +2025-05-28 21:57:14 | INFO | SysTades | PERFORMANCE | Performance: login Ausführungszeit = 3.179311752319336ms +2025-05-28 21:57:25 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/dev/website/app.py", line 424, in wrapper + return func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 3141, in discover_users + not_following_subquery = db.session.query(user_follows.c.followed_id).filter( + ^^^^^^^ +NameError: name 'follows' is not defined + +2025-05-28 21:58:02 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/dev/website/app.py", line 424, in wrapper + return func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 3141, in discover_users + users = User.query.filter( + +NameError: name 'follows' is not defined + +2025-05-28 21:58:16 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:58:16 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:58:18 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:58:18 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:58:18 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:58:18 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:58:20 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:58:20 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:58:22 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:58:22 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:58:22 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:58:22 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:58:48 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:58:48 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:58:49 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:58:50 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:58:50 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:58:50 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 21:58:52 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 21:58:52 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 21:58:53 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 21:58:53 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 21:58:54 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 21:58:54 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 22:08:02 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 22:08:02 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 22:08:04 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 22:08:04 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 22:08:04 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 22:08:04 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 22:08:06 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 22:08:06 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +2025-05-28 22:08:07 | INFO | SysTades | SYSTEM | OpenAI API-Verbindung erfolgreich hergestellt +2025-05-28 22:08:07 | INFO | SysTades | DB | Datenbank erfolgreich initialisiert +2025-05-28 22:08:07 | INFO | SysTades | DB | Datenbanktabellen erstellt/aktualisiert +2025-05-28 22:08:07 | INFO | SysTades | SYSTEM | Starte Flask-Entwicklungsserver auf http://localhost:5000 +2025-05-28 22:08:09 | INFO | SysTades | PERFORMANCE | Performance: social_feed Ausführungszeit = 60.69636344909668ms +2025-05-28 22:08:11 | INFO | SysTades | PERFORMANCE | Performance: discover Ausführungszeit = 212.85009384155273ms +2025-05-28 22:08:24 | INFO | SysTades | PERFORMANCE | Performance: social_feed Ausführungszeit = 5.427837371826172ms diff --git a/logs/errors.log b/logs/errors.log new file mode 100644 index 0000000..4719c92 --- /dev/null +++ b/logs/errors.log @@ -0,0 +1,424 @@ +2025-05-28 21:29:08 | ERROR | SysTades | ERROR | Fehler 500: 405 Method Not Allowed: The method is not allowed for the requested URL. +Endpoint: /api/thoughts, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request + self.raise_routing_exception(req) + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception + raise request.routing_exception # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request + result = self.url_adapter.match(return_rule=True) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 619, in match + raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None +werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL. + +2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler in social_feed nach 2.83ms - Exception: AttributeError: followed_id +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__ + return self._index[key][1] + ~~~~~~~~~~~^^^^^ +KeyError: 'followed_id' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2774, in social_feed + followed_posts = current_user.get_feed_posts(limit=100) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/models.py", line 193, in get_feed_posts + followed_users, SocialPost.user_id == followed_users.c.followed_id + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__ + raise AttributeError(key) from err +AttributeError: followed_id + +2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler 500: followed_id +Endpoint: /feed, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__ + return self._index[key][1] + ~~~~~~~~~~~^^^^^ +KeyError: 'followed_id' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view + return current_app.ensure_sync(func)(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2774, in social_feed + followed_posts = current_user.get_feed_posts(limit=100) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/models.py", line 193, in get_feed_posts + followed_users, SocialPost.user_id == followed_users.c.followed_id + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__ + raise AttributeError(key) from err +AttributeError: followed_id + +2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler in discover nach 16.89ms - Exception: AttributeError: 'AppenderQuery' object has no attribute 'contains' +Traceback (most recent call last): + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2800, in discover + ~current_user.following.contains(User.id) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AttributeError: 'AppenderQuery' object has no attribute 'contains' + +2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler 500: 'AppenderQuery' object has no attribute 'contains' +Endpoint: /discover, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view + return current_app.ensure_sync(func)(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2800, in discover + ~current_user.following.contains(User.id) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AttributeError: 'AppenderQuery' object has no attribute 'contains' + +2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler in social_feed nach 54.92ms - Exception: OperationalError: (sqlite3.OperationalError) near "UNION": syntax error +[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id +FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC + LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC + LIMIT ? OFFSET ?] +[parameters: (1, 100, 0, 1, 10, 0)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context + self.dialect.do_execute( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute + cursor.execute(statement, parameters) +sqlite3.OperationalError: near "UNION": syntax error + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2782, in social_feed + posts = all_posts.paginate( + ^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate + return QueryPagination( + ^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__ + items = self._query_items() + ^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items + out = query.limit(self.per_page).offset(self._query_offset).all() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all + return self._iter().all() # type: ignore + ^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter + result: Union[ScalarResult[_T], Result[_T]] = self.session.execute( + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute + return self._execute_internal( + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal + result: Result[Any] = compile_state_cls.orm_execute_statement( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement + result = conn.execute( + ^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute + return meth( + ^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection + return connection._execute_clauseelement( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement + ret = self._execute_context( + ^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context + return self._exec_single_context( + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context + self._handle_dbapi_exception( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception + raise sqlalchemy_exception.with_traceback(exc_info[2]) from e + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context + self.dialect.do_execute( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute + cursor.execute(statement, parameters) +sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error +[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id +FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC + LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC + LIMIT ? OFFSET ?] +[parameters: (1, 100, 0, 1, 10, 0)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) + +2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler 500: (sqlite3.OperationalError) near "UNION": syntax error +[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id +FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC + LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC + LIMIT ? OFFSET ?] +[parameters: (1, 100, 0, 1, 10, 0)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +Endpoint: /feed, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context + self.dialect.do_execute( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute + cursor.execute(statement, parameters) +sqlite3.OperationalError: near "UNION": syntax error + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view + return current_app.ensure_sync(func)(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/utils/logger.py", line 586, in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 2782, in social_feed + posts = all_posts.paginate( + ^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate + return QueryPagination( + ^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__ + items = self._query_items() + ^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items + out = query.limit(self.per_page).offset(self._query_offset).all() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all + return self._iter().all() # type: ignore + ^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter + result: Union[ScalarResult[_T], Result[_T]] = self.session.execute( + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute + return self._execute_internal( + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal + result: Result[Any] = compile_state_cls.orm_execute_statement( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement + result = conn.execute( + ^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute + return meth( + ^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection + return connection._execute_clauseelement( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement + ret = self._execute_context( + ^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context + return self._exec_single_context( + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context + self._handle_dbapi_exception( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception + raise sqlalchemy_exception.with_traceback(exc_info[2]) from e + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context + self.dialect.do_execute( + File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute + cursor.execute(statement, parameters) +sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error +[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id +FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC + LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id +FROM social_post +WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC + LIMIT ? OFFSET ?] +[parameters: (1, 100, 0, 1, 10, 0)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) + +2025-05-28 21:48:48 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +Endpoint: /sw.js, Method: GET, IP: 127.0.0.1 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request + self.raise_routing_exception(req) + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception + raise request.routing_exception # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request + result = self.url_adapter.match(return_rule=True) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match + raise NotFound() from None +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +2025-05-28 21:48:54 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +Endpoint: /static/fonts/inter-regular.woff2, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 257, in + view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 305, in send_static_file + return send_from_directory( + ^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/helpers.py", line 554, in send_from_directory + return werkzeug.utils.send_from_directory( # type: ignore[return-value] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/utils.py", line 574, in send_from_directory + raise NotFound() +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +2025-05-28 21:49:17 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +Endpoint: /sw.js, Method: GET, IP: 127.0.0.1 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request + self.raise_routing_exception(req) + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception + raise request.routing_exception # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request + result = self.url_adapter.match(return_rule=True) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match + raise NotFound() from None +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/dev/website/app.py", line 424, in wrapper + return func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 3141, in discover_users + not_following_subquery = db.session.query(follows.c.followed_id).filter( + ^^^^^^^ +NameError: name 'follows' is not defined + +2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/dev/website/app.py", line 424, in wrapper + return func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 3141, in discover_users + not_following_subquery = db.session.query(follows.c.followed_id).filter( + ^^^^^^^ +NameError: name 'follows' is not defined + +2025-05-28 21:56:25 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +Endpoint: /auth/login, Method: GET, IP: 127.0.0.1 +Nicht angemeldet +Traceback (most recent call last): + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request + self.raise_routing_exception(req) + File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception + raise request.routing_exception # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request + result = self.url_adapter.match(return_rule=True) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match + raise NotFound() from None +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. + +2025-05-28 21:56:41 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/dev/website/app.py", line 424, in wrapper + return func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 3141, in discover_users + not_following_subquery = db.session.query(follows.c.followed_id).filter( + ^^^^^^^ +NameError: name 'follows' is not defined + +2025-05-28 21:57:25 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/dev/website/app.py", line 424, in wrapper + return func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 3141, in discover_users + not_following_subquery = db.session.query(user_follows.c.followed_id).filter( + ^^^^^^^ +NameError: name 'follows' is not defined + +2025-05-28 21:58:02 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined +Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1 +User: 1 (admin) +Traceback (most recent call last): + File "/home/core/dev/website/app.py", line 424, in wrapper + return func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ + File "/home/core/dev/website/app.py", line 3141, in discover_users + users = User.query.filter( + +NameError: name 'follows' is not defined + diff --git a/models.py b/models.py index ca8573a..d400e94 100644 --- a/models.py +++ b/models.py @@ -45,6 +45,35 @@ user_thought_bookmark = db.Table('user_thought_bookmark', db.Column('created_at', db.DateTime, default=datetime.utcnow) ) +# Beziehungstabelle für Benutzer-Freundschaften +user_friendships = db.Table('user_friendships', + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('friend_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('created_at', db.DateTime, default=datetime.utcnow), + db.Column('status', db.String(20), default='pending') # pending, accepted, blocked +) + +# Beziehungstabelle für Benutzer-Follows +user_follows = db.Table('user_follows', + db.Column('follower_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('followed_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('created_at', db.DateTime, default=datetime.utcnow) +) + +# Beziehungstabelle für Post-Likes +post_likes = db.Table('post_likes', + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('post_id', db.Integer, db.ForeignKey('social_post.id'), primary_key=True), + db.Column('created_at', db.DateTime, default=datetime.utcnow) +) + +# Beziehungstabelle für Comment-Likes +comment_likes = db.Table('comment_likes', + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('comment_id', db.Integer, db.ForeignKey('social_comment.id'), primary_key=True), + db.Column('created_at', db.DateTime, default=datetime.utcnow) +) + class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) @@ -59,7 +88,20 @@ class User(db.Model, UserMixin): avatar = db.Column(db.String(200), nullable=True) # Profilbild-URL last_login = db.Column(db.DateTime, nullable=True) # Letzter Login - # Relationships + # Social Network Felder + display_name = db.Column(db.String(100), nullable=True) # Anzeigename + birth_date = db.Column(db.Date, nullable=True) # Geburtsdatum + gender = db.Column(db.String(20), nullable=True) # Geschlecht + phone = db.Column(db.String(20), nullable=True) # Telefonnummer + is_verified = db.Column(db.Boolean, default=False) # Verifizierter Account + is_private = db.Column(db.Boolean, default=False) # Privater Account + follower_count = db.Column(db.Integer, default=0) # Follower-Anzahl + following_count = db.Column(db.Integer, default=0) # Following-Anzahl + post_count = db.Column(db.Integer, default=0) # Post-Anzahl + online_status = db.Column(db.String(20), default='offline') # online, offline, away + last_seen = db.Column(db.DateTime, nullable=True) # Zuletzt gesehen + + # Beziehungen threads = db.relationship('Thread', backref='creator', lazy=True) messages = db.relationship('Message', backref='author', lazy=True) projects = db.relationship('Project', backref='owner', lazy=True) @@ -68,6 +110,37 @@ class User(db.Model, UserMixin): bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark, lazy='dynamic', backref=db.backref('bookmarked_by', lazy='dynamic')) + # Social Network Beziehungen + posts = db.relationship('SocialPost', backref='author', lazy=True, cascade="all, delete-orphan") + comments = db.relationship('SocialComment', backref='author', lazy=True, cascade="all, delete-orphan") + notifications = db.relationship('Notification', foreign_keys='Notification.user_id', backref='user', lazy=True, cascade="all, delete-orphan") + + # Freundschaften (bidirektional) + friends = db.relationship( + 'User', + secondary=user_friendships, + primaryjoin=id == user_friendships.c.user_id, + secondaryjoin=id == user_friendships.c.friend_id, + backref='friend_of', + lazy='dynamic' + ) + + # Following/Followers + following = db.relationship( + 'User', + secondary=user_follows, + primaryjoin=id == user_follows.c.follower_id, + secondaryjoin=id == user_follows.c.followed_id, + backref=db.backref('followers', lazy='dynamic'), + lazy='dynamic' + ) + + # Liked Posts und Comments + liked_posts = db.relationship('SocialPost', secondary=post_likes, + backref=db.backref('liked_by', lazy='dynamic'), lazy='dynamic') + liked_comments = db.relationship('SocialComment', secondary=comment_likes, + backref=db.backref('liked_by', lazy='dynamic'), lazy='dynamic') + def __repr__(self): return f'' @@ -84,6 +157,45 @@ class User(db.Model, UserMixin): @is_admin.setter def is_admin(self, value): self.role = 'admin' if value else 'user' + + # Social Network Methoden + def follow(self, user): + """Folgt einem anderen Benutzer""" + if not self.is_following(user): + self.following.append(user) + user.follower_count += 1 + user.following_count += 1 + + # Notification erstellen + notification = Notification( + user_id=user.id, + type='follow', + message=f'{self.username} folgt dir jetzt', + related_user_id=self.id + ) + db.session.add(notification) + + def unfollow(self, user): + """Entfolgt einem Benutzer""" + if self.is_following(user): + self.following.remove(user) + user.follower_count -= 1 + user.following_count -= 1 + + def is_following(self, user): + """Prüft ob der Benutzer einem anderen folgt""" + return self.following.filter(user_follows.c.followed_id == user.id).count() > 0 + + def get_feed_posts(self, limit=20): + """Holt Posts für den Feed (von gefolgten Benutzern)""" + # Hole alle User-IDs von Benutzern, denen ich folge + meine eigene + followed_user_ids = [user.id for user in self.following] + all_user_ids = followed_user_ids + [self.id] + + # Hole Posts von diesen Benutzern + return SocialPost.query.filter( + SocialPost.user_id.in_(all_user_ids) + ).order_by(SocialPost.created_at.desc()).limit(limit) class Category(db.Model): """Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap""" @@ -388,4 +500,196 @@ class MindmapShare(db.Model): ) def __repr__(self): - return f'' \ No newline at end of file + return f'' + +class SocialPost(db.Model): + """Posts im Social Network""" + id = db.Column(db.Integer, primary_key=True) + content = db.Column(db.Text, nullable=False) + image_url = db.Column(db.String(500), nullable=True) # Bild-URL + video_url = db.Column(db.String(500), nullable=True) # Video-URL + link_url = db.Column(db.String(500), nullable=True) # Link-URL + link_title = db.Column(db.String(200), nullable=True) # Link-Titel + link_description = db.Column(db.Text, nullable=True) # Link-Beschreibung + post_type = db.Column(db.String(20), default='text') # text, image, video, link, thought_share + visibility = db.Column(db.String(20), default='public') # public, friends, private + is_pinned = db.Column(db.Boolean, default=False) + like_count = db.Column(db.Integer, default=0) + comment_count = db.Column(db.Integer, default=0) + share_count = db.Column(db.Integer, default=0) + view_count = db.Column(db.Integer, default=0) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + # Verknüpfung zu Gedanken (falls der Post einen Gedanken teilt) + shared_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True) + shared_thought = db.relationship('Thought', backref='shared_in_posts') + + # Verknüpfung zu Mindmap-Knoten + shared_node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True) + shared_node = db.relationship('MindMapNode', backref='shared_in_posts') + + # Kommentare zu diesem Post + comments = db.relationship('SocialComment', backref='post', lazy=True, cascade="all, delete-orphan") + + def __repr__(self): + return f'' + + def to_dict(self): + return { + 'id': self.id, + 'content': self.content, + 'post_type': self.post_type, + 'image_url': self.image_url, + 'video_url': self.video_url, + 'link_url': self.link_url, + 'link_title': self.link_title, + 'link_description': self.link_description, + 'visibility': self.visibility, + 'is_pinned': self.is_pinned, + 'like_count': self.like_count, + 'comment_count': self.comment_count, + 'share_count': self.share_count, + 'view_count': self.view_count, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat(), + 'author': { + 'id': self.author.id, + 'username': self.author.username, + 'display_name': self.author.display_name or self.author.username, + 'avatar': self.author.avatar, + 'is_verified': self.author.is_verified + }, + 'shared_thought': self.shared_thought.to_dict() if self.shared_thought else None, + 'shared_node': self.shared_node.to_dict() if self.shared_node else None + } + +class SocialComment(db.Model): + """Kommentare zu Posts""" + id = db.Column(db.Integer, primary_key=True) + content = db.Column(db.Text, nullable=False) + like_count = db.Column(db.Integer, default=0) + reply_count = db.Column(db.Integer, default=0) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=False) + parent_id = db.Column(db.Integer, db.ForeignKey('social_comment.id'), nullable=True) + + # Antworten auf diesen Kommentar + replies = db.relationship('SocialComment', backref=db.backref('parent', remote_side=[id]), lazy=True) + + def __repr__(self): + return f'' + + def to_dict(self): + return { + 'id': self.id, + 'content': self.content, + 'like_count': self.like_count, + 'reply_count': self.reply_count, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat(), + 'author': { + 'id': self.author.id, + 'username': self.author.username, + 'display_name': self.author.display_name or self.author.username, + 'avatar': self.author.avatar, + 'is_verified': self.author.is_verified + }, + 'parent_id': self.parent_id + } + +class Notification(db.Model): + """Benachrichtigungen für Benutzer""" + id = db.Column(db.Integer, primary_key=True) + type = db.Column(db.String(50), nullable=False) # follow, like, comment, mention, friend_request, etc. + message = db.Column(db.String(500), nullable=False) + is_read = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + # Verknüpfungen zu anderen Entitäten + related_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) + related_post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=True) + related_comment_id = db.Column(db.Integer, db.ForeignKey('social_comment.id'), nullable=True) + related_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True) + + # Beziehungen + related_user = db.relationship('User', foreign_keys=[related_user_id]) + related_post = db.relationship('SocialPost', foreign_keys=[related_post_id]) + related_comment = db.relationship('SocialComment', foreign_keys=[related_comment_id]) + related_thought = db.relationship('Thought', foreign_keys=[related_thought_id]) + + def __repr__(self): + return f'' + + def to_dict(self): + return { + 'id': self.id, + 'type': self.type, + 'message': self.message, + 'is_read': self.is_read, + 'created_at': self.created_at.isoformat(), + 'related_user': self.related_user.username if self.related_user else None, + 'related_post_id': self.related_post_id, + 'related_comment_id': self.related_comment_id, + 'related_thought_id': self.related_thought_id + } + +class UserSettings(db.Model): + """Benutzereinstellungen""" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, unique=True) + + # Datenschutz-Einstellungen + profile_visibility = db.Column(db.String(20), default='public') # public, friends, private + show_email = db.Column(db.Boolean, default=False) + show_birth_date = db.Column(db.Boolean, default=False) + show_location = db.Column(db.Boolean, default=True) + allow_friend_requests = db.Column(db.Boolean, default=True) + allow_messages = db.Column(db.String(20), default='everyone') # everyone, friends, none + + # Benachrichtigungs-Einstellungen + email_notifications = db.Column(db.Boolean, default=True) + push_notifications = db.Column(db.Boolean, default=True) + notify_on_follow = db.Column(db.Boolean, default=True) + notify_on_like = db.Column(db.Boolean, default=True) + notify_on_comment = db.Column(db.Boolean, default=True) + notify_on_mention = db.Column(db.Boolean, default=True) + notify_on_friend_request = db.Column(db.Boolean, default=True) + + # Interface-Einstellungen + dark_mode = db.Column(db.Boolean, default=False) + language = db.Column(db.String(10), default='de') + + # Beziehung + user = db.relationship('User', backref=db.backref('settings', uselist=False)) + + def __repr__(self): + return f'' + +class Activity(db.Model): + """Aktivitätsprotokoll für Benutzer""" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + action = db.Column(db.String(100), nullable=False) # login, logout, post_created, thought_shared, etc. + description = db.Column(db.String(500), nullable=True) + ip_address = db.Column(db.String(45), nullable=True) # IPv4/IPv6 + user_agent = db.Column(db.String(500), nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + # Verknüpfungen zu anderen Entitäten + related_post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=True) + related_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True) + related_mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=True) + + # Beziehungen + user = db.relationship('User', backref='activities') + related_post = db.relationship('SocialPost') + related_thought = db.relationship('Thought') + related_mindmap = db.relationship('UserMindmap') + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/server.log b/server.log new file mode 100644 index 0000000..1857af3 --- /dev/null +++ b/server.log @@ -0,0 +1,22 @@ +⏰ 21:58:48.486 │ ✅ INFO  │ ⚙️ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet +⏰ 21:58:48.486 │ ✅ INFO  │ ⚙️ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +⏰ 21:58:49.951 │ ✅ INFO  │ ⚙️ [SYSTEM ] │ 📝 OpenAI API-Verbindung erfolgreich hergestellt +⏰ 21:58:50.122 │ ✅ INFO  │ 🗄️ [DB ] │ 🚫 Datenbank erfolgreich initialisiert +⏰ 21:58:50.132 │ ✅ INFO  │ 🗄️ [DB ] │ 🚫 Datenbanktabellen erstellt/aktualisiert +⏰ 21:58:50.134 │ ✅ INFO  │ ⚙️ [SYSTEM ] │ 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000 + * Serving Flask app 'app' + * Debug mode: on +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://127.0.0.1:5000 +Press CTRL+C to quit + * Restarting with watchdog (inotify) +⏰ 21:58:52.225 │ ✅ INFO  │ ⚙️ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet +⏰ 21:58:52.226 │ ✅ INFO  │ ⚙️ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000 +⏰ 21:58:53.848 │ ✅ INFO  │ ⚙️ [SYSTEM ] │ 📝 OpenAI API-Verbindung erfolgreich hergestellt +⏰ 21:58:53.997 │ ✅ INFO  │ 🗄️ [DB ] │ 🚫 Datenbank erfolgreich initialisiert +⏰ 21:58:54.002 │ ✅ INFO  │ 🗄️ [DB ] │ 🚫 Datenbanktabellen erstellt/aktualisiert +⏰ 21:58:54.006 │ ✅ INFO  │ ⚙️ [SYSTEM ] │ 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000 + * Debugger is active! + * Debugger PIN: 114-005-893 diff --git a/static/css/social.css b/static/css/social.css new file mode 100644 index 0000000..4d41c6f --- /dev/null +++ b/static/css/social.css @@ -0,0 +1,915 @@ +/* ================================ + SysTades Social Network Styles + ================================ */ + +:root { + /* Primary Colors */ + --primary-50: #f0f9ff; + --primary-100: #e0f2fe; + --primary-200: #bae6fd; + --primary-300: #7dd3fc; + --primary-400: #38bdf8; + --primary-500: #0ea5e9; + --primary-600: #0284c7; + --primary-700: #0369a1; + --primary-800: #075985; + --primary-900: #0c4a6e; + + /* Neutral Colors */ + --gray-50: #f9fafb; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-300: #d1d5db; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-600: #4b5563; + --gray-700: #374151; + --gray-800: #1f2937; + --gray-900: #111827; + + /* Semantic Colors */ + --success: #10b981; + --warning: #f59e0b; + --error: #ef4444; + --info: #3b82f6; + + /* Social Media Colors */ + --like-color: #ec4899; + --share-color: #8b5cf6; + --bookmark-color: #f59e0b; + --comment-color: var(--primary-500); + + /* Glassmorphism */ + --glass-bg: rgba(255, 255, 255, 0.1); + --glass-border: rgba(255, 255, 255, 0.2); + --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); + + /* Animations */ + --transition-fast: 0.15s ease-out; + --transition-normal: 0.3s ease-out; + --transition-slow: 0.6s ease-out; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + + /* Border Radius */ + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; + --radius-2xl: 1.5rem; +} + +/* Dark Mode Variables */ +[data-theme="dark"] { + --glass-bg: rgba(0, 0, 0, 0.1); + --glass-border: rgba(255, 255, 255, 0.1); + --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); +} + +/* ================================ + Performance Optimizations + ================================ */ + +* { + box-sizing: border-box; +} + +img { + max-width: 100%; + height: auto; +} + +/* GPU Acceleration for animations */ +.accelerated { + transform: translateZ(0); + will-change: transform; +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* ================================ + Social Feed Styles + ================================ */ + +.social-feed { + max-width: 600px; + margin: 0 auto; + padding: 1rem; +} + +.post-card { + background: var(--glass-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-xl); + margin-bottom: 1.5rem; + padding: 1.5rem; + box-shadow: var(--shadow-lg); + transition: all var(--transition-normal); + position: relative; + overflow: hidden; +} + +.post-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-xl); + border-color: var(--primary-300); +} + +.post-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--primary-500), var(--primary-600)); + opacity: 0; + transition: opacity var(--transition-normal); +} + +.post-card:hover::before { + opacity: 1; +} + +/* Post Header */ +.post-header { + display: flex; + align-items: center; + margin-bottom: 1rem; + gap: 0.75rem; +} + +.post-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + border: 2px solid var(--primary-500); + transition: all var(--transition-fast); + cursor: pointer; +} + +.post-avatar:hover { + transform: scale(1.1); + border-color: var(--primary-400); +} + +.post-author { + flex: 1; +} + +.post-author-name { + font-weight: 600; + color: var(--gray-800); + margin: 0; + font-size: 1rem; +} + +.post-author-username { + color: var(--gray-500); + font-size: 0.875rem; + margin: 0; +} + +.post-time { + color: var(--gray-400); + font-size: 0.875rem; +} + +/* Post Content */ +.post-content { + margin-bottom: 1rem; + line-height: 1.6; + color: var(--gray-700); +} + +.post-type-badge { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: var(--radius-md); + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.5rem; +} + +.post-type-text { background: var(--gray-100); color: var(--gray-600); } +.post-type-thought { background: var(--primary-100); color: var(--primary-600); } +.post-type-question { background: var(--warning); color: white; } +.post-type-insight { background: var(--success); color: white; } + +/* Post Actions */ +.post-actions { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 1rem; + border-top: 1px solid var(--gray-200); +} + +.action-group { + display: flex; + gap: 1rem; +} + +.action-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border: none; + background: transparent; + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + font-size: 0.875rem; + color: var(--gray-500); +} + +.action-btn:hover { + background: var(--gray-100); + color: var(--gray-700); + transform: translateY(-1px); +} + +.action-btn.active { + color: var(--primary-600); + background: var(--primary-50); +} + +.action-btn i { + font-size: 1rem; +} + +/* Specific action colors */ +.action-btn.like-btn.active { + color: var(--like-color); + background: rgba(236, 72, 153, 0.1); +} + +.action-btn.share-btn:hover { + color: var(--share-color); + background: rgba(139, 92, 246, 0.1); +} + +.action-btn.bookmark-btn.active { + color: var(--bookmark-color); + background: rgba(245, 158, 11, 0.1); +} + +/* ================================ + Comments Section + ================================ */ + +.comments-section { + border-top: 1px solid var(--gray-200); + padding-top: 1rem; + margin-top: 1rem; +} + +.comment-item { + display: flex; + gap: 0.75rem; + margin-bottom: 1rem; + padding: 0.75rem; + border-radius: var(--radius-lg); + background: var(--gray-50); + transition: background var(--transition-fast); +} + +.comment-item:hover { + background: var(--gray-100); +} + +.comment-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + border: 2px solid var(--primary-400); +} + +.comment-content { + flex: 1; +} + +.comment-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.25rem; +} + +.comment-author { + font-weight: 600; + color: var(--gray-800); + font-size: 0.875rem; +} + +.comment-time { + color: var(--gray-400); + font-size: 0.75rem; +} + +.comment-text { + color: var(--gray-700); + font-size: 0.875rem; + line-height: 1.5; + margin-bottom: 0.5rem; +} + +.comment-actions { + display: flex; + gap: 1rem; +} + +.comment-action { + background: none; + border: none; + color: var(--gray-400); + font-size: 0.75rem; + cursor: pointer; + transition: color var(--transition-fast); +} + +.comment-action:hover { + color: var(--primary-500); +} + +/* Comment Form */ +.comment-form { + display: flex; + gap: 0.75rem; + margin-top: 1rem; +} + +.comment-form textarea { + flex: 1; + padding: 0.75rem; + border: 1px solid var(--gray-300); + border-radius: var(--radius-lg); + resize: none; + min-height: 80px; + transition: border-color var(--transition-fast); +} + +.comment-form textarea:focus { + outline: none; + border-color: var(--primary-500); + box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1); +} + +.comment-submit { + padding: 0.75rem 1.5rem; + background: var(--primary-500); + color: white; + border: none; + border-radius: var(--radius-lg); + cursor: pointer; + transition: all var(--transition-fast); + font-weight: 500; +} + +.comment-submit:hover { + background: var(--primary-600); + transform: translateY(-1px); +} + +/* ================================ + Create Post Form + ================================ */ + +.create-post-form { + background: var(--glass-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-xl); + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: var(--shadow-md); +} + +.create-post-textarea { + width: 100%; + min-height: 120px; + padding: 1rem; + border: 1px solid var(--gray-300); + border-radius: var(--radius-lg); + resize: vertical; + font-family: inherit; + font-size: 1rem; + line-height: 1.5; + transition: all var(--transition-normal); +} + +.create-post-textarea:focus { + outline: none; + border-color: var(--primary-500); + box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1); +} + +.create-post-options { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1rem; + gap: 1rem; +} + +.post-type-select, +.post-visibility-select { + padding: 0.5rem 1rem; + border: 1px solid var(--gray-300); + border-radius: var(--radius-md); + background: white; + cursor: pointer; + transition: border-color var(--transition-fast); +} + +.post-type-select:focus, +.post-visibility-select:focus { + outline: none; + border-color: var(--primary-500); +} + +.create-post-btn { + padding: 0.75rem 2rem; + background: linear-gradient(135deg, var(--primary-500), var(--primary-600)); + color: white; + border: none; + border-radius: var(--radius-lg); + cursor: pointer; + font-weight: 600; + transition: all var(--transition-normal); + box-shadow: var(--shadow-md); +} + +.create-post-btn:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.create-post-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* ================================ + Filter Tabs + ================================ */ + +.feed-filters { + display: flex; + gap: 0.5rem; + margin-bottom: 2rem; + padding: 0.5rem; + background: var(--gray-100); + border-radius: var(--radius-xl); + overflow-x: auto; +} + +.filter-tab { + padding: 0.75rem 1.5rem; + border: none; + background: transparent; + border-radius: var(--radius-lg); + cursor: pointer; + transition: all var(--transition-fast); + font-weight: 500; + white-space: nowrap; + color: var(--gray-600); +} + +.filter-tab:hover { + background: var(--gray-200); + color: var(--gray-800); +} + +.filter-tab.active { + background: var(--primary-500); + color: white; + box-shadow: var(--shadow-md); +} + +/* ================================ + Notifications + ================================ */ + +.notification-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + border-radius: var(--radius-lg); + transition: all var(--transition-fast); + cursor: pointer; + position: relative; +} + +.notification-item:hover { + background: var(--gray-50); + transform: translateX(4px); +} + +.notification-item.unread { + background: var(--primary-50); + border-left: 4px solid var(--primary-500); +} + +.notification-icon { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + color: white; +} + +.notification-like { background: var(--like-color); } +.notification-comment { background: var(--comment-color); } +.notification-follow { background: var(--success); } +.notification-share { background: var(--share-color); } + +.notification-content { + flex: 1; +} + +.notification-text { + margin: 0 0 0.25rem 0; + color: var(--gray-800); + font-size: 0.875rem; +} + +.notification-time { + color: var(--gray-500); + font-size: 0.75rem; +} + +.notification-actions { + display: flex; + gap: 0.5rem; +} + +.notification-delete { + background: none; + border: none; + color: var(--gray-400); + cursor: pointer; + padding: 0.5rem; + border-radius: var(--radius-md); + transition: all var(--transition-fast); +} + +.notification-delete:hover { + background: var(--error); + color: white; +} + +/* ================================ + User Profile + ================================ */ + +.profile-header { + background: linear-gradient(135deg, var(--primary-500), var(--primary-600)); + color: white; + padding: 2rem; + border-radius: var(--radius-2xl); + margin-bottom: 2rem; + position: relative; + overflow: hidden; +} + +.profile-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.3; +} + +.profile-info { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 1.5rem; +} + +.profile-avatar { + width: 100px; + height: 100px; + border-radius: 50%; + border: 4px solid rgba(255, 255, 255, 0.3); + box-shadow: var(--shadow-xl); +} + +.profile-details h1 { + margin: 0 0 0.5rem 0; + font-size: 2rem; + font-weight: 700; +} + +.profile-username { + opacity: 0.9; + font-size: 1.1rem; + margin-bottom: 0.5rem; +} + +.profile-bio { + opacity: 0.8; + line-height: 1.5; + max-width: 500px; +} + +.profile-stats { + display: flex; + gap: 2rem; + margin-top: 1.5rem; +} + +.stat-item { + text-align: center; +} + +.stat-number { + display: block; + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 0.25rem; +} + +.stat-label { + font-size: 0.875rem; + opacity: 0.8; +} + +.follow-btn { + background: rgba(255, 255, 255, 0.2); + border: 2px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 0.75rem 2rem; + border-radius: var(--radius-lg); + font-weight: 600; + cursor: pointer; + transition: all var(--transition-normal); + backdrop-filter: blur(10px); +} + +.follow-btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); +} + +.follow-btn.following { + background: rgba(255, 255, 255, 0.9); + color: var(--primary-600); +} + +/* Profile Tabs */ +.profile-tabs { + display: flex; + border-bottom: 1px solid var(--gray-200); + margin-bottom: 2rem; + overflow-x: auto; +} + +.profile-tab { + padding: 1rem 2rem; + border: none; + background: none; + cursor: pointer; + position: relative; + color: var(--gray-600); + font-weight: 500; + transition: all var(--transition-fast); + white-space: nowrap; +} + +.profile-tab:hover { + color: var(--primary-600); +} + +.profile-tab.active { + color: var(--primary-600); +} + +.profile-tab.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: var(--primary-500); + border-radius: 2px 2px 0 0; +} + +/* ================================ + Responsive Design + ================================ */ + +@media (max-width: 768px) { + .social-feed { + padding: 0.5rem; + } + + .post-card { + padding: 1rem; + margin-bottom: 1rem; + } + + .post-actions { + flex-direction: column; + gap: 1rem; + align-items: stretch; + } + + .action-group { + justify-content: space-around; + } + + .create-post-options { + flex-direction: column; + align-items: stretch; + gap: 0.75rem; + } + + .feed-filters { + padding: 0.25rem; + gap: 0.25rem; + } + + .filter-tab { + padding: 0.5rem 1rem; + font-size: 0.875rem; + } + + .profile-header { + padding: 1.5rem; + } + + .profile-info { + flex-direction: column; + text-align: center; + gap: 1rem; + } + + .profile-avatar { + width: 80px; + height: 80px; + } + + .profile-details h1 { + font-size: 1.5rem; + } + + .profile-stats { + justify-content: center; + gap: 1.5rem; + } + + .profile-tabs { + gap: 0; + } + + .profile-tab { + flex: 1; + padding: 0.75rem 1rem; + text-align: center; + font-size: 0.875rem; + } +} + +/* ================================ + Loading & Animations + ================================ */ + +.loading-spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 2px solid var(--gray-300); + border-radius: 50%; + border-top-color: var(--primary-500); + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.fade-in { + animation: fadeIn 0.5s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.slide-up { + animation: slideUp 0.3s ease-out; +} + +@keyframes slideUp { + from { transform: translateY(100%); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +.pulse { + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* ================================ + Toast Notifications + ================================ */ + +.toast-container { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 1000; + pointer-events: none; +} + +.toast { + background: var(--glass-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + padding: 1rem 1.5rem; + margin-bottom: 0.5rem; + box-shadow: var(--shadow-lg); + pointer-events: all; + max-width: 400px; + animation: slideInRight 0.3s ease-out; +} + +.toast.success { + border-left: 4px solid var(--success); +} + +.toast.error { + border-left: 4px solid var(--error); +} + +.toast.warning { + border-left: 4px solid var(--warning); +} + +.toast.info { + border-left: 4px solid var(--info); +} + +@keyframes slideInRight { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +/* ================================ + Utilities + ================================ */ + +.text-gradient { + background: linear-gradient(135deg, var(--primary-500), var(--primary-600)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.glass-effect { + background: var(--glass-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); +} + +.shadow-glow { + box-shadow: 0 0 20px rgba(14, 165, 233, 0.3); +} + +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.scrollbar-hide::-webkit-scrollbar { + display: none; +} \ No newline at end of file diff --git a/static/js/social.js b/static/js/social.js new file mode 100644 index 0000000..7792626 --- /dev/null +++ b/static/js/social.js @@ -0,0 +1,1133 @@ +/** + * SysTades Social Network JavaScript + * Modernes, performantes JavaScript für Social Features + */ + +// ============================================================================= +// UTILITIES & HELPERS +// ============================================================================= + +class Utils { + static debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func.apply(this, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + static throttle(func, limit) { + let inThrottle; + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + } + + static formatTimeAgo(dateString) { + const now = new Date(); + const date = new Date(dateString); + const seconds = Math.floor((now - date) / 1000); + + const intervals = { + Jahr: 31536000, + Monat: 2592000, + Woche: 604800, + Tag: 86400, + Stunde: 3600, + Minute: 60 + }; + + for (const [unit, secondsInUnit] of Object.entries(intervals)) { + const interval = Math.floor(seconds / secondsInUnit); + if (interval >= 1) { + return `vor ${interval} ${unit}${interval !== 1 ? (unit === 'Monat' ? 'en' : unit === 'Jahr' ? 'en' : 'n') : ''}`; + } + } + return 'gerade eben'; + } + + static escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + static showToast(message, type = 'info', duration = 3000) { + const toastContainer = document.querySelector('.toast-container') || this.createToastContainer(); + const toast = document.createElement('div'); + toast.className = `toast ${type} fade-in`; + toast.innerHTML = ` +
+ + ${this.escapeHtml(message)} +
+ `; + + toastContainer.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + setTimeout(() => toast.remove(), 300); + }, duration); + } + + static createToastContainer() { + const container = document.createElement('div'); + container.className = 'toast-container'; + document.body.appendChild(container); + return container; + } + + static getToastIcon(type) { + const icons = { + success: 'check-circle', + error: 'exclamation-circle', + warning: 'exclamation-triangle', + info: 'info-circle' + }; + return icons[type] || 'info-circle'; + } + + static async fetchAPI(url, options = {}) { + try { + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + ...options.headers + }, + ...options + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('API Error:', error); + throw error; + } + } + + static animateCount(element, targetValue, duration = 1000) { + const startValue = parseInt(element.textContent) || 0; + const difference = targetValue - startValue; + const increment = difference / (duration / 16); + let currentValue = startValue; + + const updateCount = () => { + currentValue += increment; + if ((increment > 0 && currentValue >= targetValue) || + (increment < 0 && currentValue <= targetValue)) { + element.textContent = targetValue; + return; + } + element.textContent = Math.floor(currentValue); + requestAnimationFrame(updateCount); + }; + + requestAnimationFrame(updateCount); + } + + static createLoadingSpinner() { + const spinner = document.createElement('div'); + spinner.className = 'loading-spinner'; + return spinner; + } +} + +// ============================================================================= +// SOCIAL FEED CLASS +// ============================================================================= + +class SocialFeed { + constructor() { + this.currentFilter = 'public'; + this.isLoading = false; + this.page = 1; + this.hasMore = true; + this.posts = new Map(); + + this.init(); + } + + init() { + this.bindEvents(); + this.loadPosts(); + this.setupIntersectionObserver(); + } + + bindEvents() { + // Create post form + const createForm = document.getElementById('create-post-form'); + if (createForm) { + createForm.addEventListener('submit', this.handleCreatePost.bind(this)); + } + + // Filter tabs + document.querySelectorAll('.filter-tab').forEach(tab => { + tab.addEventListener('click', this.handleFilterChange.bind(this)); + }); + + // Global event delegation for post actions + document.addEventListener('click', this.handlePostAction.bind(this)); + document.addEventListener('submit', this.handleCommentSubmit.bind(this)); + } + + setupIntersectionObserver() { + const loadMoreTrigger = document.getElementById('load-more-trigger'); + if (!loadMoreTrigger) return; + + const observer = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && !this.isLoading && this.hasMore) { + this.loadMorePosts(); + } + }, { threshold: 0.1 }); + + observer.observe(loadMoreTrigger); + } + + async handleCreatePost(event) { + event.preventDefault(); + + const form = event.target; + const formData = new FormData(form); + const content = formData.get('content')?.trim(); + + if (!content) { + Utils.showToast('Bitte geben Sie Inhalt für den Post ein', 'warning'); + return; + } + + const submitBtn = form.querySelector('.create-post-btn'); + const originalText = submitBtn.textContent; + submitBtn.disabled = true; + submitBtn.innerHTML = 'Wird erstellt...'; + + try { + const postData = { + content: content, + post_type: formData.get('post_type') || 'text', + visibility: formData.get('visibility') || 'public' + }; + + const response = await Utils.fetchAPI('/api/social/posts', { + method: 'POST', + body: JSON.stringify(postData) + }); + + if (response.success) { + Utils.showToast('Post erfolgreich erstellt!', 'success'); + form.reset(); + this.refreshFeed(); + } else { + throw new Error(response.error || 'Fehler beim Erstellen des Posts'); + } + } catch (error) { + Utils.showToast(error.message, 'error'); + } finally { + submitBtn.disabled = false; + submitBtn.textContent = originalText; + } + } + + handleFilterChange(event) { + const newFilter = event.target.dataset.filter; + if (newFilter === this.currentFilter) return; + + // Update UI + document.querySelectorAll('.filter-tab').forEach(tab => { + tab.classList.remove('active'); + }); + event.target.classList.add('active'); + + // Reset pagination and reload + this.currentFilter = newFilter; + this.page = 1; + this.hasMore = true; + this.posts.clear(); + + const postsContainer = document.getElementById('posts-container'); + if (postsContainer) { + postsContainer.innerHTML = ''; + } + + this.loadPosts(); + } + + async loadPosts() { + if (this.isLoading) return; + this.isLoading = true; + + try { + const params = new URLSearchParams({ + filter: this.currentFilter, + page: this.page, + per_page: 10 + }); + + const response = await Utils.fetchAPI(`/api/social/posts?${params}`); + + if (response.success) { + this.renderPosts(response.posts); + this.hasMore = response.has_more; + this.page++; + } else { + throw new Error(response.error || 'Fehler beim Laden der Posts'); + } + } catch (error) { + Utils.showToast('Fehler beim Laden der Posts', 'error'); + console.error('Load posts error:', error); + } finally { + this.isLoading = false; + } + } + + async loadMorePosts() { + await this.loadPosts(); + } + + renderPosts(posts) { + const container = document.getElementById('posts-container'); + if (!container) return; + + const fragment = document.createDocumentFragment(); + + posts.forEach(post => { + if (!this.posts.has(post.id)) { + this.posts.set(post.id, post); + const postElement = this.createPostElement(post); + fragment.appendChild(postElement); + } + }); + + container.appendChild(fragment); + + // Animate new posts + const newPosts = container.querySelectorAll('.post-card:not(.animated)'); + newPosts.forEach((post, index) => { + post.classList.add('animated'); + setTimeout(() => { + post.classList.add('fade-in'); + }, index * 100); + }); + } + + createPostElement(post) { + const postDiv = document.createElement('div'); + postDiv.className = 'post-card'; + postDiv.dataset.postId = post.id; + + postDiv.innerHTML = ` +
+ ${Utils.escapeHtml(post.author.display_name)} + + ${Utils.formatTimeAgo(post.created_at)} +
+ + ${post.post_type !== 'text' ? ` + + ${this.getPostTypeLabel(post.post_type)} + + ` : ''} + +
+ ${this.formatPostContent(post.content)} +
+ +
+
+ + + +
+
+ +
+
+ + + `; + + return postDiv; + } + + getPostTypeLabel(type) { + const labels = { + text: 'Text', + thought: 'Gedanke', + question: 'Frage', + insight: 'Erkenntnis' + }; + return labels[type] || 'Text'; + } + + formatPostContent(content) { + // Basic formatting: links, mentions, hashtags + return Utils.escapeHtml(content) + .replace(/\n/g, '
') + .replace(/(https?:\/\/[^\s]+)/g, '$1') + .replace(/@([a-zA-Z0-9_]+)/g, '@$1') + .replace(/#([a-zA-Z0-9_]+)/g, '#$1'); + } + + async handlePostAction(event) { + const actionBtn = event.target.closest('.action-btn'); + if (!actionBtn) return; + + const action = actionBtn.dataset.action; + const postId = actionBtn.dataset.postId; + + if (!action || !postId) return; + + event.preventDefault(); + + switch (action) { + case 'like': + await this.toggleLike(postId, actionBtn); + break; + case 'comment': + this.toggleComments(postId); + break; + case 'share': + await this.sharePost(postId); + break; + case 'bookmark': + await this.toggleBookmark(postId, actionBtn); + break; + } + } + + async toggleLike(postId, button) { + try { + const response = await Utils.fetchAPI(`/api/social/posts/${postId}/like`, { + method: 'POST' + }); + + if (response.success) { + const isLiked = response.liked; + const likeCount = response.like_count; + + button.classList.toggle('active', isLiked); + + const countElement = button.querySelector('.like-count'); + Utils.animateCount(countElement, likeCount); + + // Add animation effect + const heart = button.querySelector('i'); + heart.classList.add('animate-pulse'); + setTimeout(() => heart.classList.remove('animate-pulse'), 300); + + } else { + throw new Error(response.error); + } + } catch (error) { + Utils.showToast('Fehler beim Liken des Posts', 'error'); + } + } + + toggleComments(postId) { + const commentsSection = document.getElementById(`comments-${postId}`); + if (!commentsSection) return; + + const isVisible = commentsSection.style.display !== 'none'; + + if (isVisible) { + commentsSection.style.display = 'none'; + } else { + commentsSection.style.display = 'block'; + this.loadComments(postId); + } + } + + async loadComments(postId) { + try { + const response = await Utils.fetchAPI(`/api/social/posts/${postId}/comments`); + + if (response.success) { + const commentsList = document.querySelector(`#comments-${postId} .comments-list`); + if (commentsList) { + commentsList.innerHTML = response.comments.map(comment => this.createCommentHTML(comment)).join(''); + } + } + } catch (error) { + Utils.showToast('Fehler beim Laden der Kommentare', 'error'); + } + } + + createCommentHTML(comment) { + return ` +
+ ${Utils.escapeHtml(comment.author.display_name)} +
+
+ ${Utils.escapeHtml(comment.author.display_name)} + ${Utils.formatTimeAgo(comment.created_at)} +
+

${this.formatPostContent(comment.content)}

+
+ + +
+
+
+ `; + } + + async handleCommentSubmit(event) { + if (!event.target.classList.contains('comment-form')) return; + + event.preventDefault(); + + const form = event.target; + const postId = form.dataset.postId; + const textarea = form.querySelector('textarea'); + const content = textarea.value.trim(); + + if (!content) return; + + const submitBtn = form.querySelector('.comment-submit'); + submitBtn.disabled = true; + submitBtn.innerHTML = ''; + + try { + const response = await Utils.fetchAPI(`/api/social/posts/${postId}/comments`, { + method: 'POST', + body: JSON.stringify({ content }) + }); + + if (response.success) { + Utils.showToast('Kommentar hinzugefügt!', 'success'); + textarea.value = ''; + this.loadComments(postId); + + // Update comment count + const commentBtn = document.querySelector(`[data-action="comment"][data-post-id="${postId}"]`); + if (commentBtn) { + const countElement = commentBtn.querySelector('span'); + const currentCount = parseInt(countElement.textContent) || 0; + Utils.animateCount(countElement, currentCount + 1); + } + } else { + throw new Error(response.error); + } + } catch (error) { + Utils.showToast('Fehler beim Hinzufügen des Kommentars', 'error'); + } finally { + submitBtn.disabled = false; + submitBtn.innerHTML = ''; + } + } + + async sharePost(postId) { + try { + const response = await Utils.fetchAPI(`/api/social/posts/${postId}/share`, { + method: 'POST' + }); + + if (response.success) { + Utils.showToast('Post geteilt!', 'success'); + + // Update share count + const shareBtn = document.querySelector(`[data-action="share"][data-post-id="${postId}"]`); + if (shareBtn) { + const countElement = shareBtn.querySelector('span'); + const currentCount = parseInt(countElement.textContent) || 0; + Utils.animateCount(countElement, currentCount + 1); + } + } else { + throw new Error(response.error); + } + } catch (error) { + Utils.showToast('Fehler beim Teilen des Posts', 'error'); + } + } + + async toggleBookmark(postId, button) { + try { + const response = await Utils.fetchAPI(`/api/social/posts/${postId}/bookmark`, { + method: 'POST' + }); + + if (response.success) { + const isBookmarked = response.bookmarked; + button.classList.toggle('active', isBookmarked); + + Utils.showToast( + isBookmarked ? 'Post gespeichert!' : 'Speicherung entfernt!', + 'success' + ); + } else { + throw new Error(response.error); + } + } catch (error) { + Utils.showToast('Fehler beim Speichern des Posts', 'error'); + } + } + + refreshFeed() { + this.page = 1; + this.hasMore = true; + this.posts.clear(); + + const postsContainer = document.getElementById('posts-container'); + if (postsContainer) { + postsContainer.innerHTML = ''; + } + + this.loadPosts(); + } +} + +// ============================================================================= +// NOTIFICATION CENTER CLASS +// ============================================================================= + +class NotificationCenter { + constructor() { + this.currentFilter = 'all'; + this.page = 1; + this.hasMore = true; + this.isLoading = false; + + this.init(); + } + + init() { + this.bindEvents(); + this.loadNotifications(); + this.pollForNewNotifications(); + } + + bindEvents() { + // Filter tabs + document.querySelectorAll('.notification-filter').forEach(filter => { + filter.addEventListener('click', this.handleFilterChange.bind(this)); + }); + + // Mark all as read + const markAllBtn = document.getElementById('mark-all-read'); + if (markAllBtn) { + markAllBtn.addEventListener('click', this.markAllAsRead.bind(this)); + } + + // Event delegation for notification actions + document.addEventListener('click', this.handleNotificationAction.bind(this)); + } + + handleFilterChange(event) { + const newFilter = event.target.dataset.filter; + if (newFilter === this.currentFilter) return; + + // Update UI + document.querySelectorAll('.notification-filter').forEach(filter => { + filter.classList.remove('active'); + }); + event.target.classList.add('active'); + + // Reset and reload + this.currentFilter = newFilter; + this.page = 1; + this.hasMore = true; + + const container = document.getElementById('notifications-container'); + if (container) { + container.innerHTML = ''; + } + + this.loadNotifications(); + } + + async loadNotifications() { + if (this.isLoading) return; + this.isLoading = true; + + try { + const params = new URLSearchParams({ + filter: this.currentFilter, + page: this.page, + per_page: 20 + }); + + const response = await Utils.fetchAPI(`/api/social/notifications?${params}`); + + if (response.success) { + this.renderNotifications(response.notifications); + this.hasMore = response.has_more; + this.page++; + this.updateNotificationBadge(response.unread_count); + } + } catch (error) { + Utils.showToast('Fehler beim Laden der Benachrichtigungen', 'error'); + } finally { + this.isLoading = false; + } + } + + renderNotifications(notifications) { + const container = document.getElementById('notifications-container'); + if (!container) return; + + const fragment = document.createDocumentFragment(); + + notifications.forEach(notification => { + const notificationElement = this.createNotificationElement(notification); + fragment.appendChild(notificationElement); + }); + + container.appendChild(fragment); + } + + createNotificationElement(notification) { + const div = document.createElement('div'); + div.className = `notification-item ${!notification.is_read ? 'unread' : ''}`; + div.dataset.notificationId = notification.id; + + div.innerHTML = ` +
+ +
+
+

${Utils.escapeHtml(notification.message)}

+ ${Utils.formatTimeAgo(notification.created_at)} +
+
+ ${!notification.is_read ? ` + + ` : ''} + +
+ `; + + return div; + } + + getNotificationIcon(type) { + const icons = { + like: 'heart', + comment: 'comment', + follow: 'user-plus', + share: 'share', + mention: 'at' + }; + return icons[type] || 'bell'; + } + + async handleNotificationAction(event) { + const actionBtn = event.target.closest('[data-action]'); + if (!actionBtn) return; + + const action = actionBtn.dataset.action; + const notificationItem = actionBtn.closest('.notification-item'); + const notificationId = notificationItem?.dataset.notificationId; + + if (!notificationId) return; + + event.preventDefault(); + + switch (action) { + case 'mark-read': + await this.markAsRead(notificationId, notificationItem); + break; + case 'delete': + await this.deleteNotification(notificationId, notificationItem); + break; + } + } + + async markAsRead(notificationId, element) { + try { + const response = await Utils.fetchAPI(`/api/social/notifications/${notificationId}/read`, { + method: 'POST' + }); + + if (response.success) { + element.classList.remove('unread'); + element.querySelector('.notification-mark-read')?.remove(); + this.updateNotificationBadge(); + } + } catch (error) { + Utils.showToast('Fehler beim Markieren als gelesen', 'error'); + } + } + + async deleteNotification(notificationId, element) { + try { + const response = await Utils.fetchAPI(`/api/social/notifications/${notificationId}`, { + method: 'DELETE' + }); + + if (response.success) { + element.style.opacity = '0'; + element.style.transform = 'translateX(-100%)'; + setTimeout(() => element.remove(), 300); + this.updateNotificationBadge(); + } + } catch (error) { + Utils.showToast('Fehler beim Löschen der Benachrichtigung', 'error'); + } + } + + async markAllAsRead() { + try { + const response = await Utils.fetchAPI('/api/social/notifications/mark-all-read', { + method: 'POST' + }); + + if (response.success) { + document.querySelectorAll('.notification-item.unread').forEach(item => { + item.classList.remove('unread'); + item.querySelector('.notification-mark-read')?.remove(); + }); + + this.updateNotificationBadge(0); + Utils.showToast('Alle Benachrichtigungen als gelesen markiert', 'success'); + } + } catch (error) { + Utils.showToast('Fehler beim Markieren aller Benachrichtigungen', 'error'); + } + } + + updateNotificationBadge(count) { + const badge = document.querySelector('.notification-badge'); + if (badge) { + if (count === undefined) { + // Count unread notifications + count = document.querySelectorAll('.notification-item.unread').length; + } + + if (count > 0) { + badge.textContent = count > 99 ? '99+' : count; + badge.style.display = 'block'; + } else { + badge.style.display = 'none'; + } + } + } + + pollForNewNotifications() { + setInterval(async () => { + try { + const response = await Utils.fetchAPI('/api/social/notifications?filter=unread&per_page=1'); + if (response.success && response.unread_count !== undefined) { + this.updateNotificationBadge(response.unread_count); + } + } catch (error) { + // Silent fail for polling + } + }, 30000); // Poll every 30 seconds + } +} + +// ============================================================================= +// SEARCH FUNCTIONALITY +// ============================================================================= + +class SearchManager { + constructor() { + this.searchInput = document.getElementById('global-search'); + this.searchResults = document.getElementById('search-results'); + this.isSearching = false; + + this.init(); + } + + init() { + if (!this.searchInput) return; + + this.bindEvents(); + this.setupClickOutside(); + } + + bindEvents() { + this.searchInput.addEventListener('input', + Utils.debounce(this.handleSearch.bind(this), 300) + ); + + this.searchInput.addEventListener('focus', this.showSearchResults.bind(this)); + this.searchInput.addEventListener('keydown', this.handleKeyNavigation.bind(this)); + } + + setupClickOutside() { + document.addEventListener('click', (event) => { + if (!event.target.closest('.search-container')) { + this.hideSearchResults(); + } + }); + } + + async handleSearch(event) { + const query = event.target.value.trim(); + + if (query.length < 2) { + this.hideSearchResults(); + return; + } + + if (this.isSearching) return; + this.isSearching = true; + + try { + this.showLoadingResults(); + + const [usersResponse, postsResponse] = await Promise.all([ + Utils.fetchAPI(`/api/social/users/search?q=${encodeURIComponent(query)}&limit=5`), + Utils.fetchAPI(`/api/social/posts?search=${encodeURIComponent(query)}&per_page=5`) + ]); + + const results = { + users: usersResponse.success ? usersResponse.users : [], + posts: postsResponse.success ? postsResponse.posts : [] + }; + + this.renderSearchResults(results, query); + } catch (error) { + this.showErrorResults(); + } finally { + this.isSearching = false; + } + } + + showSearchResults() { + if (this.searchResults) { + this.searchResults.style.display = 'block'; + } + } + + hideSearchResults() { + if (this.searchResults) { + this.searchResults.style.display = 'none'; + } + } + + showLoadingResults() { + if (!this.searchResults) return; + + this.searchResults.innerHTML = ` +
+
+

Suche läuft...

+
+ `; + this.showSearchResults(); + } + + showErrorResults() { + if (!this.searchResults) return; + + this.searchResults.innerHTML = ` +
+ +

Fehler bei der Suche

+
+ `; + } + + renderSearchResults(results, query) { + if (!this.searchResults) return; + + let html = ''; + + if (results.users.length > 0) { + html += ` +
+

Benutzer

+ ${results.users.map(user => this.createUserResultHTML(user)).join('')} +
+ `; + } + + if (results.posts.length > 0) { + html += ` +
+

Posts

+ ${results.posts.map(post => this.createPostResultHTML(post)).join('')} +
+ `; + } + + if (results.users.length === 0 && results.posts.length === 0) { + html = ` +
+ +

Keine Ergebnisse für "${Utils.escapeHtml(query)}"

+
+ `; + } + + this.searchResults.innerHTML = html; + this.showSearchResults(); + } + + createUserResultHTML(user) { + return ` + + ${Utils.escapeHtml(user.display_name)} +
+
${Utils.escapeHtml(user.display_name)}
+

@${Utils.escapeHtml(user.username)}

+
+
+ `; + } + + createPostResultHTML(post) { + const truncatedContent = post.content.length > 100 + ? post.content.substring(0, 100) + '...' + : post.content; + + return ` +
+
+
${Utils.escapeHtml(post.author.display_name)}
+

${Utils.escapeHtml(truncatedContent)}

+
+
+ `; + } + + handleKeyNavigation(event) { + const results = this.searchResults?.querySelectorAll('.search-result-item'); + if (!results || results.length === 0) return; + + let current = this.searchResults.querySelector('.search-result-item.highlighted'); + let currentIndex = current ? Array.from(results).indexOf(current) : -1; + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + currentIndex = Math.min(currentIndex + 1, results.length - 1); + break; + case 'ArrowUp': + event.preventDefault(); + currentIndex = Math.max(currentIndex - 1, 0); + break; + case 'Enter': + event.preventDefault(); + if (current) current.click(); + return; + case 'Escape': + this.hideSearchResults(); + this.searchInput.blur(); + return; + default: + return; + } + + // Update highlighting + results.forEach(result => result.classList.remove('highlighted')); + if (currentIndex >= 0) { + results[currentIndex].classList.add('highlighted'); + } + } +} + +// ============================================================================= +// INITIALIZATION +// ============================================================================= + +document.addEventListener('DOMContentLoaded', () => { + // Initialize components based on current page + const currentPath = window.location.pathname; + + if (currentPath === '/social/feed' || currentPath === '/') { + new SocialFeed(); + } + + if (currentPath === '/social/notifications') { + new NotificationCenter(); + } + + // Initialize global components + new SearchManager(); + + // Theme toggle functionality + const themeToggle = document.getElementById('theme-toggle'); + if (themeToggle) { + themeToggle.addEventListener('click', () => { + const currentTheme = document.documentElement.getAttribute('data-theme'); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + + document.documentElement.setAttribute('data-theme', newTheme); + localStorage.setItem('theme', newTheme); + + const icon = themeToggle.querySelector('i'); + icon.className = newTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon'; + }); + } + + // Load saved theme + const savedTheme = localStorage.getItem('theme') || 'light'; + document.documentElement.setAttribute('data-theme', savedTheme); + + // Mobile menu toggle + const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); + const mobileMenu = document.getElementById('mobile-menu'); + + if (mobileMenuToggle && mobileMenu) { + mobileMenuToggle.addEventListener('click', () => { + mobileMenu.classList.toggle('hidden'); + }); + } + + // Auto-hide loading states + setTimeout(() => { + document.querySelectorAll('.loading-spinner').forEach(spinner => { + if (spinner.parentElement) { + spinner.parentElement.style.opacity = '0'; + setTimeout(() => spinner.remove(), 300); + } + }); + }, 2000); + + console.log('🚀 SysTades Social Network initialized successfully!'); +}); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index ad88cb0..f571bb3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -307,7 +307,7 @@ .chat-assistant .chat-messages { max-height: calc(80vh - 160px) !important; } - + Mindmap + {% if current_user.is_authenticated %} + + Feed + + + Entdecken + + {% endif %} Mindmap + {% if current_user.is_authenticated %} + + Feed + + + Entdecken + + {% endif %}
- +
- - - +
+ + + +
+ +
+ + + +
+ +
+ + + +
@@ -679,9 +711,9 @@ document.addEventListener('DOMContentLoaded', function() { } // Funktionen für Zoom-Buttons und Reset - const zoomInBtn = document.getElementById('zoomIn'); - const zoomOutBtn = document.getElementById('zoomOut'); - const resetViewBtn = document.getElementById('resetView'); + const zoomInBtn = document.getElementById('zoom-in-btn'); + const zoomOutBtn = document.getElementById('zoom-out-btn'); + const resetViewBtn = document.getElementById('reset-view-btn'); if (zoomInBtn && window.cy) { zoomInBtn.addEventListener('click', function() { @@ -700,6 +732,199 @@ document.addEventListener('DOMContentLoaded', function() { if (window.cy) window.cy.fit(); }); } + + // Neue Toolbar-Funktionen + const addNodeBtn = document.getElementById('add-node-btn'); + const addThoughtBtn = document.getElementById('add-thought-btn'); + const collaborateBtn = document.getElementById('collaborate-btn'); + const exportBtn = document.getElementById('export-btn'); + const shareBtn = document.getElementById('share-btn'); + const fullscreenBtn = document.getElementById('fullscreen-btn'); + + if (addNodeBtn) { + addNodeBtn.addEventListener('click', function() { + // Öffne Modal zum Hinzufügen eines neuen Knotens + showAddNodeModal(); + }); + } + + if (addThoughtBtn) { + addThoughtBtn.addEventListener('click', function() { + // Öffne Modal zum Hinzufügen eines Gedankens + showAddThoughtModal(); + }); + } + + if (collaborateBtn) { + collaborateBtn.addEventListener('click', function() { + // Starte Kollaborationsmodus + startCollaboration(); + }); + } + + if (exportBtn) { + exportBtn.addEventListener('click', function() { + // Exportiere Mindmap + exportMindmap(); + }); + } + + if (shareBtn) { + shareBtn.addEventListener('click', function() { + // Teile Mindmap + shareMindmap(); + }); + } + + if (fullscreenBtn) { + fullscreenBtn.addEventListener('click', function() { + // Vollbild-Modus + toggleFullscreen(); + }); + } + + // Funktionen implementieren + function showAddNodeModal() { + // Erstelle ein einfaches Modal für neuen Knoten + const nodeName = prompt('Name des neuen Knotens:'); + if (nodeName && nodeName.trim()) { + addNewNode(nodeName.trim()); + } + } + + function showAddThoughtModal() { + // Erstelle ein Modal für neuen Gedanken + const thoughtTitle = prompt('Titel des Gedankens:'); + if (thoughtTitle && thoughtTitle.trim()) { + addNewThought(thoughtTitle.trim()); + } + } + + function addNewNode(name) { + // API-Aufruf zum Hinzufügen eines neuen Knotens + fetch('/api/mindmap/public/add_node', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: name, + description: '', + x_position: Math.random() * 400 + 100, + y_position: Math.random() * 400 + 100 + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Lade Mindmap neu + location.reload(); + } else { + alert('Fehler beim Hinzufügen des Knotens: ' + (data.error || 'Unbekannter Fehler')); + } + }) + .catch(error => { + console.error('Fehler:', error); + alert('Ein Fehler ist aufgetreten.'); + }); + } + + function addNewThought(title) { + // API-Aufruf zum Hinzufügen eines neuen Gedankens + fetch('/api/thoughts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title: title, + content: 'Neuer Gedanke erstellt über die Mindmap', + branch: 'Allgemein' + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('Gedanke erfolgreich erstellt!'); + } else { + alert('Fehler beim Erstellen des Gedankens: ' + (data.error || 'Unbekannter Fehler')); + } + }) + .catch(error => { + console.error('Fehler:', error); + alert('Ein Fehler ist aufgetreten.'); + }); + } + + function startCollaboration() { + // Kollaborationsmodus starten + alert('Kollaborationsmodus wird bald verfügbar sein!\n\nGeplante Features:\n- Echtzeit-Bearbeitung\n- Live-Cursor anderer Benutzer\n- Chat-Integration\n- Änderungshistorie'); + } + + function exportMindmap() { + // Mindmap exportieren + const format = prompt('Export-Format wählen:\n1. JSON\n2. PNG (geplant)\n3. PDF (geplant)\n\nGeben Sie 1, 2 oder 3 ein:', '1'); + + if (format === '1') { + // JSON-Export + if (window.cy) { + const data = window.cy.json(); + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'mindmap-export.json'; + a.click(); + URL.revokeObjectURL(url); + } + } else { + alert('Dieses Format wird bald verfügbar sein!'); + } + } + + function shareMindmap() { + // Mindmap teilen + if (navigator.share) { + navigator.share({ + title: 'SysTades Mindmap', + text: 'Schau dir diese interessante Mindmap an!', + url: window.location.href + }); + } else { + // Fallback: URL kopieren + navigator.clipboard.writeText(window.location.href).then(() => { + alert('Mindmap-Link wurde in die Zwischenablage kopiert!'); + }).catch(() => { + prompt('Kopiere diesen Link zum Teilen:', window.location.href); + }); + } + } + + function toggleFullscreen() { + // Vollbild-Modus umschalten + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen().catch(err => { + console.error('Fehler beim Aktivieren des Vollbildmodus:', err); + }); + } else { + document.exitFullscreen(); + } + } + + // Vollbild-Event-Listener + document.addEventListener('fullscreenchange', function() { + const fullscreenBtn = document.getElementById('fullscreen-btn'); + if (fullscreenBtn) { + const icon = fullscreenBtn.querySelector('i'); + if (document.fullscreenElement) { + icon.className = 'fas fa-compress'; + fullscreenBtn.title = 'Vollbild verlassen'; + } else { + icon.className = 'fas fa-expand'; + fullscreenBtn.title = 'Vollbild'; + } + } + }); }); {% endblock %} \ No newline at end of file diff --git a/templates/social/discover.html b/templates/social/discover.html new file mode 100644 index 0000000..8c4b297 --- /dev/null +++ b/templates/social/discover.html @@ -0,0 +1,512 @@ +{% extends "base.html" %} + +{% block title %}Entdecken{% endblock %} + +{% block content %} +
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/social/feed.html b/templates/social/feed.html new file mode 100644 index 0000000..b473eeb --- /dev/null +++ b/templates/social/feed.html @@ -0,0 +1,327 @@ +{% extends "base.html" %} + +{% block title %}Feed{% endblock %} + +{% block content %} +
+ + +
+ + +
+ + +
+
+
+ {{ current_user.username[0].upper() }} +
+
+ +
+ +
+
+ + +
+ +
+ + + +
+ + +
+
+ + +
+ +
+
+ + + + + Lade Posts... +
+
+ + +
+
+ +
+

+ Noch keine Posts +

+

+ Folge anderen Nutzern oder erstelle deinen ersten Post! +

+ + + Entdecken + +
+ + + +
+ + +
+ +
+ + +
+
+ + + + + Lade weitere Posts... +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/social/notifications.html b/templates/social/notifications.html new file mode 100644 index 0000000..e64942e --- /dev/null +++ b/templates/social/notifications.html @@ -0,0 +1,381 @@ +{% extends "base.html" %} +{% block title %}Benachrichtigungen - SysTades{% endblock %} + +{% block content %} +
+ +
+
+

🔔 Benachrichtigungen

+ +
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/social/profile.html b/templates/social/profile.html new file mode 100644 index 0000000..47648a7 --- /dev/null +++ b/templates/social/profile.html @@ -0,0 +1,668 @@ +{% extends "base.html" %} + +{% block title %}{{ user.display_name or user.username }} - SysTades{% endblock %} + +{% block additional_css %} + +{% endblock %} + +{% block content %} +
+ +
+
+
+ {{ user.username[0].upper() }} +
+ +
+

{{ user.display_name or user.username }}

+

@{{ user.username }}

+ {% if user.bio %} +

{{ user.bio }}

+ {% endif %} +
+ +
+
+ {{ user.post_count }} +
Posts
+
+
+ {{ user.follower_count }} +
Follower
+
+
+ {{ user.following_count }} +
Folgt
+
+
+ {{ user.mindmaps|length if user.mindmaps else 0 }} +
Mindmaps
+
+
+ + {% if user != current_user %} +
+ + +
+ {% else %} + + {% endif %} +
+
+ + +
+ +
+ + +
+ +
+
+ {% if posts %} + {% for post in posts %} +
+ +
+ {{ post.content[:300] }}{% if post.content|length > 300 %}...{% endif %} +
+
+
+ + {{ post.like_count }} +
+
+ + {{ post.comment_count }} +
+
+ + {{ post.share_count or 0 }} +
+
+
+ {% endfor %} + {% else %} +
+

Noch keine Posts

+

{% if user == current_user %}Du hast noch keine Posts erstellt.{% else %}{{ user.username }} hat noch keine Posts veröffentlicht.{% endif %}

+ {% if user == current_user %} + + + Ersten Post erstellen + + {% endif %} +
+ {% endif %} +
+
+ + + + + + + + + +
+
+{% endblock %} + +{% block additional_js %} + +{% endblock %} \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py index 359d89d..2be571c 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -29,8 +29,8 @@ __all__ = [ 'delete_user', 'create_admin_user', - # Server management - 'run_development_server', + # Server management (imported separately to avoid circular imports) + # 'run_development_server' - available in utils.server module ] # Import remaining modules that might depend on app @@ -38,4 +38,4 @@ from .db_fix import fix_database_schema from .db_rebuild import rebuild_database from .db_test import test_database_connection, test_models, print_database_stats, run_all_tests from .user_manager import list_users, create_user, reset_password, delete_user, create_admin_user -from .server import run_development_server \ No newline at end of file +# Removed server import to prevent circular import - access via utils.server directly \ No newline at end of file diff --git a/utils/__pycache__/__init__.cpython-311.pyc b/utils/__pycache__/__init__.cpython-311.pyc index 06a0b36a0a1f75945fa10c91d8af6fbefc662969..a2bec1992b0ebbc7605c6f18fbc247746b200e3f 100644 GIT binary patch delta 83 zcmX@h(agcOoR^o20SN9UnP(`nPUMqd%$um5$I21RpvgIL*;Pi~$u*3}CPy*ZGy7?Z jOkT{?G1-FIhD!>lgAs^}w@$8O-ZWW`g^jgH0>}pdY1I?& delta 156 zcmZqXILpDeoR^o20SJ_Bl`kt32T8YRlekir?vpvg6H)>THn z$pwtZ#BPZdmFC5#q?V=THeqt)(qt?W0Gj2eDK@!+se|hl8%QR# js7PS)UnYAl8K4v+5Embx?8dxF_6CF21#GBD3aADE`XnoW diff --git a/utils/__pycache__/db_check.cpython-311.pyc b/utils/__pycache__/db_check.cpython-311.pyc index f594fb58ea46e757b82f7b1b999bcd41db18bfbd..42aff1fc7d82be9bfd16c9154062e6deef401be4 100644 GIT binary patch literal 4530 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 BF>?R_ delta 1829 zcma)6U1(cn7=FL=lbqiqr)k%wX|puh+N9f@W$l_TMq+w4%pD!3^_P(ctuB+IT8p&bEBU~D&D=#6J=gW{F%chdY!9rz{ZInQ~| z^Sl!^pV zw*9nE?WG#m7HU3fzirgj0FyEe^W6a%iZIF5!>bC-a4!t<7x-yh{DhUW zt<$+eRb}M-M0RFw+OCpZL11~8#G{LyNMt+;A8-o>N&;g>4xUdmuyQ=&eh}{juJ{$gASl#P_WShAV;LwZO2$ z*LAM=AxnGQ$aP@vQ@5K{H#aw%7ftqne(L#X(FtjCh$(bXw>74i0L{2OQW(X>yc0}C z;p0Rq$d|kt(o#@Jb$UykJmcMj@vzIIO1hgb4W@*22Y-8SC@phki6Ae_Tv`{)s>pbU zh@x!cr9m1%!iTs%i(q)=DMuCI2JYl7JZhuBnPe2V*k<~zG0CCpdeuf`RiI}{2T|#l z(%8jPghgCiEl7V2(wThx;Y(}Ma78uZE= zNzG!VT!UJyr!3Z+7F(EA^D45clC^RfJ2#QZ=c`f{F>@lHtI99WXLGam>6vMZb)4lx zNb>YuJ?vwXEwFuyMx)j?`h(tgWH$@l$NgS{CtaXzzizFov5Fc)*N7b({`Sjrj>ix~ z_w=rczGGeQuju^_aPkpS!v~s=-tzS z1G*S$0YQ~R_iA0$x7Ai&y~hCrY^xRhJ$>1K)qlnBc%XfNzVF|;*co>Gb&0Ec$i$Bs zxR?I}+-W{TzxgBp0h}Qxx4=EkSZh1@ZTdU%HNEfOv*;L&Y%i{d)r)`@xIA2V8(#$t zruyMVB87eB(~y?Bg;c~_itvp05yoRK?^lj0d}%0{_H(y|p)@bv9#N5(c_HnU%U+&w zg)pwV+(%Gf_A84=1+g63o<1y=4+t!DSj5O3fk%EvW}+l{Eh$&Ulle^HjBCKEuW&Y> z$!AaHrp`Z?o6lP8^H)`x!I~xg7_GUs7ugr^2obDp2!BZjr)JE#eD07HPvAA{?t+bg m-#0=W2}ooOdRzY+Fi2lFb`SLt!+{1IBXJUN;2B`Wu;Sn0$(W4* diff --git a/utils/__pycache__/logger.cpython-311.pyc b/utils/__pycache__/logger.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e8eb288c55698026e11423d1f01e28795e36d3a GIT binary patch literal 38602 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 XRwY~4De)tGgWZBlE$wee3g!7X str: + """Ermittelt das passende Emoji basierend auf der Nachricht""" + message_lower = message.lower() + for action, emoji in self.ACTION_EMOJIS.items(): + if action in message_lower: + return emoji + return '📝' + + def format(self, record): + # Zeitstempel mit schöner Formatierung + timestamp = datetime.fromtimestamp(record.created).strftime('%H:%M:%S.%f')[:-3] + colored_timestamp = f"{Colors.DIM}⏰ {timestamp}{Colors.RESET}" + + # Level mit Farbe und Emoji + level_color = self.LEVEL_COLORS.get(record.levelname, Colors.WHITE) + level_emoji = self.LEVEL_EMOJIS.get(record.levelname, '📝') + colored_level = f"{level_color}{level_emoji} {record.levelname:<8}{Colors.RESET}" + + # Component mit Farbe und Emoji + component = getattr(record, 'component', 'SYSTEM') + component_color = self.COMPONENT_COLORS.get(component, Colors.WHITE) + component_emoji = self.COMPONENT_EMOJIS.get(component, '📝') + colored_component = f"{component_color}{component_emoji} [{component:<11}]{Colors.RESET}" + + # Message mit Action-spezifischem Emoji + message = record.getMessage() + action_emoji = self._get_action_emoji(message) + + # User-Info hinzufügen falls verfügbar + user_info = "" + if hasattr(record, 'user') and record.user: + user_info = f" {Colors.BRIGHT_BLUE}👤 {record.user}{Colors.RESET}" + + # IP-Info hinzufügen falls verfügbar + ip_info = "" + if hasattr(record, 'ip') and record.ip: + ip_info = f" {Colors.DIM}🌍 {record.ip}{Colors.RESET}" + + # Duration-Info hinzufügen falls verfügbar + duration_info = "" + if hasattr(record, 'duration') and record.duration: + if record.duration > 1000: + duration_color = Colors.BRIGHT_RED + duration_emoji = "🐌" + elif record.duration > 500: + duration_color = Colors.BRIGHT_YELLOW + duration_emoji = "⏱️" + else: + duration_color = Colors.BRIGHT_GREEN + duration_emoji = "⚡" + duration_info = f" {duration_color}{duration_emoji} {record.duration:.2f}ms{Colors.RESET}" + + # Separator für bessere Lesbarkeit + separator = f"{Colors.DIM}│{Colors.RESET}" + + # Finale formatierte Nachricht mit schöner Struktur + formatted_message = ( + f"{colored_timestamp} {separator} " + f"{colored_level} {separator} " + f"{colored_component} {separator} " + f"{action_emoji} {message}" + f"{user_info}{ip_info}{duration_info}" + ) + + return formatted_message + +class JSONFormatter(logging.Formatter): + """JSON-Formatter für strukturierte Logs""" + + def format(self, record): + log_entry = { + 'timestamp': datetime.now().isoformat(), + 'level': record.levelname, + 'module': record.module, + 'function': record.funcName, + 'line': record.lineno, + 'message': record.getMessage(), + 'logger': record.name + } + + # Benutzerinformationen hinzufügen - nur im Application Context + try: + from flask import has_app_context, g, current_user + if has_app_context(): + if hasattr(g, 'user_id'): + log_entry['user_id'] = g.user_id + elif current_user and hasattr(current_user, 'id') and current_user.is_authenticated: + log_entry['user_id'] = current_user.id + except (ImportError, RuntimeError): + # Flask ist nicht verfügbar oder kein App-Context + pass + + # Request-Informationen hinzufügen - nur im Request Context + try: + from flask import has_request_context, request + if has_request_context() and request: + log_entry['request'] = { + 'method': getattr(request, 'method', None), + 'path': getattr(request, 'path', None), + 'remote_addr': getattr(request, 'remote_addr', None), + 'user_agent': str(getattr(request, 'user_agent', '')) + } + except (ImportError, RuntimeError): + # Flask ist nicht verfügbar oder kein Request-Context + pass + + # Performance-Informationen hinzufügen - nur im Application Context + try: + from flask import has_app_context, g + if has_app_context() and hasattr(g, 'start_time'): + duration = (datetime.now() - g.start_time).total_seconds() * 1000 + log_entry['duration_ms'] = round(duration, 2) + except (ImportError, RuntimeError): + # Flask ist nicht verfügbar oder kein App-Context + pass + + # Exception-Informationen hinzufügen + if record.exc_info: + log_entry['exception'] = { + 'type': record.exc_info[0].__name__, + 'message': str(record.exc_info[1]), + 'traceback': self.formatException(record.exc_info) + } + + return json.dumps(log_entry) + +class SocialNetworkLogger: + """Hauptklasse für das Social Network Logging""" + + def __init__(self, name: str = 'SysTades'): + self.name = name + self.logger = logging.getLogger(name) + self.logger.setLevel(logging.DEBUG) + + # Log-Verzeichnis erstellen + os.makedirs(LoggerConfig.LOG_DIR, exist_ok=True) + + # Handler nur einmal hinzufügen + if not self.logger.handlers: + self._setup_handlers() + + def _setup_handlers(self): + """Setup für verschiedene Log-Handler""" + + # Console Handler mit Farben + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + console_handler.setFormatter(ColoredFormatter()) + self.logger.addHandler(console_handler) + + # File Handler für alle Logs + from logging.handlers import RotatingFileHandler + file_handler = RotatingFileHandler( + os.path.join(LoggerConfig.LOG_DIR, 'app.log'), + maxBytes=LoggerConfig.MAX_LOG_SIZE, + backupCount=LoggerConfig.BACKUP_COUNT, + encoding='utf-8' + ) + file_handler.setLevel(logging.DEBUG) + file_formatter = logging.Formatter( + '%(asctime)s | %(levelname)s | %(name)s | %(component)s | %(message)s', + datefmt=LoggerConfig.DATE_FORMAT + ) + file_handler.setFormatter(file_formatter) + self.logger.addHandler(file_handler) + + # Error Handler für nur Fehler + error_handler = RotatingFileHandler( + os.path.join(LoggerConfig.LOG_DIR, 'errors.log'), + maxBytes=LoggerConfig.MAX_LOG_SIZE, + backupCount=LoggerConfig.BACKUP_COUNT, + encoding='utf-8' + ) + error_handler.setLevel(logging.ERROR) + error_handler.setFormatter(file_formatter) + self.logger.addHandler(error_handler) + + # API Handler für API-spezifische Logs + api_handler = RotatingFileHandler( + os.path.join(LoggerConfig.LOG_DIR, 'api.log'), + maxBytes=LoggerConfig.MAX_LOG_SIZE, + backupCount=LoggerConfig.BACKUP_COUNT, + encoding='utf-8' + ) + api_handler.setLevel(logging.INFO) + api_handler.addFilter(lambda record: hasattr(record, 'component') and record.component == 'API') + api_handler.setFormatter(file_formatter) + self.logger.addHandler(api_handler) + + def _log_with_context(self, level: str, message: str, component: str = 'SYSTEM', **kwargs): + """Log mit erweiterten Kontext-Informationen""" + extra = {'component': component} + + # User-Info hinzufügen + if 'user' in kwargs: + extra['user'] = kwargs['user'] + + # IP-Info hinzufügen + if 'ip' in kwargs: + extra['ip'] = kwargs['ip'] + + # Duration-Info hinzufügen + if 'duration' in kwargs: + extra['duration'] = kwargs['duration'] + + # Weitere Context-Daten + extra.update({k: v for k, v in kwargs.items() if k not in ['user', 'ip', 'duration']}) + + getattr(self.logger, level.lower())(message, extra=extra) + + def debug(self, message: str, component: str = 'SYSTEM', **kwargs): + """Debug-Level Logging mit erweiterten Infos""" + self._log_with_context('DEBUG', message, component, **kwargs) + + def info(self, message: str, component: str = 'SYSTEM', **kwargs): + """Info-Level Logging mit erweiterten Infos""" + self._log_with_context('INFO', message, component, **kwargs) + + def warning(self, message: str, component: str = 'SYSTEM', **kwargs): + """Warning-Level Logging mit erweiterten Infos""" + self._log_with_context('WARNING', message, component, **kwargs) + + def error(self, message: str, component: str = 'ERROR', **kwargs): + """Error-Level Logging mit erweiterten Infos""" + self._log_with_context('ERROR', message, component, **kwargs) + + def critical(self, message: str, component: str = 'ERROR', **kwargs): + """Critical-Level Logging mit erweiterten Infos""" + self._log_with_context('CRITICAL', message, component, **kwargs) + + # Erweiterte spezielle Logging-Methoden für Social Network + + def auth_success(self, username: str, ip: str = None, method: str = 'password'): + """Erfolgreiche Authentifizierung mit Details""" + message = f"Benutzer '{username}' erfolgreich angemeldet" + if method != 'password': + message += f" (Methode: {method})" + self.info(message, 'AUTH', user=username, ip=ip) + + def auth_failure(self, username: str, ip: str = None, reason: str = None, method: str = 'password'): + """Fehlgeschlagene Authentifizierung mit Details""" + message = f"Anmeldung fehlgeschlagen für '{username}'" + if reason: + message += f" - Grund: {reason}" + if method != 'password': + message += f" (Methode: {method})" + self.warning(message, 'AUTH', user=username, ip=ip) + + def user_action(self, username: str, action: str, details: str = None, target: str = None): + """Erweiterte Benutzer-Aktion mit mehr Details""" + message = f"{username}: {action}" + if target: + message += f" → {target}" + if details: + message += f" ({details})" + self.info(message, 'ACTIVITY', user=username) + + def api_request(self, method: str, endpoint: str, user: str = None, status: int = None, duration: float = None, size: int = None): + """Erweiterte API Request Logging""" + message = f"{method} {endpoint}" + + # Status-spezifische Emojis und Farben + if status: + if status >= 500: + message = f"Server Error: {message}" + component = 'ERROR' + elif status >= 400: + message = f"Client Error: {message}" + component = 'API' + elif status >= 300: + message = f"Redirect: {message}" + component = 'API' + else: + message = f"Success: {message}" + component = 'API' + else: + component = 'API' + + # Zusätzliche Infos + extras = {} + if user: + extras['user'] = user + if duration: + extras['duration'] = duration * 1000 # Convert to ms + if size: + message += f" ({self._format_bytes(size)})" + + if status and status >= 400: + self.warning(message, component, **extras) + else: + self.info(message, component, **extras) + + def database_operation(self, operation: str, table: str, success: bool = True, details: str = None, affected_rows: int = None): + """Erweiterte Datenbank-Operation Logging""" + message = f"DB {operation.upper()} auf '{table}'" + + if affected_rows is not None: + message += f" ({affected_rows} Zeilen)" + + if details: + message += f" - {details}" + + if success: + self.info(message, 'DB') + else: + self.error(message, 'DB') + + def security_event(self, event: str, user: str = None, ip: str = None, severity: str = 'warning', details: str = None): + """Erweiterte Sicherheitsereignis Logging""" + message = f"Security Event: {event}" + if details: + message += f" - {details}" + + extras = {} + if user: + extras['user'] = user + if ip: + extras['ip'] = ip + + if severity == 'critical': + self.critical(message, 'SECURITY', **extras) + elif severity == 'error': + self.error(message, 'SECURITY', **extras) + else: + self.warning(message, 'SECURITY', **extras) + + def performance_metric(self, metric_name: str, value: float, unit: str = 'ms', threshold: dict = None): + """Erweiterte Performance-Metrik Logging""" + message = f"Performance: {metric_name} = {value}{unit}" + + # Threshold-basierte Bewertung + if threshold and unit == 'ms': + if value > threshold.get('critical', 2000): + self.critical(message, 'PERFORMANCE', duration=value) + elif value > threshold.get('warning', 1000): + self.warning(message, 'PERFORMANCE', duration=value) + else: + self.info(message, 'PERFORMANCE', duration=value) + else: + self.info(message, 'PERFORMANCE') + + def social_interaction(self, user: str, action: str, target: str, target_type: str = 'post', target_user: str = None): + """Erweiterte Social Media Interaktion Logging""" + message = f"{user} {action} {target_type}" + if target_user and target_user != user: + message += f" von {target_user}" + message += f" (ID: {target})" + + self.info(message, 'SOCIAL', user=user) + + def system_startup(self, version: str = None, environment: str = None, port: int = None): + """Erweiterte System-Start Logging""" + message = "🚀 SysTades Social Network gestartet" + if version: + message += f" (v{version})" + if environment: + message += f" in {environment} Umgebung" + if port: + message += f" auf Port {port}" + self.info(message, 'SYSTEM') + + def system_shutdown(self, reason: str = None, uptime: float = None): + """Erweiterte System-Shutdown Logging""" + message = "🛑 SysTades Social Network beendet" + if uptime: + message += f" (Laufzeit: {self._format_duration(uptime)})" + if reason: + message += f" - Grund: {reason}" + self.info(message, 'SYSTEM') + + def file_operation(self, operation: str, filename: str, success: bool = True, size: int = None, user: str = None): + """Datei-Operation Logging""" + message = f"File {operation.upper()}: {filename}" + if size: + message += f" ({self._format_bytes(size)})" + + extras = {} + if user: + extras['user'] = user + + if success: + self.info(message, 'SYSTEM', **extras) + else: + self.error(message, 'SYSTEM', **extras) + + def cache_operation(self, operation: str, key: str, hit: bool = None, size: int = None): + """Cache-Operation Logging""" + message = f"Cache {operation.upper()}: {key}" + if hit is not None: + message += f" ({'HIT' if hit else 'MISS'})" + if size: + message += f" ({self._format_bytes(size)})" + + self.debug(message, 'SYSTEM') + + def email_sent(self, recipient: str, subject: str, success: bool = True, error: str = None): + """E-Mail Versand Logging""" + message = f"E-Mail an {recipient}: '{subject}'" + if not success and error: + message += f" - Fehler: {error}" + + if success: + self.info(message, 'SYSTEM') + else: + self.error(message, 'SYSTEM') + + def _format_bytes(self, bytes_count: int) -> str: + """Formatiert Byte-Anzahl in lesbare Form""" + for unit in ['B', 'KB', 'MB', 'GB']: + if bytes_count < 1024.0: + return f"{bytes_count:.1f}{unit}" + bytes_count /= 1024.0 + return f"{bytes_count:.1f}TB" + + def _format_duration(self, seconds: float) -> str: + """Formatiert Dauer in lesbare Form""" + if seconds < 60: + return f"{seconds:.1f}s" + elif seconds < 3600: + return f"{seconds/60:.1f}min" + else: + return f"{seconds/3600:.1f}h" + + def exception(self, exc: Exception, context: str = None, user: str = None): + """Erweiterte Exception Logging mit mehr Details""" + message = f"Exception: {type(exc).__name__}: {str(exc)}" + if context: + message = f"{context} - {message}" + + # Stack-Trace hinzufügen + stack_trace = traceback.format_exc() + message += f"\n{stack_trace}" + + extras = {} + if user: + extras['user'] = user + + self.error(message, 'ERROR', **extras) + +def log_execution_time(component: str = 'SYSTEM'): + """Decorator für Ausführungszeit-Logging""" + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + logger = SocialNetworkLogger() + start_time = time.time() + + try: + result = func(*args, **kwargs) + execution_time = (time.time() - start_time) * 1000 + logger.performance_metric(f"{func.__name__} Ausführungszeit", execution_time, 'ms') + return result + except Exception as e: + execution_time = (time.time() - start_time) * 1000 + logger.exception(e, f"Fehler in {func.__name__} nach {execution_time:.2f}ms") + raise + + return wrapper + return decorator + +def log_api_call(func): + """Decorator für API-Call Logging""" + @wraps(func) + def wrapper(*args, **kwargs): + from flask import request, current_user + + logger = SocialNetworkLogger() + start_time = time.time() + + # Request-Informationen sammeln + method = request.method + endpoint = request.endpoint or request.path + user = current_user.username if hasattr(current_user, 'username') and current_user.is_authenticated else 'Anonymous' + + try: + result = func(*args, **kwargs) + duration = time.time() - start_time + + # Status-Code ermitteln + status = getattr(result, 'status_code', 200) if hasattr(result, 'status_code') else 200 + + logger.api_request(method, endpoint, user, status, duration) + return result + + except Exception as e: + duration = time.time() - start_time + logger.api_request(method, endpoint, user, 500, duration) + logger.exception(e, f"API-Fehler in {endpoint}") + raise + + return wrapper + +def performance_monitor(operation_name: str = None): + """Erweiterte Decorator für Performance-Monitoring mit schönen Logs""" + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + logger = SocialNetworkLogger() + start_time = time.time() + + op_name = operation_name or func.__name__ + + # User-Info ermitteln falls verfügbar + user = None + try: + from flask import current_user + if hasattr(current_user, 'username') and current_user.is_authenticated: + user = current_user.username + except: + pass + + try: + result = func(*args, **kwargs) + duration = (time.time() - start_time) * 1000 + + # Performance-Kategorisierung mit Emojis + if duration > 2000: + logger.critical(f"Kritisch langsame Operation: {op_name}", 'PERFORMANCE', + user=user, duration=duration) + elif duration > 1000: + logger.warning(f"Langsame Operation: {op_name}", 'PERFORMANCE', + user=user, duration=duration) + elif duration > 500: + logger.info(f"Mäßige Operation: {op_name}", 'PERFORMANCE', + user=user, duration=duration) + else: + logger.debug(f"Schnelle Operation: {op_name}", 'PERFORMANCE', + user=user, duration=duration) + + return result + + except Exception as e: + duration = (time.time() - start_time) * 1000 + logger.error(f"Fehler in Operation: {op_name} nach {duration:.2f}ms", 'PERFORMANCE', + user=user, duration=duration) + logger.exception(e, f"Performance Monitor - {op_name}", user=user) + raise + + return wrapper + return decorator + +def log_user_activity(activity_name: str): + """Erweiterte Decorator für User-Activity Logging mit Details""" + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + from flask import current_user, request + + logger = SocialNetworkLogger() + start_time = time.time() + + # User und Request-Info sammeln + username = 'Anonymous' + ip = None + user_agent = None + + try: + if hasattr(current_user, 'username') and current_user.is_authenticated: + username = current_user.username + if request: + ip = request.remote_addr + user_agent = str(request.user_agent)[:100] # Begrenzen + except: + pass + + try: + result = func(*args, **kwargs) + duration = (time.time() - start_time) * 1000 + + # Erfolgreiche Aktivität loggen + details = f"Erfolgreich in {duration:.2f}ms" + if user_agent: + details += f" (Browser: {user_agent.split('/')[0] if '/' in user_agent else user_agent})" + + logger.user_action(username, activity_name, details=details) + + return result + + except Exception as e: + duration = (time.time() - start_time) * 1000 + logger.error(f"Fehler in User-Activity '{activity_name}' für {username} nach {duration:.2f}ms: {str(e)}", + 'ACTIVITY', user=username, ip=ip, duration=duration) + logger.exception(e, f"User Activity - {activity_name}", user=username) + raise + + return wrapper + return decorator + +# Globale Logger-Instanz +social_logger = SocialNetworkLogger() + +def get_logger(name: str = None) -> SocialNetworkLogger: + """Factory-Funktion für Logger-Instanzen""" + if name: + return SocialNetworkLogger(name) + return social_logger + +# Convenience-Funktionen für häufige Log-Operationen +def log_user_login(username: str, ip: str = None, success: bool = True): + """Shortcut für Login-Logging""" + if success: + social_logger.auth_success(username, ip) + else: + social_logger.auth_failure(username, ip) + +def log_user_action(username: str, action: str, details: str = None): + """Shortcut für Benutzer-Aktionen""" + social_logger.user_action(username, action, details) + +def log_social_action(user: str, action: str, target: str, target_type: str = 'post'): + """Shortcut für Social Media Aktionen""" + social_logger.social_interaction(user, action, target, target_type) + +def log_error(message: str, exception: Exception = None): + """Shortcut für Error-Logging""" + if exception: + social_logger.exception(exception, message) + else: + social_logger.error(message) + +def log_performance(metric_name: str, value: float, unit: str = 'ms'): + """Shortcut für Performance-Logging""" + social_logger.performance_metric(metric_name, value, unit) + +# Setup-Funktion für initiale Konfiguration +def setup_logging(app=None, log_level: str = 'INFO'): + """Setup-Funktion für die Flask-App""" + if app: + # Flask App Logging konfigurieren + app.logger.handlers.clear() + app.logger.addHandler(social_logger.logger.handlers[0]) # Console Handler + app.logger.setLevel(getattr(logging, log_level.upper())) + + # System-Start loggen + social_logger.system_startup() + + return social_logger + +if __name__ == "__main__": + # Test des Logging-Systems + logger = SocialNetworkLogger() + + logger.info("🧪 Teste das Logging-System") + logger.auth_success("testuser", "192.168.1.1") + logger.user_action("testuser", "Post erstellt", "Neuer Gedanke geteilt") + logger.social_interaction("user1", "like", "post_123") + logger.api_request("GET", "/api/social/posts", "testuser", 200, 0.045) + logger.database_operation("INSERT", "social_posts", True, "Neuer Post gespeichert") + logger.performance_metric("Seitenladezeit", 1234.5) + logger.warning("⚠️ Test-Warnung") + logger.error("❌ Test-Fehler") + logger.debug("🔍 Debug-Information") + + print(f"\n{Colors.BRIGHT_GREEN}✅ Logging-System erfolgreich getestet!{Colors.RESET}") + print(f"{Colors.CYAN}📁 Logs gespeichert in: {LoggerConfig.LOG_DIR}/{Colors.RESET}") \ No newline at end of file diff --git a/utils/user_manager.py b/utils/user_manager.py index 77234f3..3e6f387 100644 --- a/utils/user_manager.py +++ b/utils/user_manager.py @@ -9,11 +9,22 @@ from datetime import datetime parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, parent_dir) -from app import app +# Import models direkt, app wird lazy geladen from models import db, User +def get_app(): + """Lazy loading der Flask app um zirkuläre Imports zu vermeiden""" + try: + from flask import current_app + return current_app + except RuntimeError: + # Fallback wenn kein app context existiert + from app import app + return app + def list_users(): """List all users in the database.""" + app = get_app() with app.app_context(): try: users = User.query.all() @@ -37,6 +48,7 @@ def list_users(): def create_user(username, email, password, is_admin=False): """Create a new user in the database.""" + app = get_app() with app.app_context(): try: # Check if user already exists @@ -73,6 +85,7 @@ def create_user(username, email, password, is_admin=False): def reset_password(username, new_password): """Reset password for a user.""" + app = get_app() with app.app_context(): try: user = User.query.filter_by(username=username).first() @@ -93,6 +106,7 @@ def reset_password(username, new_password): def delete_user(username): """Delete a user from the database.""" + app = get_app() with app.app_context(): try: user = User.query.filter_by(username=username).first()