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 61c40f8..0940aa6 100644 Binary files a/__pycache__/app.cpython-311.pyc and b/__pycache__/app.cpython-311.pyc differ diff --git a/__pycache__/models.cpython-311.pyc b/__pycache__/models.cpython-311.pyc index 3f6911b..2bb4204 100644 Binary files a/__pycache__/models.cpython-311.pyc and b/__pycache__/models.cpython-311.pyc differ diff --git a/app.py b/app.py index acb1558..0723c0d 100644 --- a/app.py +++ b/app.py @@ -27,13 +27,18 @@ import ssl import certifi import os from sqlalchemy.orm import joinedload +import uuid as uuid_pkg + +# Custom Logger Integration +from utils.logger import get_logger, setup_logging, log_execution_time, log_api_call, performance_monitor, log_user_activity # Modelle importieren 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 + MindmapShare, PermissionType, SocialPost, SocialComment, Notification, + user_follows ) # Lade .env-Datei @@ -52,29 +57,16 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(), 'uploads')) -# Logger-Konfiguration -log_level = os.environ.get('LOG_LEVEL', 'INFO').upper() -log_dir = os.path.join(basedir, 'logs') -os.makedirs(log_dir, exist_ok=True) -log_file = os.path.join(log_dir, 'app.log') +# Initialisiere das erweiterte Logging-System +setup_logging(app, log_level=os.environ.get('LOG_LEVEL', 'INFO')) +logger = get_logger('SysTades') -# Handler für Datei-Logs -file_handler = logging.FileHandler(log_file) -file_handler.setFormatter(logging.Formatter( - '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' -)) -file_handler.setLevel(getattr(logging, log_level)) - -# Handler für Konsolen-Logs -console_handler = logging.StreamHandler() -console_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) -console_handler.setLevel(getattr(logging, log_level)) - -# Konfiguriere App-Logger -app.logger.addHandler(file_handler) -app.logger.addHandler(console_handler) -app.logger.setLevel(getattr(logging, log_level)) -app.logger.info('Anwendung gestartet') +# System-Start protokollieren +logger.system_startup( + version='1.0.0', + environment=os.environ.get('FLASK_ENV', 'development'), + port=5000 +) # Einheitliches Fehlerbehandlungssystem class ErrorHandler: @@ -90,7 +82,12 @@ class ErrorHandler: # Request-Informationen request_info = f"Endpoint: {endpoint or request.path}, Method: {request.method}, IP: {request.remote_addr}" # Vollständige Log-Nachricht - app.logger.error(f"Fehler {code}: {str(e)}\n{request_info}\n{user_info}\n{trace}") + logger.error( + f"Fehler {code}: {str(e)}\n{request_info}\n{user_info}\n{trace}", + component='ERROR', + user=current_user.username if current_user and current_user.is_authenticated else None, + ip=request.remote_addr if request else None + ) @staticmethod def api_error(message, code=400, details=None): @@ -152,12 +149,12 @@ try: # Einfache Testanfrage resp = client.models.list() openai_available = True - print("OpenAI API-Verbindung erfolgreich hergestellt.") + logger.info("OpenAI API-Verbindung erfolgreich hergestellt", component='SYSTEM') except Exception as e: - print(f"OpenAI API-Verbindungstest fehlgeschlagen: {e}") + logger.warning(f"OpenAI API-Verbindungstest fehlgeschlagen: {e}", component='ERROR') openai_available = False except Exception as e: - print(f"Fehler bei der Initialisierung des OpenAI-Clients: {e}") + logger.error(f"Fehler bei der Initialisierung des OpenAI-Clients: {e}", component='ERROR') openai_available = False client = None @@ -231,7 +228,7 @@ def create_default_users(): db.session.add(user) db.session.commit() - print(f"{len(users)} Benutzer wurden erstellt.") + logger.info(f"{len(users)} Benutzer wurden erstellt", component='DB') def create_default_categories(): """Erstellt die Standardkategorien für die Mindmap""" @@ -324,12 +321,12 @@ def create_default_categories(): db.session.add(sub_category) db.session.commit() - print("Standard-Kategorien wurden erstellt!") + logger.info("Standard-Kategorien wurden erstellt!", component='DB') def initialize_database(): """Initialisiert die Datenbank mit Grunddaten, falls diese leer ist""" try: - print("Initialisiere die Datenbank...") + logger.info("Initialisiere die Datenbank...", component='DB') # Erstelle alle Tabellen db.create_all() @@ -338,12 +335,19 @@ def initialize_database(): categories_count = Category.query.count() users_count = User.query.count() + logger.info( + f"Datenbank-Status: {categories_count} Kategorien, {users_count} Benutzer", + component='DB' + ) + # Erstelle Standarddaten, wenn es keine Kategorien gibt if categories_count == 0: + logger.info("Erstelle Standard-Kategorien...", component='DB') create_default_categories() # Admin-Benutzer erstellen, wenn keine Benutzer vorhanden sind if users_count == 0: + logger.info("Erstelle Admin-Benutzer...", component='DB') admin_user = User( username="admin", email="admin@example.com", @@ -353,7 +357,7 @@ def initialize_database(): admin_user.set_password("admin123") # Sicheres Passwort in der Produktion verwenden! db.session.add(admin_user) db.session.commit() - print("Admin-Benutzer wurde erstellt!") + logger.info("Admin-Benutzer wurde erstellt!", component='DB') # Prüfe, ob der "Wissen"-Knoten existiert, falls nicht, erstelle ihn wissen_node = MindMapNode.query.filter_by(name="Wissen").first() @@ -366,11 +370,17 @@ def initialize_database(): ) db.session.add(wissen_node) db.session.commit() - print("'Wissen'-Knoten wurde erstellt") + logger.info("'Wissen'-Knoten wurde erstellt", component='DB') + # Überprüfe, ob es Kategorien gibt, sonst erstelle sie + if Category.query.count() == 0: + create_default_categories() + logger.info("Kategorien wurden erstellt", component='DB') + + logger.info("Datenbank-Initialisierung erfolgreich abgeschlossen", component='DB') return True except Exception as e: - print(f"Fehler bei Datenbank-Initialisierung: {e}") + logger.error(f"Fehler bei Datenbank-Initialisierung: {e}", component='ERROR') return False # Instead of before_first_request, which is deprecated in newer Flask versions @@ -379,11 +389,20 @@ def init_app_database(app_instance): """Initialisiert die Datenbank für die Flask-App""" with app_instance.app_context(): # Überprüfe und initialisiere die Datenbank bei Bedarf - if not initialize_db_if_needed(db, initialize_database): + if not initialize_db_if_needed(db, initialize_database, app_instance): print("WARNUNG: Datenbankinitialisierung fehlgeschlagen. Einige Funktionen könnten eingeschränkt sein.") + return False + return True # Call the function to initialize the database -init_app_database(app) +# init_app_database(app) # Commented out to prevent context error - will be moved to end + +# If the initialization failed, log a warning +# if not initialize_db_if_needed(db, initialize_database): # Commented out to prevent context error +# logger.warning( +# "Datenbankinitialisierung fehlgeschlagen. Einige Funktionen könnten eingeschränkt sein.", +# component='ERROR' +# ) # Benutzerdefinierter Decorator für Admin-Zugriff def admin_required(f): @@ -415,26 +434,50 @@ def load_user(id): # Routes for authentication @app.route('/login', methods=['GET', 'POST']) +@log_execution_time(component='AUTH') def login(): if request.method == 'POST': - username = request.form.get('username') - password = request.form.get('password') + # Handle form submission + username = request.form['username'] + password = request.form['password'] + remember_me = 'remember_me' in request.form user = User.query.filter_by(username=username).first() if user and user.check_password(password): - login_user(user) - # Aktualisiere letzten Login-Zeitpunkt - user.last_login = datetime.now(timezone.utc) + login_user(user, remember=remember_me) + user.last_login = datetime.utcnow() db.session.commit() - next_page = request.args.get('next') - return redirect(next_page or url_for('index')) - - flash('Ungültiger Benutzername oder Passwort') + logger.user_activity( + user_id=user.id, + action='login', + details=f'Login erfolgreich für {username}', + ip=request.remote_addr + ) + + flash('Erfolgreich angemeldet!', 'success') + return redirect(url_for('mindmap')) + else: + logger.user_activity( + user_id=None, + action='failed_login', + details=f'Fehlgeschlagener Login-Versuch für {username}', + ip=request.remote_addr + ) + flash('Ungültiger Benutzername oder Passwort', 'error') + return render_template('login.html') +# Route für /auth/login für Kompatibilität +@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')) + @app.route('/register', methods=['GET', 'POST']) +@log_execution_time(component='AUTH') def register(): if request.method == 'POST': username = request.form.get('username') @@ -442,10 +485,20 @@ def register(): password = request.form.get('password') if User.query.filter_by(username=username).first(): + logger.warning( + f"Registrierung fehlgeschlagen: Benutzername '{username}' bereits vergeben", + component='AUTH', + ip=request.remote_addr + ) flash('Benutzername existiert bereits') return redirect(url_for('register')) if User.query.filter_by(email=email).first(): + logger.warning( + f"Registrierung fehlgeschlagen: E-Mail '{email}' bereits registriert", + component='AUTH', + ip=request.remote_addr + ) flash('E-Mail ist bereits registriert') return redirect(url_for('register')) @@ -454,6 +507,13 @@ def register(): db.session.add(user) db.session.commit() # Commit, um eine ID für den Benutzer zu erhalten + # Erfolgreiche Registrierung protokollieren + logger.info( + f"Neuer Benutzer registriert: {username} ({email})", + component='AUTH', + ip=request.remote_addr + ) + # Erstelle eine Standard-Mindmap für den neuen Benutzer try: default_mindmap = UserMindmap( @@ -463,8 +523,17 @@ def register(): ) db.session.add(default_mindmap) db.session.commit() + + logger.info( + f"Standard-Mindmap für Benutzer {username} erstellt", + component='DB', + user=username + ) except Exception as e: - print(f"Fehler beim Erstellen der Standard-Mindmap: {e}") + logger.error( + f"Fehler beim Erstellen der Standard-Mindmap für {username}: {e}", + component='ERROR' + ) # Stelle sicher, dass wir trotzdem weitermachen können db.session.rollback() @@ -475,8 +544,18 @@ def register(): @app.route('/logout') @login_required +@log_execution_time(component='AUTH') def logout(): + username = current_user.username if current_user.is_authenticated else 'Unknown' logout_user() + + # Abmeldung protokollieren + logger.info( + f"Benutzer {username} abgemeldet", + component='AUTH', + ip=request.remote_addr + ) + return redirect(url_for('index')) # Route for the homepage @@ -505,12 +584,12 @@ def mindmap(): ) db.session.add(wissen_node) db.session.commit() - print("'Wissen'-Knoten wurde erstellt") + logger.info("'Wissen'-Knoten wurde erstellt", component='DB') # Überprüfe, ob es Kategorien gibt, sonst erstelle sie if Category.query.count() == 0: create_default_categories() - print("Kategorien wurden erstellt") + logger.info("Kategorien wurden erstellt", component='DB') # Stelle sicher, dass die Route für statische Dateien korrekt ist mindmap_js_path = url_for('static', filename='js/mindmap-init.js') @@ -845,6 +924,8 @@ def delete_mindmap(mindmap_id): # API-Endpunkte für UserMindmap CRUD-Operationen @app.route('/api/mindmaps', methods=['POST']) @login_required +@log_api_call +@performance_monitor('mindmap_creation') def api_create_user_mindmap(): data = request.get_json() name = data.get('name') @@ -862,6 +943,13 @@ def api_create_user_mindmap(): ) db.session.add(new_mindmap) db.session.commit() + + logger.user_action( + username=current_user.username, + action='mindmap_created', + details=f"Neue Mindmap '{name}' erstellt" + ) + return jsonify({ 'id': new_mindmap.id, 'name': new_mindmap.name, @@ -874,6 +962,7 @@ def api_create_user_mindmap(): @app.route('/api/mindmaps', methods=['GET']) @login_required +@log_api_call def api_get_user_mindmaps(): mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all() return jsonify([{ @@ -1847,13 +1936,27 @@ def unauthorized(e): # OpenAI-Integration für KI-Assistenz @app.route('/api/assistant', methods=['POST']) +@log_api_call +@performance_monitor('ai_assistant') def chat_with_assistant(): """Chat mit dem KI-Assistenten""" data = request.json user_message = data.get('message', '') + user_name = current_user.username if current_user.is_authenticated else 'Anonymous' + + logger.info( + f"KI-Assistent Anfrage von {user_name}: {user_message[:100]}...", + component='API', + user=user_name if current_user.is_authenticated else None + ) + # Überprüfe, ob die OpenAI-API verfügbar ist if not openai_available or client is None: + logger.warning( + "OpenAI API nicht verfügbar - Fallback-Antwort gesendet", + component='API' + ) # Fallback-Antwort, wenn OpenAI nicht verfügbar ist fallback_message = { "response": "Der KI-Assistent ist derzeit nicht verfügbar. Bitte versuchen Sie es später erneut oder kontaktieren Sie den Administrator.", @@ -1913,6 +2016,12 @@ def chat_with_assistant(): import traceback print(f"Stack Trace: {traceback.format_exc()}") + logger.error( + f"Fehler bei der OpenAI-Anfrage von {user_name}: {e}", + component='ERROR', + user=user_name if current_user.is_authenticated else None + ) + # Fallback-Antwort bei Fehler fallback_message = { "response": "Es tut mir leid, aber ich konnte Ihre Anfrage nicht verarbeiten. Bitte versuchen Sie es später erneut.", @@ -2062,398 +2171,115 @@ def dummy_network_bg(): """Leere Antwort für die nicht mehr verwendeten Netzwerk-Hintergrundbilder.""" return '', 200 -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 - -# API-Endpunkt für die Root-Mindmap -@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() - - # Überprüfen, ob Kategorien vorhanden sind - if not categories: - print("Keine Hauptkategorien gefunden") - return jsonify({ - 'success': False, - 'error': 'Keine Hauptkategorien gefunden', - 'details': 'Bitte führen Sie das Datenbank-Initialisierungsskript aus' - }), 404 - - print(f"Gefundene Hauptkategorien: {[cat.name for cat in categories]}") - - # Basis-Knoten erstellen (ohne has_children gesetzt, da Wissen selbst keine Unterkategorien haben soll) - nodes = [{ - 'id': 'root', - 'name': 'Wissen', - 'description': 'Zentrale Wissensbasis', - 'color_code': '#4299E1', - 'is_center': True, - 'has_children': False, # Geändert, damit "Wissen" selbst keine Unterkategorien hat - '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(len(category.children) > 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 5628e04..39b4d95 100644 Binary files a/database/systades.db and b/database/systades.db differ diff --git a/instance/logs/app.log b/instance/logs/app.log new file mode 100644 index 0000000..e69de29 diff --git a/instance/logs/errors.log b/instance/logs/errors.log new file mode 100644 index 0000000..e69de29 diff --git a/instance/logs/social.log b/instance/logs/social.log new file mode 100644 index 0000000..e69de29 diff --git a/logs/api.log b/logs/api.log new file mode 100644 index 0000000..e69de29 diff --git a/logs/app.log b/logs/app.log index 68eb29d..3f39021 100644 --- a/logs/app.log +++ b/logs/app.log @@ -1439,3 +1439,967 @@ Traceback (most recent call last): 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 C:\Users\firem\Desktop\111\Systades\website\app.py:93] +2025-05-28 20:22:12 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:22:12 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:25:36 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:25:36 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:25:36 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:25:36 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:25:44 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:25:44 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:25:44 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:25:44 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:38:54 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:38:54 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:38:54 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:38:54 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:40:18 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:40:18 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:40:18 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:40:18 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:42:31 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:42:31 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:42:31 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:42:31 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:42:43 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:42:43 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:42:43 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:42:43 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:43:25 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:43:25 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:43:25 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet +2025-05-28 20:43:25 | INFO | SysTades | SYSTEM | 🚀 SysTades Social Network gestartet (v2.0.0) in development Umgebung +2025-05-28 20:48:39,492 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:48:41,249 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:48:41,249 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:48:44,205 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:48:45,546 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:48:45,546 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +2025-05-28 20:48:49,477 INFO: Anwendung gestartet [in /home/core/dev/website/app.py:77] +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: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 06a0b36..a2bec19 100644 Binary files a/utils/__pycache__/__init__.cpython-311.pyc and b/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/utils/__pycache__/db_check.cpython-311.pyc b/utils/__pycache__/db_check.cpython-311.pyc index f594fb5..42aff1f 100644 Binary files a/utils/__pycache__/db_check.cpython-311.pyc and b/utils/__pycache__/db_check.cpython-311.pyc differ diff --git a/utils/__pycache__/logger.cpython-311.pyc b/utils/__pycache__/logger.cpython-311.pyc new file mode 100644 index 0000000..1e8eb28 Binary files /dev/null and b/utils/__pycache__/logger.cpython-311.pyc differ diff --git a/utils/__pycache__/user_manager.cpython-311.pyc b/utils/__pycache__/user_manager.cpython-311.pyc index 2400ee6..25f0d8d 100644 Binary files a/utils/__pycache__/user_manager.cpython-311.pyc and b/utils/__pycache__/user_manager.cpython-311.pyc differ diff --git a/utils/db_check.py b/utils/db_check.py index 17ec701..4113189 100644 --- a/utils/db_check.py +++ b/utils/db_check.py @@ -1,17 +1,17 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from flask import current_app from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import text import time -def check_db_connection(db): +def check_db_connection(db, app=None): """ Überprüft die Datenbankverbindung und versucht ggf. die Verbindung wiederherzustellen Args: db: SQLAlchemy-Instanz + app: Flask-App-Instanz (optional, falls nicht im App-Kontext) Returns: bool: True, wenn die Verbindung erfolgreich ist, sonst False @@ -22,7 +22,11 @@ def check_db_connection(db): while retry_count < max_retries: try: # Führe eine einfache Abfrage durch, um die Verbindung zu testen - with current_app.app_context(): + if app: + with app.app_context(): + db.session.execute(text('SELECT 1')) + else: + # Versuche ohne expliziten App-Kontext (falls bereits im Kontext) db.session.execute(text('SELECT 1')) return True except SQLAlchemyError as e: @@ -38,42 +42,60 @@ def check_db_connection(db): db.session.rollback() except: pass + except Exception as e: + print(f"Allgemeiner Fehler bei DB-Check: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + time.sleep(1) return False -def initialize_db_if_needed(db, initialize_function=None): +def initialize_db_if_needed(db, initialize_function=None, app=None): """ Initialisiert die Datenbank, falls erforderlich Args: db: SQLAlchemy-Instanz initialize_function: Funktion, die aufgerufen wird, um die Datenbank zu initialisieren + app: Flask-App-Instanz (optional, falls nicht im App-Kontext) Returns: bool: True, wenn die Datenbank bereit ist, sonst False """ # Prüfe die Verbindung - if not check_db_connection(db): + if not check_db_connection(db, app): return False # Prüfe, ob die Tabellen existieren try: - with current_app.app_context(): - # Führe eine Testabfrage auf einer Tabelle durch + if app: + with app.app_context(): + # Führe eine Testabfrage auf einer Tabelle durch + db.session.execute(text('SELECT COUNT(*) FROM user')) + else: + # Versuche ohne expliziten App-Kontext db.session.execute(text('SELECT COUNT(*) FROM user')) except SQLAlchemyError: # Tabellen existieren nicht, erstelle sie try: - with current_app.app_context(): + if app: + with app.app_context(): + db.create_all() + + # Rufe die Initialisierungsfunktion auf, falls vorhanden + if initialize_function and callable(initialize_function): + initialize_function() + else: db.create_all() - - # Rufe die Initialisierungsfunktion auf, falls vorhanden if initialize_function and callable(initialize_function): initialize_function() - return True + return True except Exception as e: print(f"Fehler bei DB-Initialisierung: {str(e)}") return False + except Exception as e: + print(f"Fehler beim Prüfen der Datenbank-Tabellen: {str(e)}") + return False return True \ No newline at end of file diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..a1c42b4 --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,792 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging +import os +import sys +from datetime import datetime +from functools import wraps +from flask import request, g, current_app +from flask_login import current_user +import traceback +import json +import time + +# ANSI Color Codes für farbige Terminal-Ausgabe +class Colors: + # Standard Colors + RESET = '\033[0m' + BOLD = '\033[1m' + DIM = '\033[2m' + + # Foreground Colors + BLACK = '\033[30m' + RED = '\033[31m' + GREEN = '\033[32m' + YELLOW = '\033[33m' + BLUE = '\033[34m' + MAGENTA = '\033[35m' + CYAN = '\033[36m' + WHITE = '\033[37m' + + # Bright Colors + BRIGHT_RED = '\033[91m' + BRIGHT_GREEN = '\033[92m' + BRIGHT_YELLOW = '\033[93m' + BRIGHT_BLUE = '\033[94m' + BRIGHT_MAGENTA = '\033[95m' + BRIGHT_CYAN = '\033[96m' + BRIGHT_WHITE = '\033[97m' + + # Background Colors + BG_RED = '\033[41m' + BG_GREEN = '\033[42m' + BG_YELLOW = '\033[43m' + BG_BLUE = '\033[44m' + BG_MAGENTA = '\033[45m' + BG_CYAN = '\033[46m' + +class LoggerConfig: + """Konfiguration für das Logging-System""" + LOG_DIR = 'logs' + MAX_LOG_SIZE = 10 * 1024 * 1024 # 10MB + BACKUP_COUNT = 5 + LOG_FORMAT = '%(asctime)s | %(levelname)s | %(name)s | %(message)s' + DATE_FORMAT = '%Y-%m-%d %H:%M:%S' + +class ColoredFormatter(logging.Formatter): + """Custom Formatter für farbige Log-Ausgaben mit schönen Emojis""" + + LEVEL_COLORS = { + 'DEBUG': Colors.BRIGHT_CYAN, + 'INFO': Colors.BRIGHT_GREEN, + 'WARNING': Colors.BRIGHT_YELLOW, + 'ERROR': Colors.BRIGHT_RED, + 'CRITICAL': Colors.BG_RED + Colors.BRIGHT_WHITE + } + + COMPONENT_COLORS = { + 'AUTH': Colors.BLUE, + 'API': Colors.GREEN, + 'DB': Colors.MAGENTA, + 'SOCIAL': Colors.CYAN, + 'SYSTEM': Colors.YELLOW, + 'ERROR': Colors.RED, + 'SECURITY': Colors.BRIGHT_MAGENTA, + 'PERFORMANCE': Colors.BRIGHT_BLUE, + 'ACTIVITY': Colors.BRIGHT_CYAN + } + + # Erweiterte Emoji-Mappings für verschiedene Komponenten und Aktionen + COMPONENT_EMOJIS = { + 'AUTH': '🔐', + 'API': '🌐', + 'DB': '🗄️', + 'SOCIAL': '👥', + 'SYSTEM': '⚙️', + 'ERROR': '💥', + 'SECURITY': '🛡️', + 'PERFORMANCE': '⚡', + 'ACTIVITY': '🎯' + } + + # Spezielle Emojis für verschiedene Log-Level + LEVEL_EMOJIS = { + 'DEBUG': '🔍', + 'INFO': '✅', + 'WARNING': '⚠️', + 'ERROR': '❌', + 'CRITICAL': '🚨' + } + + # Action-spezifische Emojis + ACTION_EMOJIS = { + 'login': '🚪', + 'logout': '🚪', + 'register': '📝', + 'like': '❤️', + 'unlike': '💔', + 'comment': '💬', + 'share': '🔄', + 'follow': '➕', + 'unfollow': '➖', + 'bookmark': '🔖', + 'unbookmark': '📑', + 'post_created': '📝', + 'post_deleted': '🗑️', + 'upload': '📤', + 'download': '📥', + 'search': '🔍', + 'notification': '🔔', + 'message': '💌', + 'profile_update': '👤', + 'settings': '⚙️', + 'admin': '👑', + 'backup': '💾', + 'restore': '🔄', + 'migration': '🚚', + 'cache': '⚡', + 'email': '📧', + 'password_reset': '🔑', + 'verification': '✅', + 'ban': '🚫', + 'unban': '✅', + 'report': '🚩', + 'moderate': '🛡️' + } + + def _get_action_emoji(self, message: str) -> 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()