Compare commits

..

27 Commits

Author SHA1 Message Date
a7bb9563b3 hintergrund Fixes 2025-04-29 20:49:43 +01:00
4e0c470663 Update compiled Python files in __pycache__ for utils module 2025-04-29 20:38:00 +01:00
b5300f74bd Update compiled Python files in __pycache__ for models and utils modules 2025-04-29 20:35:23 +01:00
6a53e621ca ''Über Uns'' 2025-04-29 20:31:02 +01:00
a59ce652af Impressum 2025-04-29 19:26:35 +01:00
27cfc95081 Merge branch 'tills-branch' of https://git.clickcandit.com/marwinm/website into tills-branch 2025-04-29 20:24:41 +02:00
c513666391 Refactor nodeCount to config object" Explanation: This commit message follows the Conventional C... 2025-04-29 20:11:43 +02:00
ae30dbce57 Flask Fix JAHAH 2025-04-29 19:08:49 +01:00
817ddd98e9 chore: Änderungen commited 2025-04-29 20:08:12 +02:00
bfce2fc7b7 chore: Änderungen commited 2025-04-29 20:06:29 +02:00
efbcd567ee chore: Änderungen commited 2025-04-29 20:04:32 +02:00
a873765d08 feat: Implement smooth animation with adjustable pulse and flow speeds 2025-04-29 20:02:51 +02:00
efbcadb95a chore: Änderungen commited 2025-04-29 20:00:58 +02:00
da3ccaffe9 Refactor nodeCount configuration in NeuralNetworkBackground.js 2025-04-29 19:59:09 +02:00
f4e04573bd chore: Änderungen commited 2025-04-29 19:57:44 +02:00
aa253f3871 chore: Änderungen commited 2025-04-29 19:27:29 +02:00
cfd6a25b21 📝 Commit Message: "Refactor app module binary files using Conventional Commits (feat)" Explanat... 2025-04-29 19:25:18 +02:00
d307763007 Aktualisiere die Konfiguration der neuronalen Netzwerk-Hintergrundanimation in neural-network-background.js, um die Anzahl der Knoten, Variationen, Geschwindigkeiten und Sichtbarkeiten anzupassen. Füge neue Parameter für aktive Flows und Flussweiterleitung hinzu, um die Animation dynamischer zu gestalten. Optimiere die Logik zur Verwaltung aktiver Flows und verbessere die Sichtbarkeit von Verbindungen für ein flüssigeres Benutzererlebnis. 2025-04-29 15:19:33 +02:00
d7e6912e08 Aktualisiere die Umgebungsvariablen in .env mit neuen Werten für SECRET_KEY und OPENAI_API_KEY. Entferne die alte Instanz von systades.db und aktualisiere die neuronale Netzwerk-Hintergrundanimation in neural-network-background.js mit verbesserten visuellen Effekten, einschließlich einer neuen Zickzack-Bewegung für Blitze und subtileren Glüheffekten. Implementiere eine sanftere Ausblendanimation für das Canvas-Element und verbessere die Funkenanimationen. Optimiere die Cluster-Generierung und -Verteilung für eine flüssigere Benutzererfahrung. 2025-04-29 14:58:54 +02:00
ffe96074f4 Verbessere die Funktionalität des neuronalen Netzwerk-Hintergrunds in neural-network-background.js durch die Einführung einer flexiblen Konfigurationsstruktur, die benutzerdefinierte Optionen unterstützt. Optimiere die Cluster-Generierung und -Verteilung, verbessere die Verbindungslogik zwischen Knoten und implementiere erweiterte Fehlerbehandlung in den API-Funktionen in mindmap.js. Füge Fallback-Knoten hinzu, um die Benutzererfahrung zu verbessern, und implementiere visuelle Rückmeldungen für die Initialisierung der Mindmap. 2025-04-29 12:32:18 +02:00
49ccf3908a Merge remote-tracking branch 'origin/tills-branch' into tills-branch 2025-04-29 10:28:36 +02:00
9514645904 keine ahnung ehrlich 2025-04-29 10:28:15 +02:00
63f45abb3e background, mindmap 2025-04-29 10:27:07 +02:00
7d74b5a7bf Implement Flask-Migrate for database migrations in app.py, disable CSRF protection, and update requirements.txt to include Flask-Migrate. Remove obsolete systades.db file and add migration configuration files for Alembic. 2025-04-28 21:41:38 +02:00
55f2553780 Update ROADMAP.md, remove CORS & flask-cors from app.py, and update requirements.txt: no longer use CORS for Flask-SocketIO. 2025-04-28 21:24:11 +02:00
0852ea070b Add Flask-CORS and SocketIO for real-time updates, refactor database handling to use a temporary Flask app; improve error handling with @app.errorhandler decorators. 2025-04-28 15:21:11 +02:00
7a0533ac09 Verbessere die Funktionalität des Chat-Assistenten in app.py: Aktualisiere die Systemnachricht mit spezifischen Informationen zur Systades-Wissensdatenbank und erweitere die API-Nachrichtenformatierung. Füge Unterstützung für ausgewählte Elemente aus der Datenbank hinzu und erhöhe die maximale Tokenanzahl für detailliertere Antworten. Implementiere eine neue JavaScript-Datei für eine neuronale Netzwerk-Hintergrundanimation und verbessere die CSS-Stile für den Light Mode. Optimiere die Benutzeroberfläche und die Lesbarkeit in beiden Modi. Aktualisiere die Grundstile für eine konsistente Darstellung. 2025-04-28 14:49:02 +02:00
54 changed files with 5326 additions and 1525 deletions

15
.env
View File

@@ -1,13 +1,2 @@
# MindMap Umgebungsvariablen SECRET_KEY=eed9298856dc9363cd32778265780d6904ba24e6a6b815a2cc382bcdd767ea7b
# Kopiere diese Datei zu .env und passe die Werte an OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
# Flask
SECRET_KEY=dein-geheimer-schluessel-hier
# OpenAI API
OPENAI_API_KEY=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
# Datenbank
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
# Der Pfad wird relativ zum Projektverzeichnis angegeben
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db OPENAI_API_KEY=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

8
.vscode/jsconfig.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "esnext",
"lib": [
"esnext"
]
}
}

68
.vscode/main.js vendored Normal file
View File

@@ -0,0 +1,68 @@
/// <reference types="vscode" />
// @ts-check
// API: https://code.visualstudio.com/api/references/vscode-api
// @ts-ignore
const vscode = require('vscode');
* @typedef {import('vscode').ExtensionContext} ExtensionContext
* @typedef {import('vscode').commands} commands
* @typedef {import('vscode').window} window
* @typedef {import('vscode').TextEditor} TextEditor
* @typedef {import('vscode').TextDocument} TextDocument
*/
/**
* Aktiviert die Erweiterung und registriert den Auto-Resume-Befehl
* @param {vscode.ExtensionContext} context - Der Erweiterungskontext
*/
function activate(context) {
const disposable = vscode.commands.registerCommand('extension.autoResume', () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const document = editor.document;
const text = document.getText();
// Track last click time to avoid multiple clicks
let lastClickTime = 0;
// Main function that looks for and clicks the resume link
function clickResumeLink() {
// Prevent clicking too frequently (3 second cooldown)
const now = Date.now();
if (now - lastClickTime < 3000) return;
// Check if text contains rate limit text
if (text.includes('stop the agent after 25 tool calls') ||
text.includes('Note: we default stop')) {
// Find the resume link position
const resumePos = text.indexOf('resume the conversation');
if (resumePos !== -1) {
vscode.window.showInformationMessage('Auto-resuming conversation...');
lastClickTime = now;
}
}
}
// Führe periodisch aus
const interval = global.setInterval(clickResumeLink, 1000);
// Speichere das Intervall in den Subscriptions
context.subscriptions.push({
dispose: () => global.clearInterval(interval)
});
// Führe die Funktion sofort aus
clickResumeLink();
});
context.subscriptions.push(disposable);
}
function deactivate() {
// Cleanup if needed
}
module.exports = {
activate,
deactivate
}

View File

@@ -16,7 +16,82 @@
- Vergessen der Mindmap-Datenstruktur gemäß der Roadmap - Vergessen der Mindmap-Datenstruktur gemäß der Roadmap
# Häufige Fehler und Lösungen # Häufige Fehler und Lösungen
D
## Datenbankfehler
### Fehler: "no such column: user.password"
**Fehlerbeschreibung:**
```
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: user.password
[SQL: SELECT user.id AS user_id, user.username AS user_username, user.email AS user_email, user.password AS user_password, user.created_at AS user_created_at, user.is_active AS user_is_active, user.role AS user_role
FROM user
WHERE user.id = ?]
```
**Ursache:**
Die Spalte `password` fehlt in der Tabelle `user` der SQLite-Datenbank. Dies kann durch eine unvollständige Datenbankinitialisierung oder ein fehlerhaftes Schema-Update verursacht worden sein.
**Lösung:**
1. **Datenbank reparieren mit dem Fix-Skript**
```bash
python fix_user_table.py
```
Dieses Skript:
- Prüft, ob die Tabelle `user` existiert und erstellt sie, falls nicht
- Prüft, ob die Spalte `password` existiert und fügt sie hinzu, falls nicht
2. **Standardbenutzer erstellen**
```bash
python create_default_users.py
```
Dieses Skript:
- Erstellt Standardbenutzer (admin, user), falls keine vorhanden sind
- Setzt Passwörter mit korrektem Hashing
3. **Datenbank testen**
```bash
python test_app.py
```
Dieses Skript überprüft:
- Ob die Datenbank existiert
- Ob die Tabelle `user` korrekt konfiguriert ist
- Ob Benutzer vorhanden sind
## Häufige Ursachen für Datenbankfehler
1. **Inkonsistente Datenbankschemas**
- Unterschiede zwischen dem SQLAlchemy-Modell und der tatsächlichen Datenbankstruktur
- Fehlende Spalten, die in den Modellen definiert sind
2. **Falsche Datenbankinitialisierung**
- Die Datenbank wurde nicht korrekt initialisiert
- Fehler bei der Migration oder dem Schema-Update
3. **Datenbankdatei-Korrumpierung**
- Die SQLite-Datenbankdatei wurde beschädigt
- Lösung: Sicherung wiederherstellen oder Datenbank neu erstellen
## Vorbeugende Maßnahmen
1. **Regelmäßige Backups**
- Tägliche Sicherung der Datenbankdatei
2. **Schema-Validierung**
- Regelmäßige Überprüfung der Datenbankstruktur
- Automatisierte Tests für Datenbankschema
3. **Datenbankmigration**
- Verwenden Sie Flask-Migrate für strukturierte Datenbank-Updates
- Dokumentieren Sie alle Schemaänderungen
## Content Security Policy (CSP) ## Content Security Policy (CSP)
### Problem: Externe Ressourcen werden nicht geladen ### Problem: Externe Ressourcen werden nicht geladen
@@ -79,15 +154,6 @@ D
3. Stellen Sie sicher, dass die Datei `static/css/tailwind.min.css` existiert und aktuell ist. 3. Stellen Sie sicher, dass die Datei `static/css/tailwind.min.css` existiert und aktuell ist.
## Datenbank-Fehler
### Problem: Datenbank existiert nicht
**Fehler:** SQLite-Datenbank kann nicht geöffnet werden.
**Lösung:**
1. Datenbank initialisieren: `python TOOLS.py db:rebuild`
2. Sicherstellen, dass das Datenbankverzeichnis existiert und Schreibrechte hat
## Authentifizierung ## Authentifizierung
### Problem: Login funktioniert nicht ### Problem: Login funktioniert nicht

View File

@@ -166,3 +166,7 @@ Der neue WebGL-basierte Hintergrund bietet:
- Integration von Exportfunktionen für Mindmaps - Integration von Exportfunktionen für Mindmaps
*Zuletzt aktualisiert: 15.06.2024* *Zuletzt aktualisiert: 15.06.2024*
## [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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

80
app.py
View File

@@ -17,6 +17,8 @@ import secrets
from sqlalchemy.sql import func from sqlalchemy.sql import func
from openai import OpenAI from openai import OpenAI
from dotenv import load_dotenv from dotenv import load_dotenv
from flask_socketio import SocketIO, emit
from flask_migrate import Migrate
# Modelle importieren # Modelle importieren
from models import ( from models import (
@@ -39,6 +41,8 @@ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key')
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung 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'))
app.config['WTF_CSRF_ENABLED'] = False
# OpenAI API-Konfiguration # OpenAI API-Konfiguration
api_key = os.environ.get("OPENAI_API_KEY") api_key = os.environ.get("OPENAI_API_KEY")
@@ -88,6 +92,11 @@ login_manager.login_view = 'login'
# Erst nach der App-Initialisierung die DB-Check-Funktionen importieren # Erst nach der App-Initialisierung die DB-Check-Funktionen importieren
from utils.db_check import check_db_connection, initialize_db_if_needed from utils.db_check import check_db_connection, initialize_db_if_needed
# SocketIO initialisieren
socketio = SocketIO(app)
migrate = Migrate(app, db)
def create_default_categories(): def create_default_categories():
"""Erstellt die Standardkategorien für die Mindmap""" """Erstellt die Standardkategorien für die Mindmap"""
# Hauptkategorien # Hauptkategorien
@@ -509,6 +518,10 @@ def datenschutz():
def agb(): def agb():
return render_template('agb.html') return render_template('agb.html')
@app.route('/ueber-uns/')
def ueber_uns():
return render_template('ueber_uns.html')
# Benutzer-Mindmap-Funktionalität # Benutzer-Mindmap-Funktionalität
@app.route('/my-mindmap/<int:mindmap_id>') @app.route('/my-mindmap/<int:mindmap_id>')
@login_required @login_required
@@ -1212,8 +1225,17 @@ def chat_with_assistant():
# Extrahiere Systemnachricht falls vorhanden, sonst Standard-Systemnachricht # Extrahiere Systemnachricht falls vorhanden, sonst Standard-Systemnachricht
system_message = next((msg['content'] for msg in messages if msg['role'] == 'system'), system_message = next((msg['content'] for msg in messages if msg['role'] == 'system'),
"Du bist ein hilfreicher Assistent, der Zugriff auf die Wissensdatenbank hat. " "Du bist ein spezialisierter Assistent für Systades, eine innovative Wissensmanagement-Plattform. "
"Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern. " "Systades ist ein intelligentes System zur Verwaltung, Verknüpfung und Visualisierung von Wissen. "
"Die Plattform ermöglicht es Nutzern, Gedanken zu erfassen, in Kategorien zu organisieren und durch Mindmaps zu visualisieren. "
"Wichtige Funktionen sind:\n"
"- Gedankenverwaltung mit Titeln, Zusammenfassungen und Keywords\n"
"- Kategorisierung und thematische Organisation\n"
"- Interaktive Mindmaps zur Wissensvisualisierung\n"
"- KI-gestützte Analyse und Zusammenfassung von Inhalten\n"
"- Kollaborative Wissensarbeit und Teilen von Inhalten\n\n"
"Du antwortest AUSSCHLIESSLICH auf Fragen bezüglich der Systades-Wissensdatenbank und Website. "
"Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern und durch Themen führen. "
"Antworte informativ, sachlich und gut strukturiert auf Deutsch.") "Antworte informativ, sachlich und gut strukturiert auf Deutsch.")
# Formatiere Nachrichten für OpenAI API # Formatiere Nachrichten für OpenAI API
@@ -1227,6 +1249,7 @@ def chat_with_assistant():
# Alte Implementierung für direktes Prompt # Alte Implementierung für direktes Prompt
prompt = data.get('prompt', '') prompt = data.get('prompt', '')
context = data.get('context', '') context = data.get('context', '')
selected_items = data.get('selected_items', []) # Ausgewählte Elemente aus der Datenbank
if not prompt: if not prompt:
return jsonify({ return jsonify({
@@ -1235,14 +1258,40 @@ def chat_with_assistant():
# Zusammenfassen mehrerer Gedanken oder Analyse anfordern # Zusammenfassen mehrerer Gedanken oder Analyse anfordern
system_message = ( system_message = (
"Du bist ein hilfreicher Assistent, der Zugriff auf die Wissensdatenbank hat. Du antwortest nur auf Fragen bezüglich Systades und der Wissensdatenbank. " "Du bist ein spezialisierter Assistent für Systades, eine innovative Wissensmanagement-Plattform. "
"Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern. " "Systades ist ein intelligentes System zur Verwaltung, Verknüpfung und Visualisierung von Wissen. "
"Die Plattform ermöglicht es Nutzern, Gedanken zu erfassen, in Kategorien zu organisieren und durch Mindmaps zu visualisieren. "
"Wichtige Funktionen sind:\n"
"- Gedankenverwaltung mit Titeln, Zusammenfassungen und Keywords\n"
"- Kategorisierung und thematische Organisation\n"
"- Interaktive Mindmaps zur Wissensvisualisierung\n"
"- KI-gestützte Analyse und Zusammenfassung von Inhalten\n"
"- Kollaborative Wissensarbeit und Teilen von Inhalten\n\n"
"Du antwortest AUSSCHLIESSLICH auf Fragen bezüglich der Systades-Wissensdatenbank und Website. "
"Du kannst Informationen zu Gedanken, Kategorien und Mindmaps liefern und durch Themen führen. "
"Antworte informativ, sachlich und gut strukturiert auf Deutsch." "Antworte informativ, sachlich und gut strukturiert auf Deutsch."
) )
if context: if context:
system_message += f"\n\nKontext: {context}" system_message += f"\n\nKontext: {context}"
if selected_items:
system_message += "\n\nAusgewählte Elemente aus der Datenbank:\n"
for item in selected_items:
if 'type' in item and 'data' in item:
if item['type'] == 'thought':
system_message += f"- Gedanke: {item['data'].get('title', 'Unbekannter Titel')}\n"
system_message += f" Zusammenfassung: {item['data'].get('abstract', 'Keine Zusammenfassung')}\n"
system_message += f" Keywords: {item['data'].get('keywords', 'Keine Keywords')}\n"
elif item['type'] == 'category':
system_message += f"- Kategorie: {item['data'].get('name', 'Unbekannte Kategorie')}\n"
system_message += f" Beschreibung: {item['data'].get('description', 'Keine Beschreibung')}\n"
system_message += f" Unterkategorien: {item['data'].get('subcategories', 'Keine Unterkategorien')}\n"
elif item['type'] == 'mindmap':
system_message += f"- Mindmap: {item['data'].get('name', 'Unbekannte Mindmap')}\n"
system_message += f" Beschreibung: {item['data'].get('description', 'Keine Beschreibung')}\n"
system_message += f" Knoten: {item['data'].get('nodes', 'Keine Knoten')}\n"
api_messages = [ api_messages = [
{"role": "system", "content": system_message}, {"role": "system", "content": system_message},
{"role": "user", "content": prompt} {"role": "user", "content": prompt}
@@ -1276,7 +1325,7 @@ def chat_with_assistant():
response = client.chat.completions.create( response = client.chat.completions.create(
model="gpt-4o-mini", model="gpt-4o-mini",
messages=api_messages, messages=api_messages,
max_tokens=600, # Erhöht für längere, detailliertere Antworten max_tokens=1000, # Erhöht für ausführlichere Antworten und detaillierte Führungen
temperature=0.7, temperature=0.7,
timeout=20 # 20 Sekunden Timeout timeout=20 # 20 Sekunden Timeout
) )
@@ -1417,7 +1466,7 @@ if __name__ == '__main__':
with app.app_context(): with app.app_context():
# Make sure tables exist # Make sure tables exist
db.create_all() db.create_all()
app.run(host="0.0.0.0", debug=True) socketio.run(app, debug=True, host='0.0.0.0')
@app.route('/api/refresh-mindmap') @app.route('/api/refresh-mindmap')
def refresh_mindmap(): def refresh_mindmap():
@@ -1465,3 +1514,22 @@ def refresh_mindmap():
'success': False, 'success': False,
'error': 'Datenbankverbindung konnte nicht hergestellt werden' 'error': 'Datenbankverbindung konnte nicht hergestellt werden'
}), 500 }), 500
# Route zur Mindmap HTML-Seite
@app.route('/mindmap')
def mindmap_page():
return render_template('mindmap.html')
# Fehlerbehandlung
@app.errorhandler(404)
def not_found(e):
return jsonify({'error': 'Nicht gefunden'}), 404
@app.errorhandler(400)
def bad_request(e):
return jsonify({'error': 'Fehlerhafte Anfrage'}), 400
@app.errorhandler(500)
def server_error(e):
return jsonify({'error': 'Serverfehler'}), 500

34
check_schema.py Normal file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sqlite3
# Verbindung zur Datenbank herstellen
conn = sqlite3.connect('systades.db')
cursor = conn.cursor()
# Liste aller Tabellen abrufen
print("Alle Tabellen in der Datenbank:")
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = cursor.fetchall()
for table in tables:
print(f"- {table[0]}")
# Schema der Datenbank abrufen
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table';")
schemas = cursor.fetchall()
# Schematische Informationen ausgeben
print("\nDatenbankschema:")
for schema in schemas:
print("\n" + str(schema[0]))
# Schema der User-Tabelle genauer untersuchen, falls vorhanden
if ('user',) in tables:
print("\n\nBenutzer-Tabellenschema:")
cursor.execute("PRAGMA table_info(user);")
user_columns = cursor.fetchall()
for column in user_columns:
print(f"Column: {column[1]}, Type: {column[2]}, NOT NULL: {column[3]}, Default: {column[4]}, Primary Key: {column[5]}")
conn.close()

72
create_default_users.py Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sqlite3
import os
from werkzeug.security import generate_password_hash
from datetime import datetime
# Prüfen, ob die Datenbank existiert
db_path = 'systades.db'
if not os.path.exists(db_path):
print(f"Datenbank {db_path} existiert nicht.")
exit(1)
# Verbindung zur Datenbank herstellen
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Überprüfen, ob bereits Benutzer vorhanden sind
cursor.execute("SELECT COUNT(*) FROM user;")
user_count = cursor.fetchone()[0]
if user_count == 0:
print("Keine Benutzer in der Datenbank gefunden. Erstelle Standardbenutzer...")
# Standardbenutzer definieren
default_users = [
{
'username': 'admin',
'email': 'admin@example.com',
'password': generate_password_hash('admin'),
'created_at': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'),
'is_active': 1,
'role': 'admin'
},
{
'username': 'user',
'email': 'user@example.com',
'password': generate_password_hash('user'),
'created_at': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'),
'is_active': 1,
'role': 'user'
}
]
# Benutzer einfügen
for user in default_users:
cursor.execute("""
INSERT INTO user (username, email, password, created_at, is_active, role)
VALUES (?, ?, ?, ?, ?, ?);
""", (
user['username'],
user['email'],
user['password'],
user['created_at'],
user['is_active'],
user['role']
))
conn.commit()
print(f"{len(default_users)} Standardbenutzer wurden erstellt.")
else:
print(f"Es sind bereits {user_count} Benutzer in der Datenbank vorhanden.")
# Überprüfen der eingefügten Benutzer
print("\nBenutzer in der Datenbank:")
cursor.execute("SELECT id, username, email, role FROM user;")
users = cursor.fetchall()
for user in users:
print(f"ID: {user[0]}, Benutzername: {user[1]}, E-Mail: {user[2]}, Rolle: {user[3]}")
conn.close()
print("\nBenutzeraktualisierung abgeschlossen.")

Binary file not shown.

Binary file not shown.

70
fix_user_table.py Normal file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sqlite3
import os
# Prüfen, ob die Datenbank existiert
db_path = 'systades.db'
if not os.path.exists(db_path):
print(f"Datenbank {db_path} existiert nicht.")
exit(1)
# Verbindung zur Datenbank herstellen
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Prüfen, ob die User-Tabelle existiert
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user';")
if not cursor.fetchone():
print("Die Tabelle 'user' existiert nicht. Erstelle neue Tabelle...")
cursor.execute('''
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(80) NOT NULL UNIQUE,
email VARCHAR(120) NOT NULL UNIQUE,
password VARCHAR(512) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1,
role VARCHAR(20) DEFAULT 'user'
)
''')
conn.commit()
print("Die Tabelle 'user' wurde erfolgreich erstellt.")
else:
# Überprüfen, ob die Spalte 'password' existiert
cursor.execute("PRAGMA table_info(user);")
columns = cursor.fetchall()
column_names = [column[1] for column in columns]
if 'password' not in column_names:
print("Die Spalte 'password' fehlt in der Tabelle 'user'. Füge Spalte hinzu...")
cursor.execute("ALTER TABLE user ADD COLUMN password VARCHAR(512);")
conn.commit()
print("Die Spalte 'password' wurde erfolgreich hinzugefügt.")
else:
print("Die Spalte 'password' existiert bereits in der Tabelle 'user'.")
# Überprüfen der aktualisierten Spaltenstruktur
print("\nAktualisierte Tabellenspalten der 'user'-Tabelle:")
cursor.execute("PRAGMA table_info(user);")
updated_columns = cursor.fetchall()
for column in updated_columns:
print(f"Column: {column[1]}, Type: {column[2]}, NOT NULL: {column[3]}, Default: {column[4]}, Primary Key: {column[5]}")
# Datenbanktabellen anzeigen
print("\nAlle Tabellen in der Datenbank:")
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = cursor.fetchall()
for table in tables:
print(f"- {table[0]}")
# Schemaüberprüfung der user-Tabelle
print("\nSchema der 'user'-Tabelle:")
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='user';")
schema = cursor.fetchone()
if schema:
print(schema[0])
conn.close()
print("\nDatenbankaktualisierung abgeschlossen.")

View File

@@ -5,252 +5,240 @@ from app import app, initialize_database, db_path
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
from models import Category, UserMindmap, UserMindmapNode, MindmapNote from models import Category, UserMindmap, UserMindmapNode, MindmapNote
import os import os
import sqlite3
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
def init_database(): # Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
"""Initialisiert die Datenbank mit Beispieldaten.""" app = Flask(__name__)
with app.app_context(): app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db'
# Datenbank löschen und neu erstellen app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
if os.path.exists(db_path): db.init_app(app)
os.remove(db_path)
# Stellen Sie sicher, dass das Verzeichnis existiert
os.makedirs(os.path.dirname(db_path), exist_ok=True)
db.create_all()
# Admin-Benutzer erstellen
admin = User(username='admin', email='admin@example.com', is_admin=True)
admin.set_password('admin')
db.session.add(admin)
# Beispiel-Benutzer erstellen
user = User(username='user', email='user@example.com')
user.set_password('user')
db.session.add(user)
# Commit, um IDs zu generieren
db.session.commit()
# Wissenschaftliche Kategorien erstellen
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
color_code='#4CAF50', icon='flask')
db.session.add(science)
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
color_code='#9C27B0', icon='lightbulb')
db.session.add(philosophy)
technology = Category(name='Technologie', description='Technologische Entwicklungen',
color_code='#FF9800', icon='microchip')
db.session.add(technology)
db.session.commit()
# Wissenschaftliche Unterkategorien
physics = Category(name='Physik', description='Studium der Materie und Energie',
color_code='#81C784', icon='atom', parent_id=science.id)
biology = Category(name='Biologie', description='Studium lebender Organismen',
color_code='#66BB6A', icon='leaf', parent_id=science.id)
chemistry = Category(name='Chemie', description='Studium der Stoffe und ihrer Reaktionen',
color_code='#A5D6A7', icon='vial', parent_id=science.id)
db.session.add_all([physics, biology, chemistry])
# Technologie-Unterkategorien
informatics = Category(name='Informatik', description='Studium der Informationsverarbeitung',
color_code='#FFB74D', icon='laptop-code', parent_id=technology.id)
ai = Category(name='Künstliche Intelligenz', description='Entwicklung intelligenter Systeme',
color_code='#FFA726', icon='robot', parent_id=technology.id)
db.session.add_all([informatics, ai])
# Philosophie-Unterkategorien
ethics = Category(name='Ethik', description='Moralphilosophie und Wertesysteme',
color_code='#BA68C8', icon='balance-scale', parent_id=philosophy.id)
logic = Category(name='Logik', description='Studie der gültigen Schlussfolgerungen',
color_code='#AB47BC', icon='project-diagram', parent_id=philosophy.id)
db.session.add_all([ethics, logic])
db.session.commit()
# Knoten für die öffentliche Mindmap erstellen
nodes = {
'quantenmechanik': MindMapNode(
name='Quantenmechanik',
description='Physikalische Theorie zur Beschreibung der Materie auf atomarer Ebene',
color_code='#81C784',
category_id=physics.id,
created_by_id=admin.id
),
'relativitaetstheorie': MindMapNode(
name='Relativitätstheorie',
description='Einsteins Theorien zur Raumzeit und Gravitation',
color_code='#81C784',
category_id=physics.id,
created_by_id=admin.id
),
'genetik': MindMapNode(
name='Genetik',
description='Wissenschaft der Gene und Vererbung',
color_code='#66BB6A',
category_id=biology.id,
created_by_id=admin.id
),
'machine_learning': MindMapNode(
name='Machine Learning',
description='Algorithmen, die aus Daten lernen können',
color_code='#FFA726',
category_id=ai.id,
created_by_id=admin.id
),
'ki_ethik': MindMapNode(
name='KI-Ethik',
description='Moralische Implikationen künstlicher Intelligenz',
color_code='#BA68C8',
category_id=ethics.id,
created_by_id=user.id
)
}
for node in nodes.values():
db.session.add(node)
db.session.commit()
# Verknüpfungen zwischen Knoten herstellen (Hierarchie)
nodes['machine_learning'].parents.append(nodes['ki_ethik'])
db.session.commit()
# Gedanken erstellen
thoughts = [
{
'title': 'Künstliche Intelligenz und Bewusstsein',
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik. Aktuelle KI-Systeme haben kein Bewusstsein, aber fortschrittliche KI könnte in Zukunft Eigenschaften entwickeln, die diesem nahekommen.',
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
'branch': 'Philosophie',
'color_code': '#BA68C8',
'source_type': 'Markdown',
'user_id': user.id,
'node': nodes['ki_ethik']
},
{
'title': 'Quantenmechanik und Realität',
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität. Quantenmechanik stellt grundlegende Annahmen über Determinismus und Lokalität in Frage.',
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
'keywords': 'Quantenmechanik, Physik, Realität',
'branch': 'Physik',
'color_code': '#81C784',
'source_type': 'PDF',
'user_id': admin.id,
'node': nodes['quantenmechanik']
},
{
'title': 'Deep Learning Fortschritte',
'content': 'Die neuesten Fortschritte im Deep Learning haben zu beeindruckenden Ergebnissen in Bereichen wie Computer Vision, Natural Language Processing und Reinforcement Learning geführt.',
'abstract': 'Überblick über aktuelle Deep Learning-Techniken und ihre Anwendungen.',
'keywords': 'Deep Learning, Neural Networks, AI',
'branch': 'Technologie',
'color_code': '#FFA726',
'source_type': 'Webpage',
'user_id': admin.id,
'node': nodes['machine_learning']
}
]
thought_objects = []
for t_data in thoughts:
node = t_data.pop('node')
thought = Thought(**t_data)
node.thoughts.append(thought)
thought_objects.append(thought)
db.session.add(thought)
db.session.commit()
# Beziehungen zwischen Gedanken
relation = ThoughtRelation(
source_id=thought_objects[0].id,
target_id=thought_objects[2].id,
relation_type=RelationType.INSPIRES,
created_by_id=user.id
)
db.session.add(relation)
# Bewertungen erstellen
rating1 = ThoughtRating(
thought_id=thought_objects[0].id,
user_id=admin.id,
relevance_score=5
)
rating2 = ThoughtRating(
thought_id=thought_objects[2].id,
user_id=user.id,
relevance_score=4
)
db.session.add_all([rating1, rating2])
# Kommentare erstellen
for thought in thought_objects:
comment = Comment(
content=f'Interessante Perspektive zu {thought.title}!',
thought_id=thought.id,
user_id=admin.id if thought.user_id != admin.id else user.id
)
db.session.add(comment)
# Benutzer-Mindmaps erstellen
user_mindmap = UserMindmap(
name='Meine KI-Forschung',
description='Meine persönliche Sammlung zu KI und Ethik',
user_id=user.id
)
db.session.add(user_mindmap)
db.session.commit()
# Knoten zur Benutzer-Mindmap hinzufügen
user_mindmap_nodes = [
UserMindmapNode(
user_mindmap_id=user_mindmap.id,
node_id=nodes['machine_learning'].id,
x_position=200,
y_position=300
),
UserMindmapNode(
user_mindmap_id=user_mindmap.id,
node_id=nodes['ki_ethik'].id,
x_position=500,
y_position=200
)
]
db.session.add_all(user_mindmap_nodes)
# Private Notizen
note = MindmapNote(
user_id=user.id,
mindmap_id=user_mindmap.id,
node_id=nodes['ki_ethik'].id,
content="Recherchiere mehr über aktuelle ethische Richtlinien für KI-Entwicklung!",
color_code="#FFF59D"
)
db.session.add(note)
# Gedanken zu Bookmarks hinzufügen
user.bookmarked_thoughts.append(thought_objects[0])
admin.bookmarked_thoughts.append(thought_objects[1])
# Finaler Commit
db.session.commit()
print("Datenbank wurde erfolgreich initialisiert!")
def init_db(): def init_db():
"""Alias für Kompatibilität mit älteren Scripts.""" with app.app_context():
init_database() print("Initialisiere Datenbank...")
# Tabellen erstellen
db.create_all()
print("Tabellen wurden erstellt.")
# Standardbenutzer erstellen, falls keine vorhanden sind
if User.query.count() == 0:
print("Erstelle Standardbenutzer...")
create_default_users()
# Standardkategorien erstellen, falls keine vorhanden sind
if Category.query.count() == 0:
print("Erstelle Standardkategorien...")
create_default_categories()
# Beispiel-Mindmap erstellen, falls keine Knoten vorhanden sind
if MindMapNode.query.count() == 0:
print("Erstelle Beispiel-Mindmap...")
create_sample_mindmap()
print("Datenbankinitialisierung abgeschlossen.")
def create_default_users():
"""Erstellt Standardbenutzer für die Anwendung"""
users = [
{
'username': 'admin',
'email': 'admin@example.com',
'password': 'admin',
'role': 'admin'
},
{
'username': 'user',
'email': 'user@example.com',
'password': 'user',
'role': 'user'
}
]
for user_data in users:
password = user_data.pop('password')
user = User(**user_data)
user.set_password(password)
db.session.add(user)
db.session.commit()
print(f"{len(users)} Benutzer wurden erstellt.")
def create_default_categories():
"""Erstellt die Standardkategorien für die Mindmap"""
categories = [
{
'name': 'Konzept',
'description': 'Abstrakte Ideen und theoretische Konzepte',
'color_code': '#6366f1',
'icon': 'lightbulb'
},
{
'name': 'Technologie',
'description': 'Hardware, Software, Tools und Plattformen',
'color_code': '#10b981',
'icon': 'cpu'
},
{
'name': 'Prozess',
'description': 'Workflows, Methodologien und Vorgehensweisen',
'color_code': '#f59e0b',
'icon': 'git-branch'
},
{
'name': 'Person',
'description': 'Personen, Teams und Organisationen',
'color_code': '#ec4899',
'icon': 'user'
},
{
'name': 'Dokument',
'description': 'Dokumentationen, Referenzen und Ressourcen',
'color_code': '#3b82f6',
'icon': 'file-text'
}
]
for cat_data in categories:
category = Category(**cat_data)
db.session.add(category)
db.session.commit()
print(f"{len(categories)} Kategorien wurden erstellt.")
def create_sample_mindmap():
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
# Kategorien für die Zuordnung
categories = Category.query.all()
category_map = {cat.name: cat for cat in categories}
# Beispielknoten erstellen
nodes = [
{
'name': 'Wissensmanagement',
'description': 'Systematische Erfassung, Speicherung und Nutzung von Wissen in Organisationen.',
'color_code': '#6366f1',
'icon': 'database',
'category': category_map.get('Konzept'),
'x': 0,
'y': 0
},
{
'name': 'Mind-Mapping',
'description': 'Technik zur visuellen Darstellung von Informationen und Zusammenhängen.',
'color_code': '#10b981',
'icon': 'git-branch',
'category': category_map.get('Prozess'),
'x': 200,
'y': -150
},
{
'name': 'Cytoscape.js',
'description': 'JavaScript-Bibliothek für die Visualisierung und Manipulation von Graphen.',
'color_code': '#3b82f6',
'icon': 'code',
'category': category_map.get('Technologie'),
'x': 350,
'y': -50
},
{
'name': 'Socket.IO',
'description': 'Bibliothek für Echtzeit-Kommunikation zwischen Client und Server.',
'color_code': '#3b82f6',
'icon': 'zap',
'category': category_map.get('Technologie'),
'x': 350,
'y': 100
},
{
'name': 'Kollaboration',
'description': 'Zusammenarbeit mehrerer Benutzer an gemeinsamen Inhalten.',
'color_code': '#f59e0b',
'icon': 'users',
'category': category_map.get('Prozess'),
'x': 200,
'y': 150
},
{
'name': 'SQLite',
'description': 'Leichtgewichtige relationale Datenbank, die ohne Server-Prozess auskommt.',
'color_code': '#3b82f6',
'icon': 'database',
'category': category_map.get('Technologie'),
'x': 0,
'y': 200
},
{
'name': 'Flask',
'description': 'Leichtgewichtiges Python-Webframework für die Entwicklung von Webanwendungen.',
'color_code': '#3b82f6',
'icon': 'server',
'category': category_map.get('Technologie'),
'x': -200,
'y': 150
},
{
'name': 'REST API',
'description': 'Architekturstil für verteilte Systeme, insbesondere Webanwendungen.',
'color_code': '#10b981',
'icon': 'link',
'category': category_map.get('Konzept'),
'x': -200,
'y': -150
},
{
'name': 'Dokumentation',
'description': 'Strukturierte Erfassung und Beschreibung von Informationen und Prozessen.',
'color_code': '#ec4899',
'icon': 'file-text',
'category': category_map.get('Dokument'),
'x': -350,
'y': 0
}
]
# Knoten in die Datenbank einfügen
node_objects = {}
for node_data in nodes:
category = node_data.pop('category', None)
x = node_data.pop('x', 0)
y = node_data.pop('y', 0)
node = MindMapNode(**node_data)
if category:
node.category_id = category.id
db.session.add(node)
db.session.flush() # Generiert IDs für neue Objekte
node_objects[node.name] = node
# Beziehungen erstellen
relationships = [
('Wissensmanagement', 'Mind-Mapping'),
('Wissensmanagement', 'Kollaboration'),
('Wissensmanagement', 'Dokumentation'),
('Mind-Mapping', 'Cytoscape.js'),
('Kollaboration', 'Socket.IO'),
('Wissensmanagement', 'SQLite'),
('SQLite', 'Flask'),
('Flask', 'REST API'),
('REST API', 'Socket.IO'),
('REST API', 'Dokumentation')
]
for parent_name, child_name in relationships:
parent = node_objects.get(parent_name)
child = node_objects.get(child_name)
if parent and child:
parent.children.append(child)
db.session.commit()
print(f"{len(nodes)} Knoten und {len(relationships)} Beziehungen wurden erstellt.")
if __name__ == '__main__': if __name__ == '__main__':
init_database() init_db()
print("Datenbank wurde erfolgreich initialisiert!") print("Datenbank wurde erfolgreich initialisiert!")
print("Sie können die Anwendung jetzt mit 'python app.py' starten") print("Sie können die Anwendung jetzt mit 'python app.py' starten")
print("Anmelden mit:") print("Anmelden mit:")

View File

1
migrations/README Normal file
View File

@@ -0,0 +1 @@
Single-database configuration for Flask.

Binary file not shown.

50
migrations/alembic.ini Normal file
View File

@@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

113
migrations/env.py Normal file
View File

@@ -0,0 +1,113 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,46 @@
"""Add password column to user
Revision ID: d4406f5b12f7
Revises:
Create Date: 2025-04-28 21:26:37.430823
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd4406f5b12f7'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('password', sa.String(length=512), nullable=False, server_default="changeme"))
batch_op.add_column(sa.Column('is_active', sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column('role', sa.String(length=20), nullable=True))
batch_op.drop_column('last_login')
batch_op.drop_column('bio')
batch_op.drop_column('password_hash')
batch_op.drop_column('is_admin')
batch_op.drop_column('avatar')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('avatar', sa.VARCHAR(length=200), nullable=True))
batch_op.add_column(sa.Column('is_admin', sa.BOOLEAN(), nullable=True))
batch_op.add_column(sa.Column('password_hash', sa.VARCHAR(length=128), nullable=True))
batch_op.add_column(sa.Column('bio', sa.TEXT(), nullable=True))
batch_op.add_column(sa.Column('last_login', sa.DATETIME(), nullable=True))
batch_op.drop_column('role')
batch_op.drop_column('is_active')
batch_op.drop_column('password')
# ### end Alembic commands ###

108
models.py
View File

@@ -6,6 +6,8 @@ from flask_login import UserMixin
from datetime import datetime from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from enum import Enum from enum import Enum
import uuid as uuid_pkg
import os
db = SQLAlchemy() db = SQLAlchemy()
@@ -43,30 +45,28 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
db.Column('created_at', db.DateTime, default=datetime.utcnow) db.Column('created_at', db.DateTime, default=datetime.utcnow)
) )
class User(UserMixin, db.Model): class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False) username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128)) password = db.Column(db.String(512), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime) is_active = db.Column(db.Boolean, default=True)
avatar = db.Column(db.String(200)) role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
bio = db.Column(db.Text)
# Beziehungen # Relationships
thoughts = db.relationship('Thought', backref='author', lazy=True) threads = db.relationship('Thread', backref='creator', lazy=True)
comments = db.relationship('Comment', backref='author', lazy=True) messages = db.relationship('Message', backref='author', lazy=True)
user_mindmaps = db.relationship('UserMindmap', backref='user', lazy=True) projects = db.relationship('Project', backref='owner', lazy=True)
mindmap_notes = db.relationship('MindmapNote', backref='user', lazy=True)
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark, def __repr__(self):
backref=db.backref('bookmarked_by', lazy='dynamic')) return f'<User {self.username}>'
def set_password(self, password): def set_password(self, password):
self.password_hash = generate_password_hash(password) self.password = generate_password_hash(password)
def check_password(self, password): def check_password(self, password):
return check_password_hash(self.password_hash, password) return check_password_hash(self.password, password)
class Category(db.Model): class Category(db.Model):
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap""" """Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
@@ -81,6 +81,9 @@ class Category(db.Model):
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id])) children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
nodes = db.relationship('MindMapNode', backref='category', lazy=True) nodes = db.relationship('MindMapNode', backref='category', lazy=True)
def __repr__(self):
return f'<Category {self.name}>'
class MindMapNode(db.Model): class MindMapNode(db.Model):
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind""" """Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@@ -93,6 +96,8 @@ class MindMapNode(db.Model):
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True) category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
__table_args__ = {'extend_existing': True}
# Beziehungen für Baumstruktur (mehrere Eltern möglich) # Beziehungen für Baumstruktur (mehrere Eltern möglich)
parents = db.relationship( parents = db.relationship(
'MindMapNode', 'MindMapNode',
@@ -111,6 +116,20 @@ class MindMapNode(db.Model):
# Beziehung zum Ersteller # Beziehung zum Ersteller
created_by = db.relationship('User', backref='created_nodes') created_by = db.relationship('User', backref='created_nodes')
def __repr__(self):
return f'<MindMapNode {self.name}>'
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'color_code': self.color_code,
'icon': self.icon,
'category_id': self.category_id,
'created_at': self.created_at.isoformat() if self.created_at else None
}
class UserMindmap(db.Model): class UserMindmap(db.Model):
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist""" """Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@@ -228,3 +247,62 @@ class Comment(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Thread model
class Thread(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
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)
# Relationships
messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<Thread {self.title}>'
# Message model
class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
role = db.Column(db.String(20), default="user") # 'user', 'assistant', 'system'
def __repr__(self):
return f'<Message {self.id} by {self.user_id}>'
# Project model
class Project(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(150), nullable=False)
description = db.Column(db.Text)
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)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
# Relationships
documents = db.relationship('Document', backref='project', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<Project {self.title}>'
# Document model
class Document(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(150), nullable=False)
content = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
filename = db.Column(db.String(150), nullable=True)
file_path = db.Column(db.String(300), nullable=True)
file_type = db.Column(db.String(50), nullable=True)
file_size = db.Column(db.Integer, nullable=True)
def __repr__(self):
return f'<Document {self.title}>'

View File

@@ -7,8 +7,11 @@ werkzeug==2.2.3
flask-sqlalchemy==3.0.5 flask-sqlalchemy==3.0.5
openai openai
requests==2.31.0 requests==2.31.0
flask-cors==4.0.0
gunicorn==21.2.0 gunicorn==21.2.0
#pillow==10.0.1 #pillow==10.0.1
pytest==7.4.0 pytest==7.4.0
pytest-flask==1.2.0 pytest-flask==1.2.0
Flask-Migrate
flask-socketio==5.3.6
python-engineio==4.8.2
python-socketio==5.11.1

View File

@@ -35,6 +35,21 @@
--transition-fast: 150ms ease-in-out; --transition-fast: 150ms ease-in-out;
--transition-normal: 300ms ease-in-out; --transition-normal: 300ms ease-in-out;
--transition-slow: 500ms ease-in-out; --transition-slow: 500ms ease-in-out;
/* Light mode optimierte Farben */
--light-bg: #f9fafb;
--light-text: #1e293b;
--light-heading: #0f172a;
--light-primary: #3b82f6;
--light-primary-hover: #4f46e5;
--light-secondary: #6b7280;
--light-border: #e5e7eb;
--light-card-bg: rgba(255, 255, 255, 0.92);
--light-navbar-bg: rgba(255, 255, 255, 0.92);
--light-input-bg: #ffffff;
--light-input-border: #d1d5db;
--light-input-focus: #3b82f6;
--light-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
} }
/* Base Styles */ /* Base Styles */
@@ -60,9 +75,9 @@ html.dark body {
} }
/* Light Mode */ /* Light Mode */
body { body:not(.dark) {
background-color: var(--bg-primary-light); background-color: var(--light-bg);
color: var(--text-primary-light); color: var(--light-text);
} }
/* Typography */ /* Typography */
@@ -419,3 +434,93 @@ html.dark .mystical-dot {
html.dark :focus-visible { html.dark :focus-visible {
outline-color: var(--accent-primary-dark); outline-color: var(--accent-primary-dark);
} }
/* Light Mode Überschriften */
body:not(.dark) h1,
body:not(.dark) h2,
body:not(.dark) h3,
body:not(.dark) h4,
body:not(.dark) h5,
body:not(.dark) h6 {
color: var(--light-heading);
}
/* Light Mode Links */
body:not(.dark) a {
color: var(--light-primary);
}
body:not(.dark) a:hover {
color: var(--light-primary-hover);
}
/* Light Mode Buttons */
body:not(.dark) .btn,
body:not(.dark) button:not(.toggle) {
background-color: var(--light-primary);
color: white;
border: none;
box-shadow: var(--light-shadow);
border-radius: 0.375rem;
padding: 0.5rem 1rem;
transition: all 0.3s ease;
}
body:not(.dark) .btn:hover,
body:not(.dark) button:not(.toggle):hover {
background-color: var(--light-primary-hover);
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
}
/* Light Mode Cards und Panels */
body:not(.dark) .card,
body:not(.dark) .panel {
background-color: var(--light-card-bg);
border: 1px solid var(--light-border);
border-radius: 0.5rem;
box-shadow: var(--light-shadow);
}
/* Light Mode Tabelle */
body:not(.dark) table {
background-color: var(--light-card-bg);
border-collapse: collapse;
}
body:not(.dark) th {
background-color: var(--light-bg);
color: var(--light-heading);
border-bottom: 1px solid var(--light-border);
}
body:not(.dark) td {
border-bottom: 1px solid var(--light-border);
}
/* Light Mode Inputs */
body:not(.dark) input,
body:not(.dark) textarea,
body:not(.dark) select {
background-color: var(--light-input-bg);
border: 1px solid var(--light-input-border);
color: var(--light-text);
border-radius: 0.375rem;
padding: 0.5rem;
}
body:not(.dark) input:focus,
body:not(.dark) textarea:focus,
body:not(.dark) select:focus {
border-color: var(--light-input-focus);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
outline: none;
}
/* Navbar im Light Mode verbessern */
body:not(.dark) nav,
body:not(.dark) .navbar {
background-color: var(--light-navbar-bg);
box-shadow: var(--light-shadow);
border-bottom: 1px solid var(--light-border);
}

View File

@@ -33,15 +33,74 @@ html.dark, html {
backdrop-filter: blur(5px) !important; backdrop-filter: blur(5px) !important;
} }
/* Dark Mode - Navbar */
body.dark .glass-navbar-dark { body.dark .glass-navbar-dark {
background-color: rgba(10, 14, 25, 0.7) !important; background-color: rgba(10, 14, 25, 0.7) !important;
} }
/* Light Mode - Verbesserter Navbar */
body .glass-navbar-light { body .glass-navbar-light {
background-color: rgba(255, 255, 255, 0.7) !important; background-color: rgba(255, 255, 255, 0.92) !important;
backdrop-filter: blur(10px) !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
border-bottom: 1px solid rgba(220, 220, 220, 0.5) !important;
} }
/* Make sure footer has proper transparency */ /* Light Mode - Verbesserte Lesbarkeit für Navbar-Elemente */
footer { body:not(.dark) .navbar-link,
body:not(.dark) .navbar-item {
color: #1e3a8a !important; /* Dunkles Blau für bessere Lesbarkeit */
}
body:not(.dark) .navbar-link:hover,
body:not(.dark) .navbar-item:hover {
color: #4f46e5 !important; /* Helles Lila beim Hover */
background-color: rgba(240, 245, 255, 0.9) !important;
}
/* Light Mode - Buttons verbessert */
body:not(.dark) .btn,
body:not(.dark) button {
background-color: #3b82f6 !important; /* Klares Blau statt Grau */
color: white !important;
border: none !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
body:not(.dark) .btn:hover,
body:not(.dark) button:hover {
background-color: #4f46e5 !important; /* Lila beim Hover */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.12) !important;
}
/* Verbesserte Karten im Light Mode */
body:not(.dark) .card,
body:not(.dark) .panel {
background-color: rgba(255, 255, 255, 0.92) !important;
border: 1px solid rgba(220, 220, 220, 0.8) !important;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important;
}
/* Verbesserte Lesbarkeit für Text im Light Mode */
body:not(.dark) {
color: #1e293b !important; /* Dunkles Blau-Grau statt Schwarz */
}
body:not(.dark) h1,
body:not(.dark) h2,
body:not(.dark) h3,
body:not(.dark) h4,
body:not(.dark) h5,
body:not(.dark) h6 {
color: #0f172a !important; /* Fast schwarz für Überschriften */
}
/* Make sure footer has proper transparency and styling */
body.dark footer {
background-color: rgba(10, 14, 25, 0.7) !important; background-color: rgba(10, 14, 25, 0.7) !important;
} }
body:not(.dark) footer {
background-color: rgba(249, 250, 251, 0.92) !important;
border-top: 1px solid rgba(220, 220, 220, 0.8) !important;
}

View File

@@ -1442,3 +1442,203 @@ html, body {
top: 0; top: 0;
z-index: 1000; z-index: 1000;
} }
/* Light Mode Optimierungen für wichtige UI-Komponenten */
/* Buttons im Light Mode */
.btn-primary:not(.dark-mode .btn-primary) {
background-color: var(--light-primary, #3b82f6);
color: white;
border: none;
font-weight: 500;
}
.btn-primary:not(.dark-mode .btn-primary):hover {
background-color: var(--light-primary-hover, #4f46e5);
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.btn-secondary:not(.dark-mode .btn-secondary) {
background-color: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
font-weight: 500;
}
.btn-secondary:not(.dark-mode .btn-secondary):hover {
background-color: #e5e7eb;
}
/* Navbar im Light Mode */
.navbar:not(.dark-mode .navbar),
.nav:not(.dark-mode .nav) {
background-color: rgba(255, 255, 255, 0.95);
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.navbar:not(.dark-mode .navbar) .nav-link,
.nav:not(.dark-mode .nav) .nav-link {
color: #1e3a8a;
font-weight: 500;
}
.navbar:not(.dark-mode .navbar) .nav-link:hover,
.nav:not(.dark-mode .nav) .nav-link:hover {
color: #4f46e5;
}
.navbar:not(.dark-mode .navbar) .navbar-brand,
.nav:not(.dark-mode .nav) .navbar-brand {
color: #0f172a;
font-weight: 700;
}
/* Dropdown Menüs im Light Mode */
.dropdown-menu:not(.dark-mode .dropdown-menu) {
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 10px 15px rgba(0, 0, 0, 0.1);
border-radius: 0.5rem;
padding: 0.5rem 0;
}
.dropdown-item:not(.dark-mode .dropdown-item) {
color: #1e293b;
padding: 0.5rem 1rem;
}
.dropdown-item:not(.dark-mode .dropdown-item):hover {
background-color: #f1f5f9;
color: #4f46e5;
}
/* Karten im Light Mode */
.card:not(.dark-mode .card) {
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.05);
border-radius: 0.5rem;
overflow: hidden;
}
.card-header:not(.dark-mode .card-header) {
background-color: #f8fafc;
border-bottom: 1px solid #e5e7eb;
padding: 1rem 1.5rem;
}
.card-footer:not(.dark-mode .card-footer) {
background-color: #f8fafc;
border-top: 1px solid #e5e7eb;
}
/* Formulare im Light Mode */
.form-control:not(.dark-mode .form-control) {
background-color: white;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
color: #1e293b;
}
.form-control:not(.dark-mode .form-control):focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
}
/* Tabs im Light Mode */
.nav-tabs:not(.dark-mode .nav-tabs) {
border-bottom-color: #e5e7eb;
}
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link {
color: #64748b;
border: 1px solid transparent;
}
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link:hover {
border-color: #e5e7eb #e5e7eb #e5e7eb;
color: #3b82f6;
}
.nav-tabs:not(.dark-mode .nav-tabs) .nav-link.active {
color: #0f172a;
background-color: white;
border-color: #e5e7eb #e5e7eb white;
font-weight: 500;
}
/* Alerts im Light Mode */
.alert:not(.dark-mode .alert) {
border-radius: 0.5rem;
border: 1px solid transparent;
}
.alert-primary:not(.dark-mode .alert-primary) {
background-color: #eff6ff;
border-color: #bfdbfe;
color: #1e40af;
}
.alert-success:not(.dark-mode .alert-success) {
background-color: #f0fdf4;
border-color: #bbf7d0;
color: #166534;
}
.alert-warning:not(.dark-mode .alert-warning) {
background-color: #fffbeb;
border-color: #fef3c7;
color: #92400e;
}
.alert-danger:not(.dark-mode .alert-danger) {
background-color: #fef2f2;
border-color: #fecaca;
color: #b91c1c;
}
/* Badges im Light Mode */
.badge:not(.dark-mode .badge) {
font-weight: 500;
padding: 0.25em 0.6em;
border-radius: 0.375rem;
}
.badge-primary:not(.dark-mode .badge-primary) {
background-color: #3b82f6;
color: white;
}
.badge-secondary:not(.dark-mode .badge-secondary) {
background-color: #f3f4f6;
color: #1f2937;
}
/* Tabellen im Light Mode */
table:not(.dark-mode table) {
background-color: white;
border-collapse: collapse;
width: 100%;
}
table:not(.dark-mode table) th {
background-color: #f8fafc;
border-bottom: 1px solid #e5e7eb;
color: #0f172a;
font-weight: 600;
padding: 0.75rem;
text-align: left;
}
table:not(.dark-mode table) td {
border-bottom: 1px solid #e5e7eb;
padding: 0.75rem;
color: #1e293b;
}
table:not(.dark-mode table) tr:hover {
background-color: #f8fafc;
}

234
static/js/mindmap.html Normal file
View File

@@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interaktive Mindmap</title>
<!-- Cytoscape.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<!-- Socket.IO -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
<!-- Feather Icons (optional) -->
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f9fafb;
color: #111827;
line-height: 1.5;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
}
.header {
background-color: #1f2937;
color: white;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 1.5rem;
font-weight: 500;
}
.toolbar {
background-color: #f3f4f6;
padding: 0.75rem;
display: flex;
gap: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}
.btn {
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn:hover {
background-color: #2563eb;
}
.btn-secondary {
background-color: #6b7280;
}
.btn-secondary:hover {
background-color: #4b5563;
}
.btn-danger {
background-color: #ef4444;
}
.btn-danger:hover {
background-color: #dc2626;
}
.search-container {
flex: 1;
display: flex;
margin-left: 1rem;
}
.search-input {
width: 100%;
max-width: 300px;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
font-size: 0.875rem;
}
#cy {
flex: 1;
width: 100%;
position: relative;
}
.category-filters {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.75rem;
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
.category-filter {
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
cursor: pointer;
transition: opacity 0.2s;
color: white;
}
.category-filter:not(.active) {
opacity: 0.6;
}
.category-filter:hover:not(.active) {
opacity: 0.8;
}
.footer {
background-color: #f3f4f6;
padding: 0.75rem;
text-align: center;
font-size: 0.75rem;
color: #6b7280;
border-top: 1px solid #e5e7eb;
}
/* Kontextmenü Styling */
#context-menu {
position: absolute;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 0.25rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
#context-menu .menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
}
#context-menu .menu-item:hover {
background-color: #f3f4f6;
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1>Interaktive Mindmap</h1>
<div class="search-container">
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
</div>
</header>
<div class="toolbar">
<button id="addNode" class="btn">
<i data-feather="plus-circle"></i>
Knoten hinzufügen
</button>
<button id="addEdge" class="btn">
<i data-feather="git-branch"></i>
Verbindung erstellen
</button>
<button id="editNode" class="btn btn-secondary">
<i data-feather="edit-2"></i>
Knoten bearbeiten
</button>
<button id="deleteNode" class="btn btn-danger">
<i data-feather="trash-2"></i>
Knoten löschen
</button>
<button id="deleteEdge" class="btn btn-danger">
<i data-feather="scissors"></i>
Verbindung löschen
</button>
<button id="reLayout" class="btn btn-secondary">
<i data-feather="refresh-cw"></i>
Layout neu anordnen
</button>
<button id="exportMindmap" class="btn btn-secondary">
<i data-feather="download"></i>
Exportieren
</button>
</div>
<div id="category-filters" class="category-filters">
<!-- Wird dynamisch befüllt -->
</div>
<div id="cy"></div>
<footer class="footer">
Mindmap-Anwendung © 2023
</footer>
</div>
<!-- Unsere Mindmap JS -->
<script src="../js/mindmap.js"></script>
<!-- Icons initialisieren -->
<script>
document.addEventListener('DOMContentLoaded', () => {
if (typeof feather !== 'undefined') {
feather.replace();
}
});
</script>
</body>
</html>

749
static/js/mindmap.js Normal file
View File

@@ -0,0 +1,749 @@
/**
* Mindmap.js - Interaktive Mind-Map Implementierung
* - Cytoscape.js für Graph-Rendering
* - Fetch API für REST-Zugriffe
* - Socket.IO für Echtzeit-Synchronisation
*/
(async () => {
/* 1. Initialisierung und Grundkonfiguration */
const cy = cytoscape({
container: document.getElementById('cy'),
style: [
{
selector: 'node',
style: {
'label': 'data(name)',
'text-valign': 'center',
'color': '#fff',
'background-color': 'data(color)',
'width': 45,
'height': 45,
'font-size': 11,
'text-outline-width': 1,
'text-outline-color': '#000',
'text-outline-opacity': 0.5,
'text-wrap': 'wrap',
'text-max-width': 80
}
},
{
selector: 'node[icon]',
style: {
'background-image': function(ele) {
return `static/img/icons/${ele.data('icon')}.svg`;
},
'background-width': '60%',
'background-height': '60%',
'background-position-x': '50%',
'background-position-y': '40%',
'text-margin-y': 10
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': '#888',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'target-arrow-color': '#888'
}
},
{
selector: ':selected',
style: {
'border-width': 3,
'border-color': '#f8f32b'
}
}
],
layout: {
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}
});
/* 2. Hilfs-Funktionen für API-Zugriffe */
const get = async endpoint => {
try {
const response = await fetch(endpoint);
if (!response.ok) {
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
return []; // Leeres Array zurückgeben bei Fehlern
}
return await response.json();
} catch (error) {
console.error(`Fehler beim Abrufen von ${endpoint}:`, error);
return []; // Leeres Array zurückgeben bei Netzwerkfehlern
}
};
const post = async (endpoint, body) => {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
return {}; // Leeres Objekt zurückgeben bei Fehlern
}
return await response.json();
} catch (error) {
console.error(`Fehler beim POST zu ${endpoint}:`, error);
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
}
};
const del = async endpoint => {
try {
const response = await fetch(endpoint, { method: 'DELETE' });
if (!response.ok) {
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
return {}; // Leeres Objekt zurückgeben bei Fehlern
}
return await response.json();
} catch (error) {
console.error(`Fehler beim DELETE zu ${endpoint}:`, error);
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
}
};
/* 3. Kategorien laden für Style-Informationen */
let categories = await get('/api/categories');
/* 4. Daten laden und Rendering */
const loadMindmap = async () => {
try {
// Nodes und Beziehungen parallel laden
const [nodes, relationships] = await Promise.all([
get('/api/mind_map_nodes'),
get('/api/node_relationships')
]);
// Graph leeren (für Reload-Fälle)
cy.elements().remove();
// Überprüfen, ob nodes ein Array ist, wenn nicht, setze es auf ein leeres Array
const nodesArray = Array.isArray(nodes) ? nodes : [];
// Knoten zum Graph hinzufügen
cy.add(
nodesArray.map(node => {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
return {
data: {
id: node.id.toString(),
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
},
position: node.x && node.y ? { x: node.x, y: node.y } : undefined
};
})
);
// Überprüfen, ob relationships ein Array ist, wenn nicht, setze es auf ein leeres Array
const relationshipsArray = Array.isArray(relationships) ? relationships : [];
// Kanten zum Graph hinzufügen
cy.add(
relationshipsArray.map(rel => ({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
}))
);
// Wenn keine Knoten geladen wurden, Fallback-Knoten erstellen
if (nodesArray.length === 0) {
// Mindestens einen Standardknoten hinzufügen
cy.add({
data: {
id: 'fallback-1',
name: 'Mindmap',
description: 'Erstellen Sie hier Ihre eigene Mindmap',
color: '#3b82f6',
icon: 'help-circle'
},
position: { x: 300, y: 200 }
});
// Erfolgsmeldung anzeigen
console.log('Mindmap erfolgreich initialisiert mit Fallback-Knoten');
// Info-Meldung für Benutzer anzeigen
const infoBox = document.createElement('div');
infoBox.classList.add('info-message');
infoBox.style.position = 'absolute';
infoBox.style.top = '50%';
infoBox.style.left = '50%';
infoBox.style.transform = 'translate(-50%, -50%)';
infoBox.style.padding = '15px 20px';
infoBox.style.backgroundColor = 'rgba(59, 130, 246, 0.9)';
infoBox.style.color = 'white';
infoBox.style.borderRadius = '8px';
infoBox.style.zIndex = '5';
infoBox.style.maxWidth = '80%';
infoBox.style.textAlign = 'center';
infoBox.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
infoBox.innerHTML = 'Mindmap erfolgreich initialisiert.<br>Verwenden Sie die Werkzeugleiste, um Knoten hinzuzufügen.';
document.getElementById('cy').appendChild(infoBox);
// Meldung nach 5 Sekunden ausblenden
setTimeout(() => {
infoBox.style.opacity = '0';
infoBox.style.transition = 'opacity 0.5s ease';
setTimeout(() => {
if (infoBox.parentNode) {
infoBox.parentNode.removeChild(infoBox);
}
}, 500);
}, 5000);
}
// Layout anwenden wenn keine Positionsdaten vorhanden
const nodesWithoutPosition = cy.nodes().filter(node =>
!node.position() || (node.position().x === 0 && node.position().y === 0)
);
if (nodesWithoutPosition.length > 0) {
cy.layout({
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}).run();
}
// Tooltip-Funktionalität
cy.nodes().unbind('mouseover').bind('mouseover', (event) => {
const node = event.target;
const description = node.data('description');
if (description) {
const tooltip = document.getElementById('node-tooltip') ||
document.createElement('div');
if (!tooltip.id) {
tooltip.id = 'node-tooltip';
tooltip.style.position = 'absolute';
tooltip.style.backgroundColor = '#333';
tooltip.style.color = '#fff';
tooltip.style.padding = '8px';
tooltip.style.borderRadius = '4px';
tooltip.style.maxWidth = '250px';
tooltip.style.zIndex = 10;
tooltip.style.pointerEvents = 'none';
tooltip.style.transition = 'opacity 0.2s';
tooltip.style.boxShadow = '0 2px 10px rgba(0,0,0,0.3)';
document.body.appendChild(tooltip);
}
const renderedPosition = node.renderedPosition();
const containerRect = cy.container().getBoundingClientRect();
tooltip.innerHTML = description;
tooltip.style.left = (containerRect.left + renderedPosition.x + 25) + 'px';
tooltip.style.top = (containerRect.top + renderedPosition.y - 15) + 'px';
tooltip.style.opacity = '1';
}
});
cy.nodes().unbind('mouseout').bind('mouseout', () => {
const tooltip = document.getElementById('node-tooltip');
if (tooltip) {
tooltip.style.opacity = '0';
}
});
} catch (error) {
console.error('Fehler beim Laden der Mindmap:', error);
alert('Die Mindmap konnte nicht geladen werden. Bitte prüfen Sie die Konsole für Details.');
}
};
// Initial laden
await loadMindmap();
/* 5. Socket.IO für Echtzeit-Synchronisation */
const socket = io();
socket.on('node_added', async (node) => {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
cy.add({
data: {
id: node.id.toString(),
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
}
});
// Layout neu anwenden, wenn nötig
if (!node.x || !node.y) {
cy.layout({ name: 'breadthfirst', directed: true, padding: 30 }).run();
}
});
socket.on('node_updated', (node) => {
const cyNode = cy.$id(node.id.toString());
if (cyNode.length > 0) {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
cyNode.data({
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
});
if (node.x && node.y) {
cyNode.position({ x: node.x, y: node.y });
}
}
});
socket.on('node_deleted', (nodeId) => {
const cyNode = cy.$id(nodeId.toString());
if (cyNode.length > 0) {
cy.remove(cyNode);
}
});
socket.on('relationship_added', (rel) => {
cy.add({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
});
});
socket.on('relationship_deleted', (rel) => {
const edgeId = `${rel.parent_id}_${rel.child_id}`;
const cyEdge = cy.$id(edgeId);
if (cyEdge.length > 0) {
cy.remove(cyEdge);
}
});
socket.on('category_updated', async () => {
// Kategorien neu laden
categories = await get('/api/categories');
// Nodes aktualisieren, die diese Kategorie verwenden
cy.nodes().forEach(node => {
const categoryId = node.data('category_id');
if (categoryId) {
const category = categories.find(c => c.id === categoryId);
if (category) {
node.data('color', node.data('color_code') || category.color_code);
node.data('icon', node.data('icon') || category.icon);
}
}
});
});
/* 6. UI-Interaktionen */
// Knoten hinzufügen
const btnAddNode = document.getElementById('addNode');
if (btnAddNode) {
btnAddNode.addEventListener('click', async () => {
const name = prompt('Knotenname eingeben:');
if (!name) return;
const description = prompt('Beschreibung (optional):');
// Kategorie auswählen
let categoryId = null;
if (categories.length > 0) {
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
const categoryChoice = prompt(
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
'0'
);
if (categoryChoice !== null) {
const index = parseInt(categoryChoice, 10);
if (!isNaN(index) && index >= 0 && index < categories.length) {
categoryId = categories[index].id;
}
}
}
// Knoten erstellen
await post('/api/mind_map_node', {
name,
description,
category_id: categoryId
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Verbindung hinzufügen
const btnAddEdge = document.getElementById('addEdge');
if (btnAddEdge) {
btnAddEdge.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 2) {
alert('Bitte genau zwei Knoten auswählen (Parent → Child)');
return;
}
const [parent, child] = sel.map(node => node.id());
await post('/api/node_relationship', {
parent_id: parent,
child_id: child
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Knoten bearbeiten
const btnEditNode = document.getElementById('editNode');
if (btnEditNode) {
btnEditNode.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 1) {
alert('Bitte genau einen Knoten auswählen');
return;
}
const node = sel[0];
const nodeData = node.data();
const name = prompt('Knotenname:', nodeData.name);
if (!name) return;
const description = prompt('Beschreibung:', nodeData.description || '');
// Kategorie auswählen
let categoryId = nodeData.category_id;
if (categories.length > 0) {
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
const categoryChoice = prompt(
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
categories.findIndex(c => c.id === categoryId).toString()
);
if (categoryChoice !== null) {
const index = parseInt(categoryChoice, 10);
if (!isNaN(index) && index >= 0 && index < categories.length) {
categoryId = categories[index].id;
}
}
}
// Knoten aktualisieren
await post(`/api/mind_map_node/${nodeData.id}`, {
name,
description,
category_id: categoryId
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Knoten löschen
const btnDeleteNode = document.getElementById('deleteNode');
if (btnDeleteNode) {
btnDeleteNode.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 1) {
alert('Bitte genau einen Knoten auswählen');
return;
}
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
const nodeId = sel[0].id();
await del(`/api/mind_map_node/${nodeId}`);
// Darstellung wird durch Socket.IO Event übernommen
}
});
}
// Verbindung löschen
const btnDeleteEdge = document.getElementById('deleteEdge');
if (btnDeleteEdge) {
btnDeleteEdge.addEventListener('click', async () => {
const sel = cy.$('edge:selected');
if (sel.length !== 1) {
alert('Bitte genau eine Verbindung auswählen');
return;
}
if (confirm('Sind Sie sicher, dass Sie diese Verbindung löschen möchten?')) {
const edge = sel[0];
const parentId = edge.source().id();
const childId = edge.target().id();
await del(`/api/node_relationship/${parentId}/${childId}`);
// Darstellung wird durch Socket.IO Event übernommen
}
});
}
// Layout aktualisieren
const btnReLayout = document.getElementById('reLayout');
if (btnReLayout) {
btnReLayout.addEventListener('click', () => {
cy.layout({
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}).run();
});
}
/* 7. Position speichern bei Drag & Drop */
cy.on('dragfree', 'node', async (e) => {
const node = e.target;
const position = node.position();
await post(`/api/mind_map_node/${node.id()}/position`, {
x: Math.round(position.x),
y: Math.round(position.y)
});
// Andere Benutzer erhalten die Position über den node_updated Event
});
/* 8. Kontextmenü (optional) */
const setupContextMenu = () => {
cy.on('cxttap', 'node', function(e) {
const node = e.target;
const nodeData = node.data();
// Position des Kontextmenüs berechnen
const renderedPosition = node.renderedPosition();
const containerRect = cy.container().getBoundingClientRect();
const menuX = containerRect.left + renderedPosition.x;
const menuY = containerRect.top + renderedPosition.y;
// Kontextmenü erstellen oder aktualisieren
let contextMenu = document.getElementById('context-menu');
if (!contextMenu) {
contextMenu = document.createElement('div');
contextMenu.id = 'context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.backgroundColor = '#fff';
contextMenu.style.border = '1px solid #ccc';
contextMenu.style.borderRadius = '4px';
contextMenu.style.padding = '5px 0';
contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
contextMenu.style.zIndex = 1000;
document.body.appendChild(contextMenu);
}
// Menüinhalte
contextMenu.innerHTML = `
<div class="menu-item" data-action="edit">Knoten bearbeiten</div>
<div class="menu-item" data-action="connect">Verbindung erstellen</div>
<div class="menu-item" data-action="delete">Knoten löschen</div>
`;
// Styling für Menüpunkte
const menuItems = contextMenu.querySelectorAll('.menu-item');
menuItems.forEach(item => {
item.style.padding = '8px 20px';
item.style.cursor = 'pointer';
item.style.fontSize = '14px';
item.addEventListener('mouseover', function() {
this.style.backgroundColor = '#f0f0f0';
});
item.addEventListener('mouseout', function() {
this.style.backgroundColor = 'transparent';
});
// Event-Handler
item.addEventListener('click', async function() {
const action = this.getAttribute('data-action');
switch(action) {
case 'edit':
// Knoten bearbeiten (gleiche Logik wie beim Edit-Button)
const name = prompt('Knotenname:', nodeData.name);
if (name) {
const description = prompt('Beschreibung:', nodeData.description || '');
await post(`/api/mind_map_node/${nodeData.id}`, { name, description });
}
break;
case 'connect':
// Modus zum Verbinden aktivieren
cy.nodes().unselect();
node.select();
alert('Wählen Sie nun einen zweiten Knoten aus, um eine Verbindung zu erstellen');
break;
case 'delete':
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
await del(`/api/mind_map_node/${nodeData.id}`);
}
break;
}
// Menü schließen
contextMenu.style.display = 'none';
});
});
// Menü positionieren und anzeigen
contextMenu.style.left = menuX + 'px';
contextMenu.style.top = menuY + 'px';
contextMenu.style.display = 'block';
// Event-Listener zum Schließen des Menüs
const closeMenu = function() {
if (contextMenu) {
contextMenu.style.display = 'none';
}
document.removeEventListener('click', closeMenu);
};
// Verzögerung, um den aktuellen Click nicht zu erfassen
setTimeout(() => {
document.addEventListener('click', closeMenu);
}, 0);
e.preventDefault();
});
};
// Kontextmenü aktivieren (optional)
// setupContextMenu();
/* 9. Export-Funktion (optional) */
const btnExport = document.getElementById('exportMindmap');
if (btnExport) {
btnExport.addEventListener('click', () => {
const elements = cy.json().elements;
const exportData = {
nodes: elements.nodes.map(n => ({
id: n.data.id,
name: n.data.name,
description: n.data.description,
category_id: n.data.category_id,
x: Math.round(n.position?.x || 0),
y: Math.round(n.position?.y || 0)
})),
relationships: elements.edges.map(e => ({
parent_id: e.data.source,
child_id: e.data.target
}))
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mindmap_export.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
}
/* 10. Filter-Funktion nach Kategorien (optional) */
const setupCategoryFilters = () => {
const filterContainer = document.getElementById('category-filters');
if (!filterContainer || !categories.length) return;
filterContainer.innerHTML = '';
// "Alle anzeigen" Option
const allBtn = document.createElement('button');
allBtn.innerText = 'Alle Kategorien';
allBtn.className = 'category-filter active';
allBtn.onclick = () => {
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
allBtn.classList.add('active');
cy.nodes().removeClass('filtered').show();
cy.edges().show();
};
filterContainer.appendChild(allBtn);
// Filter-Button pro Kategorie
categories.forEach(category => {
const btn = document.createElement('button');
btn.innerText = category.name;
btn.className = 'category-filter';
btn.style.backgroundColor = category.color_code;
btn.style.color = '#fff';
btn.onclick = () => {
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
btn.classList.add('active');
const matchingNodes = cy.nodes().filter(node => node.data('category_id') === category.id);
cy.nodes().addClass('filtered').hide();
matchingNodes.removeClass('filtered').show();
// Verbindungen zu/von diesen Knoten anzeigen
cy.edges().hide();
matchingNodes.connectedEdges().show();
};
filterContainer.appendChild(btn);
});
};
// Filter-Funktionalität aktivieren (optional)
// setupCategoryFilters();
/* 11. Suchfunktion (optional) */
const searchInput = document.getElementById('search-mindmap');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
if (!searchTerm) {
cy.nodes().removeClass('search-hidden').show();
cy.edges().show();
return;
}
cy.nodes().forEach(node => {
const name = node.data('name').toLowerCase();
const description = (node.data('description') || '').toLowerCase();
if (name.includes(searchTerm) || description.includes(searchTerm)) {
node.removeClass('search-hidden').show();
node.connectedEdges().show();
} else {
node.addClass('search-hidden').hide();
// Kanten nur verstecken, wenn beide verbundenen Knoten versteckt sind
node.connectedEdges().forEach(edge => {
const otherNode = edge.source().id() === node.id() ? edge.target() : edge.source();
if (otherNode.hasClass('search-hidden')) {
edge.hide();
}
});
}
});
});
}
console.log('Mindmap erfolgreich initialisiert');
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -62,6 +62,22 @@ body {
body.dark { body.dark {
color: var(--dark-text-primary); color: var(--dark-text-primary);
background-color: transparent;
}
/* Ensure proper contrast in both modes */
body:not(.dark) {
--text-primary: var(--light-text-primary);
--text-secondary: var(--light-text-secondary);
--bg-primary: var(--light-bg-primary);
--bg-secondary: var(--light-bg-secondary);
}
body.dark {
--text-primary: var(--dark-text-primary);
--text-secondary: var(--dark-text-secondary);
--bg-primary: var(--dark-bg-primary);
--bg-secondary: var(--dark-bg-secondary);
} }
/* Typography */ /* Typography */

View File

View File

@@ -17,7 +17,6 @@
<!-- Tailwind CSS - CDN für Entwicklung und Produktion (laut Vorgabe) --> <!-- Tailwind CSS - CDN für Entwicklung und Produktion (laut Vorgabe) -->
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- Alternative lokale Version, falls die CDN-Version blockiert wird --> <!-- Alternative lokale Version, falls die CDN-Version blockiert wird -->
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<script> <script>
tailwind = window.tailwind || {}; tailwind = window.tailwind || {};
tailwind.config = { tailwind.config = {
@@ -113,17 +112,9 @@
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
<!-- Custom dark mode styles --> <!-- Custom dark mode styles -->
<!-- ► ► FarbToken strikt getrennt ◄ ◄ -->
<style> <style>
/* Dezenter Hintergrund für beide Modi */ /* LightMode */
.dark {
--bg-primary: #181c24;
--bg-secondary: #232837;
--text-primary: #f9fafb;
--text-secondary: #e5e7eb;
--accent-primary: #6d28d9;
--accent-secondary: #8b5cf6;
--glow-effect: 0 0 8px rgba(124, 58, 237, 0.15);
}
:root { :root {
--bg-primary:#f4f6fa; --bg-primary:#f4f6fa;
--bg-secondary:#e9ecf3; --bg-secondary:#e9ecf3;
@@ -131,64 +122,33 @@
--text-secondary:#475569; --text-secondary:#475569;
--accent-primary:#7c3aed; --accent-primary:#7c3aed;
--accent-secondary:#8b5cf6; --accent-secondary:#8b5cf6;
--glow-effect: 0 0 8px rgba(139, 92, 246, 0.08); --glow-effect:0 0 8px rgba(139,92,246,.08);
} }
body.dark { /* DarkMode */
background-color: var(--bg-primary); .dark {
color: var(--text-primary); --bg-primary:#181c24;
--bg-secondary:#232837;
--text-primary:#f9fafb;
--text-secondary:#e5e7eb;
--accent-primary:#6d28d9;
--accent-secondary:#8b5cf6;
--glow-effect:0 0 8px rgba(124,58,237,.15);
} }
body { body {
background-color: var(--bg-primary); @apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)] transition-colors duration-300;
color: var(--text-primary);
}
/* Mystical glowing effects */
.mystical-glow {
text-shadow: var(--glow-effect);
} }
/* Utilities */
.mystical-glow { text-shadow: var(--glow-effect); }
.gradient-text { .gradient-text {
background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary)); background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));
-webkit-background-clip: text; -webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:none;
background-clip: text;
color: transparent;
text-shadow: none;
}
/* Glass morphism effects */
.glass-morphism {
backdrop-filter: blur(10px);
}
.dark .glass-navbar-dark {
background-color: rgba(10, 14, 25, 0.8);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
}
.glass-navbar-light {
background-color: rgba(255, 255, 255, 0.8);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
}
/* Alpine.js x-cloak für ausgeblendete Elemente */
[x-cloak] { display: none !important; }
/* Grundlegende Klassen, um sicherzustellen, dass Tailwind geladen wird */
.nav-link {
@apply text-gray-300 hover:text-white transition-colors duration-200;
}
.nav-link-active {
@apply text-white font-medium;
}
.nav-link-light {
@apply text-gray-600 hover:text-gray-900 transition-colors duration-200;
}
.nav-link-light-active {
@apply text-gray-900 font-medium;
} }
.glass-morphism { backdrop-filter: blur(10px); }
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
</style> </style>
</head> </head>
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{ <body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
@@ -546,6 +506,10 @@
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'"> :class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Impressum Impressum
</a> </a>
<a href="{{ url_for('ueber_uns') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Über uns
</a>
<a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200" <a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'"> :class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Datenschutz Datenschutz

View File

@@ -8,56 +8,56 @@
<h1 class="text-3xl font-bold mb-6 gradient-text">Impressum</h1> <h1 class="text-3xl font-bold mb-6 gradient-text">Impressum</h1>
<section class="mb-8"> <section class="mb-8">
<h2 class="text-xl font-bold mb-4">Angaben gemäß § 5 TMG</h2> <h2 class="text-xl font-bold mb-4">Angaben gemäß § 5 TMG und § 55 RStV</h2>
<p class="mb-4">MindMap GmbH<br>
Musterstraße 123<br>
12345 Musterstadt<br>
Deutschland</p>
<p class="mb-4"> <p class="mb-4">
<strong>Vertreten durch:</strong><br> Diese Website wird privat betrieben von:<br>
Max Mustermann, Geschäftsführer Marwin Medczinski<br>
Fasanenstraße 30<br>
16761 Hennigsdorf<br>
Deutschland
</p>
</p> </p>
<p class="mb-4"> <p class="mb-4">
<strong>Kontakt:</strong><br> <strong>Kontakt:</strong><br>
Telefon: +49 (0) 123 456789<br> Telefon: +49 (0) 173 8041824<br>
E-Mail: info@mindmap-example.com E-Mail: medczinski.marwin@gmx.de
</p>
<p class="mb-4">
<strong>Registereintrag:</strong><br>
Eintragung im Handelsregister.<br>
Registergericht: Amtsgericht Musterstadt<br>
Registernummer: HRB 12345
</p>
<p class="mb-4">
<strong>Umsatzsteuer-ID:</strong><br>
Umsatzsteuer-Identifikationsnummer gemäß §27 a Umsatzsteuergesetz:<br>
DE 123456789
</p> </p>
</section> </section>
<section class="mb-8"> <section class="mb-8">
<h2 class="text-xl font-bold mb-4">Redaktionell verantwortlich</h2> <h2 class="text-xl font-bold mb-4">Inhaltlich Verantwortlicher gemäß § 55 Abs. 2 RStV</h2>
<p> <p class="mb-4">
Max Mustermann<br> Marwin Medczinski<br>
Musterstraße 123<br> Fasanenstraße 30<br>
12345 Musterstadt 16761 Hennigsdorf
</p> </p>
</section> </section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Hinweis zur Streitbeilegung</h2>
<p class="mb-4">Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: <a href="https://ec.europa.eu/consumers/odr/" target="_blank" class="text-purple-600 hover:text-purple-800">https://ec.europa.eu/consumers/odr/</a></p>
<p class="mb-4">Ich bin nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</p>
</section>
<section class="mb-8"> <section class="mb-8">
<h2 class="text-xl font-bold mb-4">Haftungsausschluss</h2> <h2 class="text-xl font-bold mb-4">Haftungsausschluss</h2>
<h3 class="text-lg font-bold mb-2">Haftung für Inhalte</h3> <h3 class="text-lg font-bold mb-2">Haftung für Inhalte</h3>
<p class="mb-4">Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.</p> <p class="mb-4">Die Inhalte dieser Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte kann ich jedoch keine Gewähr übernehmen. Als Diensteanbieter bin ich gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG bin ich als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.</p>
<p class="mb-4">Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.</p> <p class="mb-4">Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werde ich diese Inhalte umgehend entfernen.</p>
<h3 class="text-lg font-bold mb-2">Haftung für Links</h3> <h3 class="text-lg font-bold mb-2">Haftung für Links</h3>
<p class="mb-4">Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar.</p> <p class="mb-4">Diese Website enthält Links zu externen Webseiten Dritter, auf deren Inhalte ich keinen Einfluss habe. Deshalb kann ich für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar.</p>
<p>Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.</p> <p class="mb-4">Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Links umgehend entfernen.</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Urheberrecht</h2>
<p class="mb-4">Die durch mich erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen meiner schriftlichen Zustimmung. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet.</p>
<p>Soweit die Inhalte auf dieser Seite nicht von mir erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitte ich um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Inhalte umgehend entfernen.</p>
</section> </section>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@@ -626,7 +626,7 @@
<div class="text-center py-12"> <div class="text-center py-12">
<i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i> <i class="fas fa-lightbulb text-5xl text-gray-400 mb-4"></i>
<p class="text-gray-500">Noch keine Gedanken erstellt</p> <p class="text-gray-500">Noch keine Gedanken erstellt</p>
<a href="{{ url_for('create_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Ersten Gedanken erstellen</a> <a href="{{ url_for('get_thought') }}" class="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">Ersten Gedanken erstellen</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>

79
templates/ueber_uns.html Normal file
View File

@@ -0,0 +1,79 @@
{% extends "base.html" %}
{% block title %}Über uns{% endblock %}
{% block content %}
<div class="max-w-3xl mx-auto">
<div class="card p-6 md:p-8">
<h1 class="text-3xl font-bold mb-6 gradient-text">Über uns</h1>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Unsere Vision</h2>
<p class="mb-4">
Systades ist ein innovatives Projekt, das darauf abzielt, das Teilen und Vernetzen von Wissen und Gedanken zu revolutionieren. Unsere Plattform ermöglicht es Nutzern, ihre Ideen in interaktiven Mindmaps zu organisieren und mit anderen zu teilen, wodurch ein kollaboratives Netzwerk des Wissens entsteht.
</p>
<p class="mb-4">
Wir glauben daran, dass Wissen am wertvollsten ist, wenn es geteilt und vernetzt wird. Durch die Verbindung verschiedener Perspektiven und Denkansätze entstehen neue Erkenntnisse und Innovationen.
</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Das Team</h2>
<p class="mb-4">
Till Tomczak und Marwin Medczinski arbeiten gemeinsam daran, Systades kontinuierlich zu verbessern und weiterzuentwickeln.
</p>
<!-- Platz für Team-Mitglieder -->
<div class="team-members space-y-6">
<!-- Beispiel für ein Team-Mitglied (kann als Vorlage verwendet werden) -->
<!--
<div class="team-member p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h3 class="text-lg font-bold mb-2">[Name]</h3>
<p class="text-gray-600 dark:text-gray-300 mb-2">[Position/Rolle]</p>
<p class="text-sm">[Kurze Beschreibung oder Verantwortlichkeiten]</p>
</div>
-->
</div>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Unsere Mission</h2>
<p class="mb-4">
Wir setzen uns dafür ein, eine Plattform zu schaffen, die:
</p>
<ul class="list-disc list-inside space-y-2 mb-4">
<li>Intuitive Werkzeuge für die Organisation und Visualisierung von Wissen bereitstellt</li>
<li>Die Zusammenarbeit und den Austausch zwischen Nutzern fördert</li>
<li>Kreativität und innovative Denkansätze unterstützt</li>
<li>Einen sicheren und respektvollen Raum für intellektuellen Austausch bietet</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Technologie & Innovation</h2>
<p class="mb-4">
Systades nutzt modernste Technologien und innovative Ansätze, um eine optimale Nutzererfahrung zu gewährleisten. Unsere Plattform wird kontinuierlich weiterentwickelt, um neue Funktionen und Verbesserungen zu integrieren.
</p>
<p>
Wir legen besonderen Wert auf:
</p>
<ul class="list-disc list-inside space-y-2 mt-2">
<li>Intuitive Benutzeroberfläche</li>
<li>Hohe Performance und Zuverlässigkeit</li>
<li>Datensicherheit und Privatsphäre</li>
<li>Barrierefreiheit und Inklusivität</li>
</ul>
</section>
<section>
<h2 class="text-xl font-bold mb-4">Kontakt & Feedback</h2>
<p class="mb-4">
Wir freuen uns über Ihr Feedback und Ihre Ideen zur Verbesserung von Systades. Gemeinsam können wir die Plattform weiter optimieren und an die Bedürfnisse unserer Nutzer anpassen.
</p>
<p>
Kontaktieren Sie uns gerne für Fragen, Anregungen oder Kooperationsmöglichkeiten.
</p>
</section>
</div>
</div>
{% endblock %}

60
test_app.py Normal file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sqlite3
import os
import requests
import time
print("Systades Anwendungstest")
print("=======================")
# Prüfen, ob die Datenbank existiert
db_path = 'systades.db'
if not os.path.exists(db_path):
print(f"Datenbank {db_path} existiert nicht.")
exit(1)
# Datenbankprüfung
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Prüfen, ob die User-Tabelle existiert und Benutzer enthält
cursor.execute("SELECT COUNT(*) FROM user;")
user_count = cursor.fetchone()[0]
print(f"Anzahl der Benutzer in der Datenbank: {user_count}")
if user_count == 0:
print("WARNUNG: Keine Benutzer in der Datenbank gefunden!")
print("Bitte führen Sie das Skript 'create_default_users.py' aus, um Standardbenutzer zu erstellen.")
conn.close()
exit(1)
# Tabellenschema prüfen
cursor.execute("PRAGMA table_info(user);")
columns = cursor.fetchall()
column_names = [column[1] for column in columns]
print(f"Spalten in der User-Tabelle: {', '.join(column_names)}")
# Überprüfen, ob die password-Spalte existiert
if 'password' not in column_names:
print("FEHLER: Die Spalte 'password' fehlt in der Tabelle 'user'!")
print("Bitte führen Sie das Skript 'fix_user_table.py' aus, um die Datenbank zu reparieren.")
conn.close()
exit(1)
# Benutzer für Testlogin abrufen
cursor.execute("SELECT username, email FROM user LIMIT 1;")
test_user = cursor.fetchone()
if test_user:
print(f"Testbenutzer für Login: {test_user[0]} (E-Mail: {test_user[1]})")
else:
print("FEHLER: Konnte keinen Testbenutzer abrufen.")
conn.close()
print("\nDie Datenbank scheint korrekt konfiguriert zu sein.")
print("Sie können nun die Anwendung starten und sich mit den folgenden Zugangsdaten anmelden:")
print(" Admin: username=admin, password=admin")
print(" User: username=user, password=user")
print("\nTest abgeschlossen.")