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:
414
app.py
414
app.py
@@ -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)
|
||||
|
||||
122
config.json
122
config.json
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user