modified: app.py

modified:   config.json
	modified:   projects_list.json
	deleted:    projects_webserver.json
	deleted:    public_projects_list.json
	modified:   templates/available_projects.html
This commit is contained in:
SimolZimol
2025-07-06 11:50:17 +02:00
parent d66ce43838
commit 5109de2d19
6 changed files with 777 additions and 495 deletions

414
app.py
View File

@@ -109,31 +109,57 @@ class ProjectManager:
"""Klone Projekt aus Git-Repository"""
project_path = os.path.join(PROJECTS_DIR, project_name)
# Prüfe ob Projekt bereits existiert
if os.path.exists(project_path):
return False, f"Projekt {project_name} existiert bereits"
try:
# Git Clone ausführen
result = subprocess.run([
'git', 'clone', project_url, project_path
], capture_output=True, text=True, timeout=300)
if result.returncode == 0:
print(f"✓ Git Clone erfolgreich für {project_name}")
# Nach erfolgreichem Klonen die Version speichern
try:
self.save_project_version(project_name, project_url)
print(f"✓ Version für {project_name} gespeichert")
except Exception as e:
print(f"Warnung: Konnte Version für {project_name} nicht speichern: {e}")
print(f"Warnung: Konnte Version für {project_name} nicht speichern: {e}")
return True, f"Projekt {project_name} erfolgreich geklont"
return True, f"Projekt {project_name} erfolgreich geklont und konfiguriert"
else:
# Bereinige bei Fehler
if os.path.exists(project_path):
try:
import shutil
shutil.rmtree(project_path)
except:
pass
return False, f"Fehler beim Klonen: {result.stderr}"
except subprocess.TimeoutExpired:
# Bereinige bei Timeout
if os.path.exists(project_path):
try:
import shutil
shutil.rmtree(project_path)
except:
pass
return False, "Timeout beim Klonen des Projekts"
except Exception as e:
# Bereinige bei Fehler
if os.path.exists(project_path):
try:
import shutil
shutil.rmtree(project_path)
except:
pass
return False, f"Fehler beim Klonen: {str(e)}"
def save_project_version(self, project_name, project_url=None):
def save_project_version(self, project_name, project_url=None, installation_method='clone'):
"""Speichere die aktuelle Version eines Projekts"""
project_path = os.path.join(PROJECTS_DIR, project_name)
@@ -141,8 +167,8 @@ class ProjectManager:
version = self.extract_project_version(project_path, project_name, project_url)
if version:
# Speichere Version in einer .version Datei
version_file = os.path.join(project_path, '.app_installer_version')
# Speichere Version in der NEUEN version_app_in Datei (Hauptdatei)
version_file = os.path.join(project_path, 'version_app_in')
try:
with open(version_file, 'w', encoding='utf-8') as f:
import json
@@ -150,18 +176,42 @@ class ProjectManager:
'version': version,
'installed_at': datetime.now().isoformat(),
'project_url': project_url,
'installation_method': installation_method,
'last_updated': datetime.now().isoformat()
}
json.dump(version_data, f, indent=2)
print(f"Version {version} für Projekt {project_name} gespeichert")
print(f"Version {version} für Projekt {project_name} in version_app_in gespeichert")
except Exception as e:
print(f"Fehler beim Speichern der Version: {e}")
print(f"Fehler beim Speichern der version_app_in Datei: {e}")
# Erstelle auch eine Backup-Datei (.app_installer_version) für Rückwärtskompatibilität
backup_version_file = os.path.join(project_path, '.app_installer_version')
try:
with open(backup_version_file, 'w', encoding='utf-8') as f:
version_data = {
'version': version,
'installed_at': datetime.now().isoformat(),
'project_url': project_url,
'installation_method': installation_method,
'last_updated': datetime.now().isoformat()
}
json.dump(version_data, f, indent=2)
print(f"📋 Backup-Versionsdatei erstellt: .app_installer_version")
except Exception as e:
print(f"⚠️ Warnung: Backup-Versionsdatei konnte nicht erstellt werden: {e}")
def extract_project_version(self, project_path, project_name, project_url=None):
"""Extrahiere Version aus verschiedenen Quellen"""
version = None
# 1. Versuche package.json (Node.js Projekte)
# 1. HÖCHSTE PRIORITÄT: Versuche aus Projektliste (falls URL bekannt)
if project_url:
version = self.get_version_from_project_list(project_name, project_url)
if version:
print(f"✓ Version aus Projektliste: {version}")
return version
# 2. Versuche package.json (Node.js Projekte)
package_json_path = os.path.join(project_path, 'package.json')
if os.path.exists(package_json_path):
try:
@@ -170,12 +220,12 @@ class ProjectManager:
package_data = json.load(f)
version = package_data.get('version')
if version:
print(f"Version aus package.json: {version}")
print(f"Version aus package.json: {version}")
return version
except Exception as e:
print(f"Fehler beim Lesen von package.json: {e}")
print(f"Fehler beim Lesen von package.json: {e}")
# 2. Versuche pyproject.toml oder setup.py (Python Projekte)
# 3. Versuche pyproject.toml oder setup.py (Python Projekte)
pyproject_path = os.path.join(project_path, 'pyproject.toml')
if os.path.exists(pyproject_path):
try:
@@ -185,12 +235,12 @@ class ProjectManager:
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
if match:
version = match.group(1)
print(f"Version aus pyproject.toml: {version}")
print(f"Version aus pyproject.toml: {version}")
return version
except Exception as e:
print(f"Fehler beim Lesen von pyproject.toml: {e}")
print(f"Fehler beim Lesen von pyproject.toml: {e}")
# 3. Versuche aus Git Tags
# 4. Versuche aus Git Tags
try:
result = subprocess.run(['git', 'describe', '--tags', '--abbrev=0'],
cwd=project_path, capture_output=True, text=True, timeout=10)
@@ -199,38 +249,51 @@ class ProjectManager:
# Bereinige Tag (entferne v prefix falls vorhanden)
if version.startswith('v'):
version = version[1:]
print(f"Version aus Git Tag: {version}")
print(f"Version aus Git Tag: {version}")
return version
except Exception as e:
print(f"Fehler beim Lesen von Git Tags: {e}")
print(f"Fehler beim Lesen von Git Tags: {e}")
# 4. Versuche aus Git Commit Hash (als Fallback)
# 5. Versuche aus Git Commit Hash (als Fallback)
try:
result = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'],
cwd=project_path, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
commit_hash = result.stdout.strip()
version = f"git-{commit_hash}"
print(f"Version aus Git Commit: {version}")
print(f"Version aus Git Commit: {version}")
return version
except Exception as e:
print(f"Fehler beim Lesen von Git Commit: {e}")
# 5. Versuche aus Projektliste (falls URL bekannt)
if project_url:
version = self.get_version_from_project_list(project_name, project_url)
if version:
print(f"Version aus Projektliste: {version}")
return version
print(f"Fehler beim Lesen von Git Commit: {e}")
# 6. Fallback: Default Version
print(f"Keine Version gefunden für {project_name}, verwende 1.0.0")
print(f"Keine Version gefunden für {project_name}, verwende 1.0.0")
return "1.0.0"
def get_version_from_project_list(self, project_name, project_url):
"""Hole Version aus der Projektliste"""
"""Hole Version aus der Projektliste (priorisiert Remote-URLs)"""
try:
# Durchsuche alle Projektlisten
# 1. ERSTE PRIORITÄT: Versuche die konfigurierte Remote-URL
config = self.load_config()
remote_url = config.get('project_list_url')
if remote_url:
print(f"🌐 Hole Version von Remote-URL: {remote_url}")
try:
remote_projects = self.fetch_project_list(remote_url)
for project in remote_projects:
if (project.get('name') == project_name or
project.get('url') == project_url):
metadata = project.get('metadata', {})
version = metadata.get('version')
if version:
print(f"✓ Remote-Version gefunden: {version}")
return version
except Exception as e:
print(f"⚠ Fehler beim Abrufen der Remote-Projektliste: {e}")
# 2. FALLBACK: Lokale Projektlisten
print(f"📁 Suche in lokalen Projektlisten...")
for project_list_file in ['projects_list.json', 'public_projects_list.json', 'projects_webserver.json']:
if os.path.exists(project_list_file):
with open(project_list_file, 'r', encoding='utf-8') as f:
@@ -241,26 +304,53 @@ class ProjectManager:
metadata = project.get('metadata', {})
version = metadata.get('version')
if version:
print(f"✓ Lokale Version gefunden: {version}")
return version
except Exception as e:
print(f"Fehler beim Lesen der Projektliste: {e}")
print(f"Fehler beim Lesen der Projektliste: {e}")
print(f"⚠ Keine Version gefunden für Projekt {project_name}")
return None
def get_installed_version(self, project_name):
"""Hole die installierte Version eines Projekts"""
"""Hole die installierte Version eines Projekts aus der version_app_in Datei"""
project_path = os.path.join(PROJECTS_DIR, project_name)
version_file = os.path.join(project_path, '.app_installer_version')
# ERSTE PRIORITÄT: Prüfe die neue version_app_in Datei
version_file = os.path.join(project_path, 'version_app_in')
if os.path.exists(version_file):
try:
with open(version_file, 'r', encoding='utf-8') as f:
import json
version_data = json.load(f)
return version_data.get('version')
version = version_data.get('version')
if version:
print(f"📋 Version aus version_app_in: {version}")
return version
except Exception as e:
print(f"Fehler beim Lesen der Versionsdatei: {e}")
print(f"Fehler beim Lesen der version_app_in Datei: {e}")
# Fallback: Versuche direkt aus Projekt zu extrahieren
# ZWEITE PRIORITÄT: Fallback auf alte .app_installer_version Datei
old_version_file = os.path.join(project_path, '.app_installer_version')
if os.path.exists(old_version_file):
try:
with open(old_version_file, 'r', encoding='utf-8') as f:
import json
version_data = json.load(f)
version = version_data.get('version')
# Migration: Verschiebe zu neuer Datei
if version:
print(f"🔄 Migriere Version für {project_name} von .app_installer_version zu version_app_in")
self.save_project_version(project_name, version_data.get('project_url'), version_data.get('installation_method', 'clone'))
return version
except Exception as e:
print(f"❌ Fehler beim Lesen der alten Versionsdatei: {e}")
# DRITTE PRIORITÄT: Versuche direkt aus Projekt zu extrahieren
print(f"⚠️ Keine Versionsdatei gefunden für {project_name}, extrahiere aus Projekt...")
return self.extract_project_version(project_path, project_name)
def get_project_info(self, project_name):
@@ -368,6 +458,67 @@ class ProjectManager:
except Exception as e:
return False, f"Unerwarteter Fehler beim Build: {str(e)}"
def pull_docker_image(self, image_url, project_name):
"""Lade Docker-Image herunter"""
if not self.docker_available:
return False, "Docker ist nicht verfügbar"
try:
print(f"Lade Docker-Image herunter: {image_url}")
# Docker pull ausführen
image = self.docker_client.images.pull(image_url)
# Tag Image mit Projektname für lokale Verwendung
image.tag(f"{project_name}:latest")
print(f"✓ Docker-Image {image_url} erfolgreich heruntergeladen")
# Erstelle minimales Projektverzeichnis für Konfiguration
project_path = os.path.join(PROJECTS_DIR, project_name)
os.makedirs(project_path, exist_ok=True)
# Erstelle .dockerimage Datei um zu markieren, dass es ein Image-Download ist
with open(os.path.join(project_path, '.dockerimage'), 'w', encoding='utf-8') as f:
json.dump({
'image_url': image_url,
'pulled_at': datetime.now().isoformat(),
'installation_method': 'image'
}, f, indent=2)
return True, f"Docker-Image {project_name} erfolgreich heruntergeladen"
except docker.errors.APIError as e:
return False, f"Docker-API-Fehler: {str(e)}"
except Exception as e:
return False, f"Fehler beim Herunterladen des Images: {str(e)}"
def get_installation_methods(self, project):
"""Ermittle verfügbare Installationsmethoden für ein Projekt"""
methods = []
# Prüfe neues Schema mit installation.methods
if 'installation' in project and 'methods' in project['installation']:
for method_type, method_info in project['installation']['methods'].items():
if method_info.get('available', True):
methods.append({
'type': method_type,
'url': method_info['url'],
'description': method_info.get('description', ''),
'preferred': project['installation'].get('preferred') == method_type
})
else:
# Fallback für alte Projekte ohne neues Schema
if 'url' in project:
methods.append({
'type': 'clone',
'url': project['url'],
'description': 'Quellcode klonen und bauen',
'preferred': True
})
return methods
def check_port_availability(self, port):
"""Prüfe ob ein Port verfügbar ist"""
import socket
@@ -997,13 +1148,39 @@ def refresh_projects():
def available_projects():
"""Zeige verfügbare Projekte zum Installieren"""
config = project_manager.load_config()
return render_template('available_projects.html', projects=config.get('projects', []))
# Versuche zuerst aktuelle Projekte von Remote-URL zu laden
projects = []
remote_url = config.get('project_list_url')
if remote_url:
print(f"🌐 Lade aktuelle Projektliste von: {remote_url}")
try:
projects = project_manager.fetch_project_list(remote_url)
if projects:
print(f"{len(projects)} Projekte von Remote-URL geladen")
# Aktualisiere auch die lokale Konfiguration
config['projects'] = projects
project_manager.save_config(config)
else:
print("⚠ Keine Projekte von Remote-URL erhalten, verwende lokale Fallback")
projects = config.get('projects', [])
except Exception as e:
print(f"⚠ Fehler beim Laden von Remote-URL: {e}")
print("📁 Verwende lokale Projektliste als Fallback")
projects = config.get('projects', [])
else:
print("📁 Keine Remote-URL konfiguriert, verwende lokale Projekte")
projects = config.get('projects', [])
return render_template('available_projects.html', projects=projects)
@app.route('/install_project', methods=['POST'])
def install_project():
"""Installiere Projekt"""
project_url = request.form.get('project_url')
project_name = request.form.get('project_name')
installation_method = request.form.get('installation_method', 'clone') # 'clone' oder 'image'
force_reinstall = request.form.get('force_reinstall', 'false').lower() == 'true'
if not project_url or not project_name:
@@ -1012,26 +1189,59 @@ def install_project():
# Prüfe ob Projekt bereits existiert
project_path = os.path.join(PROJECTS_DIR, project_name)
if os.path.exists(project_path):
if force_reinstall:
# Entferne bestehendes Projekt vor Neuinstallation
remove_success, remove_msg = project_manager.remove_project(project_name)
if not remove_success:
return jsonify({'success': False, 'message': f'Fehler beim Entfernen: {remove_msg}'})
print(f" Projekt {project_name} existiert bereits, führe Update durch...")
# Existierendes Projekt: Führe automatisch Update durch
success, message = project_manager.update_project(project_name)
if success:
# Hole aktuelle installierte Version nach Update
installed_version = project_manager.get_installed_version(project_name)
return jsonify({
'success': True,
'message': f'Projekt {project_name} wurde aktualisiert (Version: {installed_version})',
'updated': True,
'version': installed_version
})
else:
return jsonify({'success': False, 'message': f'Projekt {project_name} bereits installiert. Verwenden Sie Update stattdessen.'})
return jsonify({
'success': False,
'message': f'Update von {project_name} fehlgeschlagen: {message}'
})
# Klone Projekt
success, message = project_manager.clone_project(project_url, project_name)
if success:
# Versuche automatisch zu bauen
build_success, build_message = project_manager.build_project(project_name)
if build_success:
message += f" und gebaut: {build_message}"
# Neue Installation basierend auf Methode
if installation_method == 'image':
# Docker-Image herunterladen
success, message = project_manager.pull_docker_image(project_url, project_name)
if success:
# Nach erfolgreichem Download die Version speichern
try:
project_manager.save_project_version(project_name, project_url, installation_method='image')
except Exception as e:
print(f"Warnung: Konnte Version nach Image-Download nicht speichern: {e}")
return jsonify({
'success': True,
'message': f'{message}. Bereit zum Starten.',
'method': 'image'
})
else:
message += f" (Build fehlgeschlagen: {build_message})"
return jsonify({'success': False, 'message': message})
return jsonify({'success': success, 'message': message})
else:
# Standard: Git Clone
success, message = project_manager.clone_project(project_url, project_name)
if success:
# Versuche automatisch zu bauen
build_success, build_message = project_manager.build_project(project_name)
if build_success:
message += f" und gebaut: {build_message}"
else:
message += f" (Build fehlgeschlagen: {build_message})"
return jsonify({'success': success, 'message': message, 'method': 'clone'})
@app.route('/build_project/<project_name>')
def build_project(project_name):
@@ -1850,60 +2060,74 @@ def api_installed_projects():
project_info = {
'name': project_name,
'status': project_manager.get_container_status(project_name),
'version': '1.0.0' # Default version
'version': project_manager.get_installed_version(project_name) or '1.0.0'
}
# Versuche Version aus package.json zu lesen
package_json_path = os.path.join(project_path, 'package.json')
if os.path.exists(package_json_path):
try:
with open(package_json_path, 'r', encoding='utf-8') as f:
package_data = json.load(f)
project_info['version'] = package_data.get('version', '1.0.0')
except:
pass
# Versuche Version aus README.md zu extrahieren
readme_path = os.path.join(project_path, 'README.md')
if os.path.exists(readme_path):
try:
with open(readme_path, 'r', encoding='utf-8') as f:
readme_content = f.read()
# Suche nach Version-Pattern
import re
version_match = re.search(r'[Vv]ersion[\s:]*(\d+\.\d+\.\d+)', readme_content)
if version_match:
project_info['version'] = version_match.group(1)
except:
pass
# Versuche Version aus Dockerfile zu extrahieren
dockerfile_path = os.path.join(project_path, 'Dockerfile')
if os.path.exists(dockerfile_path):
try:
with open(dockerfile_path, 'r', encoding='utf-8') as f:
dockerfile_content = f.read()
# Suche nach LABEL version
import re
version_match = re.search(r'LABEL version[=\s]+"?([0-9.]+)"?', dockerfile_content, re.IGNORECASE)
if version_match:
project_info['version'] = version_match.group(1)
except:
pass
installed_projects.append(project_info)
return jsonify({
'success': True,
'projects': installed_projects
'projects': installed_projects,
'count': len(installed_projects)
})
except Exception as e:
print(f"Fehler bei API installed_projects: {e}")
return jsonify({
'success': False,
'error': str(e),
'projects': []
}), 500
'message': str(e),
'projects': [],
'count': 0
})
@app.route('/api/projects_list.json')
def api_projects_list():
"""API Endpoint für die lokale Projektliste (für Tests)"""
try:
with open('projects_list.json', 'r', encoding='utf-8') as f:
projects = json.load(f)
return jsonify(projects)
except Exception as e:
return jsonify([]), 500
@app.route('/api/refresh_projects', methods=['POST'])
def api_refresh_projects():
"""API Endpoint zum Aktualisieren der Projektliste via AJAX"""
try:
config = project_manager.load_config()
if not config.get('project_list_url'):
return jsonify({
'success': False,
'error': 'Keine Projekt-URL konfiguriert'
})
# Lade aktuelle Projekte von Remote-URL
projects = project_manager.fetch_project_list(config['project_list_url'])
if projects:
# Aktualisiere lokale Konfiguration
config['projects'] = projects
project_manager.save_config(config)
return jsonify({
'success': True,
'message': f'{len(projects)} Projekte erfolgreich aktualisiert',
'projects': projects,
'count': len(projects)
})
else:
return jsonify({
'success': False,
'error': 'Keine Projekte von Remote-URL empfangen'
})
except Exception as e:
print(f"Fehler bei API refresh_projects: {e}")
return jsonify({
'success': False,
'error': f'Fehler beim Aktualisieren: {str(e)}'
})
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)

View File

@@ -1,62 +1,106 @@
{
"project_list_url": "https://simolzimol.eu/projects_list.json",
"project_list_url": "http://localhost:5000/api/projects_list.json",
"auto_refresh_minutes": 30,
"docker_registry": "",
"projects": [
{
"url": "https://gitea.simolzimol.net/Simon/quizify",
"name": "quizify",
"description": "Ein interaktives Quiz-System mit modernem Web-Interface",
"language": "JavaScript",
"tags": [
"quiz",
"web",
"javascript",
"node.js",
"interactive"
],
"category": "Web Application",
"config_hints": {
"backup": "Automatische Datenbank-Backups konfigurierbar",
"docker_compose": "docker-compose.yml für Development und Testing",
"dockerfile": "Multi-stage Build für optimierte Container-Größe",
"env_example": ".env.example vorhanden mit allen nötigen Variablen"
},
"demo_url": "https://demo.simolzimol.net/quizify",
"description": "Ein interaktives Quiz-System mit modernem Web-Interface für Echtzeit-Quizzes und Wettbewerbe",
"docker_port": 3000,
"environment_vars": {
"ADMIN_PASSWORD": "",
"DATABASE_URL": "sqlite:///data/quiz.db",
"JWT_SECRET": "",
"NODE_ENV": "production",
"PORT": "3000",
"DATABASE_URL": "sqlite:///data/quiz.db"
"PORT": "3000"
},
"features": [
"Interaktive Quiz-Erstellung mit Drag & Drop",
"Multi-User Support mit Echtzeit-Synchronisation",
"Live-Scoring und Bestenlisten",
"Export/Import von Quiz-Daten (JSON, CSV)",
"Responsive Design für alle Geräte",
"Admin-Dashboard für Benutzerverwaltung",
"Multiplayer-Modi (Team vs Team)",
"Zeitbasierte Challenges",
"Statistiken und Analytics"
],
"install_time": "2-3 Minuten",
"installation": {
"methods": {
"clone": {
"available": true,
"description": "Quellcode klonen und selbst bauen",
"type": "git",
"url": "https://gitea.simolzimol.net/Simon/quizify"
},
"image": {
"available": false,
"description": "Vorgefertigtes Docker-Image herunterladen",
"type": "docker",
"url": "docker.io/simolzimol/quizify:1.3.0"
}
},
"preferred": "clone"
},
"installation_notes": "Benötigt Node.js 18+ und SQLite. Automatische Datenbank-Migration beim ersten Start. Admin-Account wird automatisch erstellt.",
"language": "JavaScript",
"metadata": {
"author": "Simon",
"created": "2024-01-15",
"documentation": "https://gitea.simolzimol.net/Simon/quizify/wiki",
"downloads": 245,
"homepage": "https://gitea.simolzimol.net/Simon/quizify",
"issues": "https://gitea.simolzimol.net/Simon/quizify/issues",
"last_updated": "2025-07-05",
"license": "MIT",
"stars": 150,
"version": "1.3.0"
},
"min_resources": {
"cpu_cores": 1,
"disk_mb": 500,
"ram_mb": 512
},
"name": "quizify",
"rating": {
"downloads": 245,
"reviews": 12,
"score": 4.5
},
"requirements": {
"docker": true,
"min_cpu": "1 Core",
"min_disk": "500MB",
"min_memory": "512MB",
"ports": [
3000,
8080
]
},
"metadata": {
"created": "2024-01-15",
"last_updated": "2025-07-05",
"version": "1.2.0",
"author": "Simon",
"license": "MIT",
"homepage": "https://gitea.simolzimol.net/Simon/quizify",
"documentation": "https://gitea.simolzimol.net/Simon/quizify/wiki",
"issues": "https://gitea.simolzimol.net/Simon/quizify/issues"
},
"features": [
"Interaktive Quiz-Erstellung",
"Multi-User Support",
"Real-time Scoring",
"Export/Import Funktionen",
"Responsive Design"
],
"screenshots": [
"https://simolzimol.eu/assets/test.jpg",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot2.png"
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot1.png",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot2.png",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot3.png"
],
"installation_notes": "Benötigt Node.js 18+ und SQLite. Automatische Datenbank-Migration beim ersten Start.",
"config_hints": {
"env_example": ".env.example vorhanden",
"dockerfile": "Multi-stage Build verfügbar",
"docker_compose": "docker-compose.yml für Development"
}
"size_mb": 45,
"tags": [
"quiz",
"web",
"javascript",
"node.js",
"interactive",
"realtime",
"multiplayer"
],
"url": "https://gitea.simolzimol.net/Simon/quizify"
}
]
}

View File

@@ -2,47 +2,89 @@
{
"url": "https://gitea.simolzimol.net/Simon/quizify",
"name": "quizify",
"description": "Ein interaktives Quiz-System mit modernem Web-Interface",
"description": "Ein interaktives Quiz-System mit modernem Web-Interface für Echtzeit-Quizzes und Wettbewerbe",
"language": "JavaScript",
"tags": ["quiz", "web", "javascript", "node.js", "interactive"],
"tags": ["quiz", "web", "javascript", "node.js", "interactive", "realtime", "multiplayer"],
"category": "Web Application",
"docker_port": 3000,
"environment_vars": {
"NODE_ENV": "production",
"PORT": "3000",
"DATABASE_URL": "sqlite:///data/quiz.db"
"DATABASE_URL": "sqlite:///data/quiz.db",
"JWT_SECRET": "",
"ADMIN_PASSWORD": ""
},
"requirements": {
"docker": true,
"min_memory": "512MB",
"min_cpu": "1 Core",
"min_disk": "500MB",
"ports": [3000, 8080]
},
"installation": {
"methods": {
"clone": {
"available": true,
"url": "https://gitea.simolzimol.net/Simon/quizify",
"type": "git",
"description": "Quellcode klonen und selbst bauen"
},
"image": {
"available": false,
"url": "docker.io/simolzimol/quizify:1.3.0",
"type": "docker",
"description": "Vorgefertigtes Docker-Image herunterladen"
}
},
"preferred": "clone"
},
"metadata": {
"created": "2024-01-15",
"last_updated": "2025-07-05",
"version": "1.2.0",
"version": "1.3.0",
"author": "Simon",
"license": "MIT",
"homepage": "https://gitea.simolzimol.net/Simon/quizify",
"documentation": "https://gitea.simolzimol.net/Simon/quizify/wiki",
"issues": "https://gitea.simolzimol.net/Simon/quizify/issues"
"issues": "https://gitea.simolzimol.net/Simon/quizify/issues",
"downloads": 245,
"stars": 150
},
"features": [
"Interaktive Quiz-Erstellung",
"Multi-User Support",
"Real-time Scoring",
"Export/Import Funktionen",
"Responsive Design"
"Interaktive Quiz-Erstellung mit Drag & Drop",
"Multi-User Support mit Echtzeit-Synchronisation",
"Live-Scoring und Bestenlisten",
"Export/Import von Quiz-Daten (JSON, CSV)",
"Responsive Design für alle Geräte",
"Admin-Dashboard für Benutzerverwaltung",
"Multiplayer-Modi (Team vs Team)",
"Zeitbasierte Challenges",
"Statistiken und Analytics"
],
"screenshots": [
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot1.png",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot2.png"
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot2.png",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot3.png"
],
"installation_notes": "Benötigt Node.js 18+ und SQLite. Automatische Datenbank-Migration beim ersten Start.",
"installation_notes": "Benötigt Node.js 18+ und SQLite. Automatische Datenbank-Migration beim ersten Start. Admin-Account wird automatisch erstellt.",
"config_hints": {
"env_example": ".env.example vorhanden",
"dockerfile": "Multi-stage Build verfügbar",
"docker_compose": "docker-compose.yml für Development"
"env_example": ".env.example vorhanden mit allen nötigen Variablen",
"dockerfile": "Multi-stage Build für optimierte Container-Größe",
"docker_compose": "docker-compose.yml für Development und Testing",
"backup": "Automatische Datenbank-Backups konfigurierbar"
},
"rating": {
"score": 4.5,
"reviews": 12,
"downloads": 245
},
"size_mb": 45,
"install_time": "2-3 Minuten",
"demo_url": "https://demo.simolzimol.net/quizify",
"min_resources": {
"ram_mb": 512,
"cpu_cores": 1,
"disk_mb": 500
}
}
]

View File

@@ -1,73 +0,0 @@
[
{
"url": "https://gitea.simolzimol.net/Simon/quizify",
"name": "quizify",
"description": "Ein interaktives Quiz-System mit modernem Web-Interface für Echtzeit-Quizzes und Wettbewerbe",
"language": "JavaScript",
"tags": ["quiz", "web", "javascript", "node.js", "interactive", "realtime", "multiplayer"],
"category": "Web Application",
"docker_port": 3000,
"environment_vars": {
"NODE_ENV": "production",
"PORT": "3000",
"DATABASE_URL": "sqlite:///data/quiz.db",
"JWT_SECRET": "your-secret-key-here",
"ADMIN_PASSWORD": "admin123"
},
"requirements": {
"docker": true,
"min_memory": "512MB",
"min_cpu": "1 Core",
"min_disk": "500MB",
"ports": [3000, 8080]
},
"metadata": {
"created": "2024-01-15",
"last_updated": "2025-07-05",
"version": "1.3.0",
"author": "Simon",
"license": "MIT",
"homepage": "https://gitea.simolzimol.net/Simon/quizify",
"documentation": "https://gitea.simolzimol.net/Simon/quizify/wiki",
"issues": "https://gitea.simolzimol.net/Simon/quizify/issues",
"downloads": 245,
"stars": 15
},
"features": [
"Interaktive Quiz-Erstellung mit Drag & Drop",
"Multi-User Support mit Echtzeit-Synchronisation",
"Live-Scoring und Bestenlisten",
"Export/Import von Quiz-Daten (JSON, CSV)",
"Responsive Design für alle Geräte",
"Admin-Dashboard für Benutzerverwaltung",
"Multiplayer-Modi (Team vs Team)",
"Zeitbasierte Challenges",
"Statistiken und Analytics"
],
"screenshots": [
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot1.png",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot2.png",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot3.png"
],
"installation_notes": "Benötigt Node.js 18+ und SQLite. Automatische Datenbank-Migration beim ersten Start. Admin-Account wird automatisch erstellt.",
"config_hints": {
"env_example": ".env.example vorhanden mit allen nötigen Variablen",
"dockerfile": "Multi-stage Build für optimierte Container-Größe",
"docker_compose": "docker-compose.yml für Development und Testing",
"backup": "Automatische Datenbank-Backups konfigurierbar"
},
"rating": {
"score": 4.5,
"reviews": 12,
"downloads": 245
},
"size_mb": 45,
"install_time": "2-3 Minuten",
"demo_url": "https://demo.simolzimol.net/quizify",
"min_resources": {
"ram_mb": 512,
"cpu_cores": 1,
"disk_mb": 500
}
}
]

View File

@@ -1,143 +0,0 @@
[
{
"url": "https://gitea.simolzimol.net/Simon/quizify",
"name": "quizify",
"description": "Ein interaktives Quiz-System mit modernem Web-Interface für Echtzeit-Quizzes und Wettbewerbe",
"language": "JavaScript",
"tags": ["quiz", "web", "javascript", "node.js", "interactive", "realtime", "multiplayer"],
"category": "Web Application",
"docker_port": 3000,
"environment_vars": {
"NODE_ENV": "production",
"PORT": "3000",
"DATABASE_URL": "sqlite:///data/quiz.db",
"JWT_SECRET": "your-secret-key-here",
"ADMIN_PASSWORD": "admin123"
},
"requirements": {
"docker": true,
"min_memory": "512MB",
"min_cpu": "1 Core",
"min_disk": "500MB",
"ports": [3000, 8080]
},
"metadata": {
"created": "2024-01-15",
"last_updated": "2025-07-05",
"version": "1.3.0",
"author": "Simon",
"license": "MIT",
"homepage": "https://gitea.simolzimol.net/Simon/quizify",
"documentation": "https://gitea.simolzimol.net/Simon/quizify/wiki",
"issues": "https://gitea.simolzimol.net/Simon/quizify/issues",
"downloads": 245,
"stars": 15
},
"features": [
"Interaktive Quiz-Erstellung mit Drag & Drop",
"Multi-User Support mit Echtzeit-Synchronisation",
"Live-Scoring und Bestenlisten",
"Export/Import von Quiz-Daten (JSON, CSV)",
"Responsive Design für alle Geräte",
"Admin-Dashboard für Benutzerverwaltung",
"Multiplayer-Modi (Team vs Team)",
"Zeitbasierte Challenges",
"Statistiken und Analytics"
],
"screenshots": [
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot1.png",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot2.png",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot3.png"
],
"installation_notes": "Benötigt Node.js 18+ und SQLite. Automatische Datenbank-Migration beim ersten Start. Admin-Account wird automatisch erstellt.",
"config_hints": {
"env_example": ".env.example vorhanden mit allen nötigen Variablen",
"dockerfile": "Multi-stage Build für optimierte Container-Größe",
"docker_compose": "docker-compose.yml für Development und Testing",
"backup": "Automatische Datenbank-Backups konfigurierbar"
},
"rating": {
"score": 4.5,
"reviews": 12,
"downloads": 245
},
"size_mb": 45,
"install_time": "2-3 Minuten",
"demo_url": "https://demo.simolzimol.net/quizify",
"min_resources": {
"ram_mb": 512,
"cpu_cores": 1,
"disk_mb": 500
}
}
]
"install_time": "2-3 Minuten",
"size_mb": 45,
"min_resources": {
"ram_mb": 512,
"disk_mb": 200,
"cpu_cores": 1
},
"ports": [3000, 8080],
"environment_vars": {
"NODE_ENV": "production",
"PORT": "3000",
"DATABASE_URL": "sqlite:///data/quiz.db",
"SECRET_KEY": "your-secret-key-here"
},
"metadata": {
"created": "2024-01-15",
"last_updated": "2025-07-05",
"version": "1.2.0",
"author": "Simon",
"license": "MIT",
"homepage": "https://gitea.simolzimol.net/Simon/quizify",
"documentation": "https://gitea.simolzimol.net/Simon/quizify/wiki",
"issues": "https://gitea.simolzimol.net/Simon/quizify/issues",
"stars": 15,
"forks": 3,
"commits": 127
},
"features": [
"Interaktive Quiz-Erstellung mit Drag & Drop",
"Multi-User Support mit Rollen",
"Real-time Scoring und Leaderboards",
"Export/Import von Quizzes (JSON, CSV)",
"Responsive Design für Mobile/Desktop",
"Offline-Modus für lokale Quizzes",
"Statistiken und Analytics"
],
"screenshots": [
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot1.png",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/screenshot2.png",
"https://gitea.simolzimol.net/Simon/quizify/raw/branch/main/docs/demo.gif"
],
"installation_notes": "Benötigt Node.js 18+ und SQLite. Automatische Datenbank-Migration beim ersten Start. Standardzugang: admin/admin",
"config_hints": {
"env_example": ".env.example vorhanden mit allen Optionen",
"dockerfile": "Multi-stage Build für Produktion optimiert",
"docker_compose": "docker-compose.yml für Development verfügbar",
"has_tests": true,
"has_docs": true
},
"dependencies": ["node:18-alpine", "sqlite3", "express", "socket.io"],
"demo_url": "https://demo.simolzimol.net/quizify",
"preview_available": true,
"auto_start": true,
"health_check": "/api/health",
"backup_compatible": true,
"update_strategy": "rolling",
"changelog": "https://gitea.simolzimol.net/Simon/quizify/releases",
"security": {
"has_https": true,
"auth_required": true,
"data_encryption": true,
"security_scan": "passed"
},
"rating": {
"score": 4.8,
"reviews": 23,
"downloads": 1250
}
}
]

View File

@@ -9,9 +9,9 @@
<button class="btn btn-outline-info me-2" onclick="showBulkInstall()">
<i class="fas fa-layer-group"></i> Masseninstallation
</button>
<a href="{{ url_for('refresh_projects') }}" class="btn btn-primary">
<button id="refreshProjectsBtn" class="btn btn-primary" onclick="refreshProjectsList()">
<i class="fas fa-sync-alt"></i> Liste aktualisieren
</a>
</button>
</div>
</div>
@@ -234,12 +234,24 @@
<!-- Action Buttons -->
<div class="mt-auto">
<div class="d-grid gap-2">
<!-- Hauptinstallation (bevorzugte Methode) -->
{% if project.installation and project.installation.methods %}
{% set preferred_method = project.installation.preferred or 'clone' %}
{% set methods = project.installation.methods %}
{% set preferred = methods[preferred_method] if preferred_method in methods else methods.values()|list|first %}
<div class="btn-group" role="group">
<button class="btn btn-success install-btn"
data-url="{{ project.url }}"
data-url="{{ preferred.url }}"
data-name="{{ project.name }}"
onclick="installProject('{{ project.url }}', '{{ project.name }}')">
<i class="fas fa-download"></i> Installieren
data-method="{{ preferred_method }}"
onclick="installProject('{{ preferred.url }}', '{{ project.name }}', '{{ preferred_method }}')">
<i class="fas fa-download"></i>
{% if preferred_method == 'image' %}
<i class="fab fa-docker"></i> Docker installieren
{% else %}
<i class="fab fa-git-alt"></i> Code klonen
{% endif %}
</button>
{% if project.demo_url %}
<a href="{{ project.demo_url }}" target="_blank" class="btn btn-outline-info">
@@ -248,6 +260,45 @@
{% endif %}
</div>
<!-- Alternative Installationsmethode (falls verfügbar) -->
{% if methods|length > 1 %}
<div class="btn-group btn-group-sm" role="group">
{% for method_type, method_info in methods.items() %}
{% if method_type != preferred_method and method_info.get('available', True) %}
<button class="btn btn-outline-secondary"
data-url="{{ method_info.url }}"
data-name="{{ project.name }}"
data-method="{{ method_type }}"
onclick="installProject('{{ method_info.url }}', '{{ project.name }}', '{{ method_type }}')">
{% if method_type == 'image' %}
<i class="fab fa-docker"></i> Docker
{% else %}
<i class="fab fa-git-alt"></i> Code
{% endif %}
</button>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% else %}
<!-- Fallback für alte Projekte ohne neue Struktur -->
<div class="btn-group" role="group">
<button class="btn btn-success install-btn"
data-url="{{ project.url }}"
data-name="{{ project.name }}"
data-method="clone"
onclick="installProject('{{ project.url }}', '{{ project.name }}', 'clone')">
<i class="fas fa-download"></i> <i class="fab fa-git-alt"></i> Installieren
</button>
{% if project.demo_url %}
<a href="{{ project.demo_url }}" target="_blank" class="btn btn-outline-info">
<i class="fas fa-external-link-alt"></i> Demo
</a>
{% endif %}
</div>
{% endif %}
<!-- Zusätzliche Buttons -->
<div class="btn-group btn-group-sm" role="group">
{% if project.metadata and project.metadata.documentation %}
@@ -356,7 +407,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="projectDetailsTitle">Projektdetails</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen" onclick="closeModalSafely('projectDetailsModal')"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen" onclick="closeModalSimple('projectDetailsModal')"></button>
</div>
<div class="modal-body" id="projectDetailsContent">
<!-- Content wird per JavaScript gefüllt -->
@@ -367,7 +418,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="closeModalSafely('projectDetailsModal')">Schließen</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="closeModalSimple('projectDetailsModal')">Schließen</button>
<button type="button" class="btn btn-success" id="installFromDetails">
<i class="fas fa-download"></i> Installieren
</button>
@@ -547,49 +598,74 @@ function updateInstalledCount() {
}
}
// Projekt installieren
function installProject(url, name) {
// Projekt installieren mit Installationsmethode
function installProject(url, name, method = 'clone') {
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installiere...';
// Zeige spezifischen Loading-Text basierend auf Methode
if (method === 'image') {
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Docker lädt...';
} else {
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Klont Code...';
}
button.disabled = true;
console.log(`🚀 Installiere ${name} via ${method} von ${url}`);
fetch('/install_project', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `project_url=${encodeURIComponent(url)}&project_name=${encodeURIComponent(name)}`
body: `project_url=${encodeURIComponent(url)}&project_name=${encodeURIComponent(name)}&installation_method=${encodeURIComponent(method)}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
button.innerHTML = '<i class="fas fa-check"></i> Installiert';
// Erfolgsmeldung mit Methode und Version
const version = data.version || '1.0.0';
const methodIcon = method === 'image' ? '🐳' : '📦';
button.innerHTML = `<i class="fas fa-check"></i> ${methodIcon} Installiert (v${version})`;
button.className = 'btn btn-success';
// Update installierte Projekte Liste
if (!installedProjects.includes(name)) {
installedProjects.push(name);
updateProjectStatus();
updateInstalledCount();
const existingProject = installedProjects.find(p => p.name === name);
if (existingProject) {
existingProject.version = version;
existingProject.method = method;
} else {
installedProjects.push({
name: name,
version: version,
method: method,
status: 'running'
});
}
updateProjectStatus();
updateInstalledCount();
// Nach 3 Sekunden zu Update-Button wechseln
setTimeout(() => {
button.innerHTML = '<i class="fas fa-sync-alt"></i> Update';
button.className = 'btn btn-warning';
button.onclick = () => updateProject(url, name);
button.disabled = false;
}, 2000);
}, 3000);
} else {
button.innerHTML = originalText;
button.disabled = false;
alert('Fehler: ' + data.message);
// Zeige detaillierten Fehler
showAlert('danger', `Installationsfehler für ${name}: ${data.message}`);
}
})
.catch(error => {
button.innerHTML = originalText;
button.disabled = false;
alert('Netzwerkfehler: ' + error);
console.error('Installation error:', error);
showAlert('danger', `Netzwerkfehler bei Installation von ${name}: ${error}`);
});
}
@@ -794,127 +870,180 @@ function installAppsSequentially(apps, index) {
function showProjectDetails(projectName) {
console.log('🔍 Zeige Projektdetails für:', projectName);
// SOFORTIGE Backdrop-Bereinigung beim Klick
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
console.log('🗑️ Entferne bestehenden Backdrop sofort');
backdrop.remove();
});
// SOFORTIGE und AGGRESSIVE Backdrop-Bereinigung
forceCleanupModals();
// Finde Projekt in der Liste
const projectData = {{ projects|tojson }};
const project = projectData.find(p => p.name === projectName);
// Warte bis alles sauber ist, dann öffne Modal
setTimeout(() => {
// Finde Projekt in der Liste
const projectData = {{ projects|tojson }};
const project = projectData.find(p => p.name === projectName);
if (!project) {
alert('Projektdaten nicht gefunden');
return;
}
// Fülle Modal mit Daten
document.getElementById('projectDetailsTitle').textContent = project.name;
let content = `
<div class="row">
<div class="col-md-8">
<h6>Beschreibung</h6>
<p>${project.description || 'Keine Beschreibung verfügbar'}</p>
${project.features ? `
<h6>Features</h6>
<ul>
${project.features.map(f => `<li>${f}</li>`).join('')}
</ul>
` : ''}
${project.screenshots ? `
<h6>Screenshots</h6>
<div class="row">
${project.screenshots.slice(0, 3).map(img => `
<div class="col-4">
<img src="${img}" class="img-fluid rounded" alt="Screenshot"
style="max-height: 150px; object-fit: cover; cursor: pointer;"
onclick="window.open('${img}', '_blank')">
</div>
`).join('')}
</div>
` : ''}
${project.installation_notes ? `
<h6 class="mt-3">Installationshinweise</h6>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>${project.installation_notes}
</div>
` : ''}
</div>
<div class="col-md-4">
<h6>Technische Details</h6>
<table class="table table-sm">
<tr><td>Sprache:</td><td>${project.language || 'Unbekannt'}</td></tr>
<tr><td>Kategorie:</td><td>${project.category || 'Unbekannt'}</td></tr>
<tr><td>Version:</td><td>${project.metadata?.version || 'Unbekannt'}</td></tr>
<tr><td>Autor:</td><td>${project.metadata?.author || 'Unbekannt'}</td></tr>
<tr><td>Lizenz:</td><td>${project.metadata?.license || 'Unbekannt'}</td></tr>
<tr><td>Größe:</td><td>${project.size_mb ? project.size_mb + ' MB' : 'Unbekannt'}</td></tr>
<tr><td>Port:</td><td>${project.docker_port || 8080}</td></tr>
${project.install_time ? `<tr><td>Installationszeit:</td><td>${project.install_time}</td></tr>` : ''}
</table>
${project.min_resources ? `
<h6>Systemanforderungen</h6>
<table class="table table-sm">
<tr><td>RAM:</td><td>${project.min_resources.ram_mb} MB</td></tr>
<tr><td>CPU:</td><td>${project.min_resources.cpu_cores} Core(s)</td></tr>
<tr><td>Disk:</td><td>${project.min_resources.disk_mb} MB</td></tr>
</table>
` : ''}
${project.metadata?.homepage || project.metadata?.documentation ? `
<h6>Links</h6>
<div class="d-grid gap-2">
${project.metadata.homepage ? `<a href="${project.metadata.homepage}" target="_blank" class="btn btn-outline-primary btn-sm"><i class="fas fa-home"></i> Homepage</a>` : ''}
${project.metadata.documentation ? `<a href="${project.metadata.documentation}" target="_blank" class="btn btn-outline-info btn-sm"><i class="fas fa-book"></i> Dokumentation</a>` : ''}
${project.demo_url ? `<a href="${project.demo_url}" target="_blank" class="btn btn-outline-success btn-sm"><i class="fas fa-play"></i> Demo</a>` : ''}
</div>
` : ''}
</div>
</div>
`;
document.getElementById('projectDetailsContent').innerHTML = content;
// Install Button Setup mit verbesserter Logik
const installBtn = document.getElementById('installFromDetails');
const installedProject = installedProjects.find(p => p.name === projectName);
if (installedProject) {
const availableVersion = project.metadata?.version || '1.0.0';
const installedVersion = installedProject.version;
const versionComparison = compareVersions(availableVersion, installedVersion);
if (versionComparison > 0) {
installBtn.innerHTML = `<i class="fas fa-arrow-up"></i> Update (${installedVersion}${availableVersion})`;
installBtn.className = 'btn btn-warning';
installBtn.disabled = false;
installBtn.onclick = () => {
closeModalSimple('projectDetailsModal');
updateProject(project.url, project.name);
};
} else {
installBtn.innerHTML = `<i class="fas fa-check"></i> Bereits installiert (${installedVersion})`;
installBtn.className = 'btn btn-success';
installBtn.disabled = true;
}
} else {
installBtn.innerHTML = '<i class="fas fa-download"></i> Installieren';
installBtn.className = 'btn btn-success';
installBtn.disabled = false;
installBtn.onclick = () => {
closeModalSimple('projectDetailsModal');
installProject(project.url, project.name);
};
}
// Öffne Modal mit EINFACHER API
openModalSimple('projectDetailsModal');
}, 200); // Längere Wartezeit für absolute Sicherheit
}
// EINFACHE Modal-Funktionen ohne komplexe Bootstrap-Logik
function openModalSimple(modalId) {
console.log('🎯 Öffne Modal einfach:', modalId);
if (!project) {
alert('Projektdaten nicht gefunden');
// SOFORT alle Backdrops entfernen
document.querySelectorAll('.modal-backdrop').forEach(backdrop => backdrop.remove());
// Body-Klassen bereinigen
document.body.classList.remove('modal-open');
document.body.style.removeProperty('overflow');
document.body.style.removeProperty('padding-right');
const modalElement = document.getElementById(modalId);
if (!modalElement) {
console.error('❌ Modal nicht gefunden:', modalId);
return;
}
// Fülle Modal mit Daten
document.getElementById('projectDetailsTitle').textContent = project.name;
// Modal direkt zeigen OHNE Bootstrap-Instanz
modalElement.style.display = 'block';
modalElement.classList.add('show');
modalElement.setAttribute('aria-modal', 'true');
modalElement.removeAttribute('aria-hidden');
let content = `
<div class="row">
<div class="col-md-8">
<h6>Beschreibung</h6>
<p>${project.description || 'Keine Beschreibung verfügbar'}</p>
${project.features ? `
<h6>Features</h6>
<ul>
${project.features.map(f => `<li>${f}</li>`).join('')}
</ul>
` : ''}
${project.screenshots ? `
<h6>Screenshots</h6>
<div class="row">
${project.screenshots.slice(0, 3).map(img => `
<div class="col-4">
<img src="${img}" class="img-fluid rounded" alt="Screenshot"
style="max-height: 150px; object-fit: cover; cursor: pointer;"
onclick="window.open('${img}', '_blank')">
</div>
`).join('')}
</div>
` : ''}
${project.installation_notes ? `
<h6 class="mt-3">Installationshinweise</h6>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>${project.installation_notes}
</div>
` : ''}
</div>
<div class="col-md-4">
<h6>Technische Details</h6>
<table class="table table-sm">
<tr><td>Sprache:</td><td>${project.language || 'Unbekannt'}</td></tr>
<tr><td>Kategorie:</td><td>${project.category || 'Unbekannt'}</td></tr>
<tr><td>Version:</td><td>${project.metadata?.version || 'Unbekannt'}</td></tr>
<tr><td>Autor:</td><td>${project.metadata?.author || 'Unbekannt'}</td></tr>
<tr><td>Lizenz:</td><td>${project.metadata?.license || 'Unbekannt'}</td></tr>
<tr><td>Größe:</td><td>${project.size_mb ? project.size_mb + ' MB' : 'Unbekannt'}</td></tr>
<tr><td>Port:</td><td>${project.docker_port || 8080}</td></tr>
${project.install_time ? `<tr><td>Installationszeit:</td><td>${project.install_time}</td></tr>` : ''}
</table>
${project.min_resources ? `
<h6>Systemanforderungen</h6>
<table class="table table-sm">
<tr><td>RAM:</td><td>${project.min_resources.ram_mb} MB</td></tr>
<tr><td>CPU:</td><td>${project.min_resources.cpu_cores} Core(s)</td></tr>
<tr><td>Disk:</td><td>${project.min_resources.disk_mb} MB</td></tr>
</table>
` : ''}
${project.metadata?.homepage || project.metadata?.documentation ? `
<h6>Links</h6>
<div class="d-grid gap-2">
${project.metadata.homepage ? `<a href="${project.metadata.homepage}" target="_blank" class="btn btn-outline-primary btn-sm"><i class="fas fa-home"></i> Homepage</a>` : ''}
${project.metadata.documentation ? `<a href="${project.metadata.documentation}" target="_blank" class="btn btn-outline-info btn-sm"><i class="fas fa-book"></i> Dokumentation</a>` : ''}
${project.demo_url ? `<a href="${project.demo_url}" target="_blank" class="btn btn-outline-success btn-sm"><i class="fas fa-play"></i> Demo</a>` : ''}
</div>
` : ''}
</div>
</div>
`;
// Body-Klasse für Modal hinzufügen
document.body.classList.add('modal-open');
document.getElementById('projectDetailsContent').innerHTML = content;
console.log('✅ Modal einfach geöffnet:', modalId);
}
function closeModalSimple(modalId) {
console.log('🎯 Schließe Modal einfach:', modalId);
// Install Button Setup mit verbesserter Logik
const installBtn = document.getElementById('installFromDetails');
const installedProject = installedProjects.find(p => p.name === projectName);
if (installedProject) {
const availableVersion = project.metadata?.version || '1.0.0';
const installedVersion = installedProject.version;
const versionComparison = compareVersions(availableVersion, installedVersion);
if (versionComparison > 0) {
installBtn.innerHTML = `<i class="fas fa-arrow-up"></i> Update (${installedVersion}${availableVersion})`;
installBtn.className = 'btn btn-warning';
installBtn.disabled = false;
installBtn.onclick = () => {
closeModalSafely('projectDetailsModal');
updateProject(project.url, project.name);
};
} else {
installBtn.innerHTML = '<i class="fas fa-check"></i> Bereits aktuell';
installBtn.className = 'btn btn-success';
installBtn.disabled = true;
}
} else {
installBtn.innerHTML = '<i class="fas fa-download"></i> Installieren';
installBtn.className = 'btn btn-success';
installBtn.disabled = false;
installBtn.onclick = () => {
closeModalSafely('projectDetailsModal');
installProject(project.url, project.name);
};
const modalElement = document.getElementById(modalId);
if (modalElement) {
modalElement.style.display = 'none';
modalElement.classList.remove('show');
modalElement.setAttribute('aria-hidden', 'true');
modalElement.removeAttribute('aria-modal');
}
// Modal sicher anzeigen
showModalSafely('projectDetailsModal');
// Body bereinigen
document.body.classList.remove('modal-open');
document.body.style.removeProperty('overflow');
document.body.style.removeProperty('padding-right');
// Alle Backdrops entfernen
document.querySelectorAll('.modal-backdrop').forEach(backdrop => backdrop.remove());
console.log('✅ Modal einfach geschlossen:', modalId);
}
// Sichere Modal-Funktionen
@@ -1035,5 +1164,64 @@ function forceCleanupModals() {
console.log('✅ Modal-Bereinigung abgeschlossen');
}, 50);
}
// AJAX Funktionen für Projektliste-Update
function refreshProjectsList() {
const refreshBtn = document.getElementById('refreshProjectsBtn');
const originalText = refreshBtn.innerHTML;
// Zeige Loading-Status
refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Aktualisiere...';
refreshBtn.disabled = true;
fetch('/api/refresh_projects', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Zeige Erfolg
showAlert('success', data.message);
// Lade Seite neu um aktualisierte Projekte anzuzeigen
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showAlert('danger', data.error || 'Fehler beim Aktualisieren der Projektliste');
}
})
.catch(error => {
console.error('Fehler beim Aktualisieren:', error);
showAlert('danger', 'Netzwerkfehler beim Aktualisieren der Projektliste');
})
.finally(() => {
// Button zurücksetzen
refreshBtn.innerHTML = originalText;
refreshBtn.disabled = false;
});
}
function showAlert(type, message) {
// Erstelle Alert-Element
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Füge am Anfang des Containers hinzu
const container = document.querySelector('.container-fluid') || document.querySelector('.container');
container.insertBefore(alertDiv, container.firstChild);
// Automatisch nach 5 Sekunden ausblenden
setTimeout(() => {
alertDiv.remove();
}, 5000);
}
</script>
{% endblock %}