modified: app.py
modified: config.json new file: projects_webserver.json new file: public_projects_list.json modified: templates/available_projects.html new file: templates/available_projects_new.html modified: templates/docker_status.html
This commit is contained in:
381
app.py
381
app.py
@@ -4,6 +4,8 @@ import os
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import shutil
|
||||
from urllib.parse import urlparse
|
||||
import docker
|
||||
import yaml
|
||||
@@ -116,6 +118,12 @@ class ProjectManager:
|
||||
], capture_output=True, text=True, timeout=300)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Nach erfolgreichem Klonen die Version speichern
|
||||
try:
|
||||
self.save_project_version(project_name, project_url)
|
||||
except Exception as e:
|
||||
print(f"Warnung: Konnte Version für {project_name} nicht speichern: {e}")
|
||||
|
||||
return True, f"Projekt {project_name} erfolgreich geklont"
|
||||
else:
|
||||
return False, f"Fehler beim Klonen: {result.stderr}"
|
||||
@@ -125,6 +133,136 @@ class ProjectManager:
|
||||
except Exception as e:
|
||||
return False, f"Fehler beim Klonen: {str(e)}"
|
||||
|
||||
def save_project_version(self, project_name, project_url=None):
|
||||
"""Speichere die aktuelle Version eines Projekts"""
|
||||
project_path = os.path.join(PROJECTS_DIR, project_name)
|
||||
|
||||
# Versuche Version aus verschiedenen Quellen zu extrahieren
|
||||
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')
|
||||
try:
|
||||
with open(version_file, 'w', encoding='utf-8') as f:
|
||||
import json
|
||||
version_data = {
|
||||
'version': version,
|
||||
'installed_at': datetime.now().isoformat(),
|
||||
'project_url': project_url,
|
||||
'last_updated': datetime.now().isoformat()
|
||||
}
|
||||
json.dump(version_data, f, indent=2)
|
||||
print(f"Version {version} für Projekt {project_name} gespeichert")
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Speichern der Version: {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)
|
||||
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:
|
||||
import json
|
||||
package_data = json.load(f)
|
||||
version = package_data.get('version')
|
||||
if version:
|
||||
print(f"Version aus package.json: {version}")
|
||||
return version
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Lesen von package.json: {e}")
|
||||
|
||||
# 2. Versuche pyproject.toml oder setup.py (Python Projekte)
|
||||
pyproject_path = os.path.join(project_path, 'pyproject.toml')
|
||||
if os.path.exists(pyproject_path):
|
||||
try:
|
||||
with open(pyproject_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
import re
|
||||
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
print(f"Version aus pyproject.toml: {version}")
|
||||
return version
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Lesen von pyproject.toml: {e}")
|
||||
|
||||
# 3. Versuche aus Git Tags
|
||||
try:
|
||||
result = subprocess.run(['git', 'describe', '--tags', '--abbrev=0'],
|
||||
cwd=project_path, capture_output=True, text=True, timeout=10)
|
||||
if result.returncode == 0:
|
||||
version = result.stdout.strip()
|
||||
# Bereinige Tag (entferne v prefix falls vorhanden)
|
||||
if version.startswith('v'):
|
||||
version = version[1:]
|
||||
print(f"Version aus Git Tag: {version}")
|
||||
return version
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Lesen von Git Tags: {e}")
|
||||
|
||||
# 4. 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}")
|
||||
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
|
||||
|
||||
# 6. Fallback: Default Version
|
||||
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"""
|
||||
try:
|
||||
# Durchsuche alle 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:
|
||||
projects = json.load(f)
|
||||
for project in projects:
|
||||
if (project.get('name') == project_name or
|
||||
project.get('url') == project_url):
|
||||
metadata = project.get('metadata', {})
|
||||
version = metadata.get('version')
|
||||
if version:
|
||||
return version
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Lesen der Projektliste: {e}")
|
||||
return None
|
||||
|
||||
def get_installed_version(self, project_name):
|
||||
"""Hole die installierte Version eines Projekts"""
|
||||
project_path = os.path.join(PROJECTS_DIR, project_name)
|
||||
version_file = os.path.join(project_path, '.app_installer_version')
|
||||
|
||||
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')
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Lesen der Versionsdatei: {e}")
|
||||
|
||||
# Fallback: Versuche direkt aus Projekt zu extrahieren
|
||||
return self.extract_project_version(project_path, project_name)
|
||||
|
||||
def get_project_info(self, project_name):
|
||||
"""Hole Projektinformationen"""
|
||||
project_path = os.path.join(PROJECTS_DIR, project_name)
|
||||
@@ -139,7 +277,8 @@ class ProjectManager:
|
||||
'has_env_example': os.path.exists(os.path.join(project_path, '.env.example')),
|
||||
'has_docker_compose': os.path.exists(os.path.join(project_path, 'docker-compose.yml')),
|
||||
'status': self.get_container_status(project_name),
|
||||
'created': datetime.fromtimestamp(os.path.getctime(project_path)).strftime('%Y-%m-%d %H:%M')
|
||||
'created': datetime.fromtimestamp(os.path.getctime(project_path)).strftime('%Y-%m-%d %H:%M'),
|
||||
'version': self.get_installed_version(project_name)
|
||||
}
|
||||
|
||||
# Lese README falls vorhanden
|
||||
@@ -692,6 +831,132 @@ class ProjectManager:
|
||||
|
||||
return False, error_msg
|
||||
|
||||
def update_project(self, project_name):
|
||||
"""Update ein Projekt durch Neuinstallation"""
|
||||
try:
|
||||
print(f"Starting update for project: {project_name}")
|
||||
|
||||
# Hole Projekt-Info um URL zu bekommen
|
||||
project_info = self.get_project_info(project_name)
|
||||
if not project_info:
|
||||
return False, f"Projekt {project_name} nicht gefunden"
|
||||
|
||||
# Versuche URL aus Git-Remote zu bekommen
|
||||
project_path = os.path.join(PROJECTS_DIR, project_name)
|
||||
git_url = None
|
||||
|
||||
try:
|
||||
result = subprocess.run(['git', 'remote', 'get-url', 'origin'],
|
||||
cwd=project_path, capture_output=True, text=True, timeout=10)
|
||||
if result.returncode == 0:
|
||||
git_url = result.stdout.strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
if not git_url:
|
||||
return False, "Keine Git-URL für Update gefunden"
|
||||
|
||||
# Stoppe Container falls laufend
|
||||
container_running = False
|
||||
if self.docker_available and self.docker_client:
|
||||
try:
|
||||
container = self.docker_client.containers.get(project_name)
|
||||
if container.status == 'running':
|
||||
container_running = True
|
||||
container.stop()
|
||||
print(f"Container {project_name} gestoppt für Update")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Backup erstellen
|
||||
backup_path = f"{project_path}_backup_{int(time.time())}"
|
||||
try:
|
||||
import shutil
|
||||
shutil.copytree(project_path, backup_path)
|
||||
print(f"Backup erstellt: {backup_path}")
|
||||
except Exception as e:
|
||||
print(f"Backup-Warnung: {e}")
|
||||
|
||||
# Git Pull versuchen
|
||||
try:
|
||||
result = subprocess.run(['git', 'pull'], cwd=project_path,
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if result.returncode == 0:
|
||||
print(f"Git pull erfolgreich für {project_name}")
|
||||
|
||||
# Rebuild falls Dockerfile existiert
|
||||
if os.path.exists(os.path.join(project_path, 'Dockerfile')):
|
||||
build_success, build_msg = self.build_project(project_name)
|
||||
if not build_success:
|
||||
print(f"Build-Warnung: {build_msg}")
|
||||
|
||||
# Container wieder starten falls er lief
|
||||
if container_running:
|
||||
start_success, start_msg = self.start_project(project_name)
|
||||
if start_success:
|
||||
print(f"Container {project_name} nach Update gestartet")
|
||||
|
||||
# Backup entfernen bei Erfolg
|
||||
try:
|
||||
shutil.rmtree(backup_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Version nach erfolgreichem Update aktualisieren
|
||||
try:
|
||||
self.save_project_version(project_name, git_url)
|
||||
except Exception as e:
|
||||
print(f"Warnung: Konnte Version nach Update nicht aktualisieren: {e}")
|
||||
|
||||
return True, f"Projekt {project_name} erfolgreich aktualisiert"
|
||||
else:
|
||||
print(f"Git pull fehlgeschlagen: {result.stderr}")
|
||||
raise Exception(f"Git pull fehlgeschlagen: {result.stderr}")
|
||||
|
||||
except Exception as git_error:
|
||||
print(f"Git pull fehlgeschlagen, versuche Neuinstallation: {git_error}")
|
||||
|
||||
# Entferne altes Projekt
|
||||
remove_success, remove_msg = self.remove_project(project_name)
|
||||
if not remove_success:
|
||||
return False, f"Update fehlgeschlagen: {remove_msg}"
|
||||
|
||||
# Installiere neu
|
||||
clone_success, clone_msg = self.clone_project(git_url, project_name)
|
||||
if clone_success:
|
||||
# Build nach Neuinstallation
|
||||
if os.path.exists(os.path.join(PROJECTS_DIR, project_name, 'Dockerfile')):
|
||||
self.build_project(project_name)
|
||||
|
||||
# Starte falls vorher gelaufen
|
||||
if container_running:
|
||||
self.start_project(project_name)
|
||||
|
||||
# Backup entfernen bei erfolgreichem Update
|
||||
try:
|
||||
if os.path.exists(backup_path):
|
||||
shutil.rmtree(backup_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True, f"Projekt {project_name} durch Neuinstallation aktualisiert"
|
||||
else:
|
||||
# Restore backup bei komplettem Fehler
|
||||
try:
|
||||
if os.path.exists(backup_path):
|
||||
if os.path.exists(project_path):
|
||||
shutil.rmtree(project_path)
|
||||
shutil.move(backup_path, project_path)
|
||||
print(f"Backup wiederhergestellt für {project_name}")
|
||||
except Exception as restore_error:
|
||||
print(f"Backup-Wiederherstellung fehlgeschlagen: {restore_error}")
|
||||
|
||||
return False, f"Update fehlgeschlagen: {clone_msg}"
|
||||
|
||||
except Exception as e:
|
||||
print(f"Update-Fehler für {project_name}: {e}")
|
||||
return False, f"Update-Fehler: {str(e)}"
|
||||
|
||||
# Globale Instanz
|
||||
project_manager = ProjectManager()
|
||||
|
||||
@@ -739,10 +1004,22 @@ def install_project():
|
||||
"""Installiere Projekt"""
|
||||
project_url = request.form.get('project_url')
|
||||
project_name = request.form.get('project_name')
|
||||
force_reinstall = request.form.get('force_reinstall', 'false').lower() == 'true'
|
||||
|
||||
if not project_url or not project_name:
|
||||
return jsonify({'success': False, 'message': 'URL und Name erforderlich'})
|
||||
|
||||
# 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}'})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': f'Projekt {project_name} bereits installiert. Verwenden Sie Update stattdessen.'})
|
||||
|
||||
# Klone Projekt
|
||||
success, message = project_manager.clone_project(project_url, project_name)
|
||||
|
||||
@@ -1526,5 +1803,107 @@ def serve_test_icons():
|
||||
with open('test_icons.html', 'r', encoding='utf-8') as f:
|
||||
return f.read(), 200, {'Content-Type': 'text/html; charset=utf-8'}
|
||||
|
||||
@app.route('/update_project', methods=['POST'])
|
||||
def update_project():
|
||||
"""Update ein Projekt"""
|
||||
project_name = request.form.get('project_name')
|
||||
|
||||
if not project_name:
|
||||
return jsonify({'success': False, 'message': 'Projektname erforderlich'})
|
||||
|
||||
# Update Projekt
|
||||
success, message = project_manager.update_project(project_name)
|
||||
|
||||
return jsonify({'success': success, 'message': message})
|
||||
|
||||
@app.route('/update_project_get/<project_name>')
|
||||
def update_project_get(project_name):
|
||||
"""Update Projekt via GET (Weiterleitung)"""
|
||||
success, message = project_manager.update_project(project_name)
|
||||
flash(message, 'success' if success else 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/api/update_project/<project_name>', methods=['POST'])
|
||||
def api_update_project(project_name):
|
||||
"""API Endpoint zum Updaten eines Projekts"""
|
||||
try:
|
||||
success, message = project_manager.update_project(project_name)
|
||||
return jsonify({
|
||||
'success': success,
|
||||
'message': message
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/installed_projects')
|
||||
def api_installed_projects():
|
||||
"""API Endpoint für installierte Projekte mit Versionsinfo"""
|
||||
try:
|
||||
installed_projects = []
|
||||
|
||||
for project_name in os.listdir(PROJECTS_DIR):
|
||||
project_path = os.path.join(PROJECTS_DIR, project_name)
|
||||
if os.path.isdir(project_path):
|
||||
project_info = {
|
||||
'name': project_name,
|
||||
'status': project_manager.get_container_status(project_name),
|
||||
'version': '1.0.0' # Default version
|
||||
}
|
||||
|
||||
# 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
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'projects': []
|
||||
}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
|
||||
55
config.json
55
config.json
@@ -3,5 +3,60 @@
|
||||
"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",
|
||||
"docker_port": 3000,
|
||||
"environment_vars": {
|
||||
"NODE_ENV": "production",
|
||||
"PORT": "3000",
|
||||
"DATABASE_URL": "sqlite:///data/quiz.db"
|
||||
},
|
||||
"requirements": {
|
||||
"docker": true,
|
||||
"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"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
73
projects_webserver.json
Normal file
73
projects_webserver.json
Normal file
@@ -0,0 +1,73 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
143
public_projects_list.json
Normal file
143
public_projects_list.json
Normal file
@@ -0,0 +1,143 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
765
templates/available_projects_new.html
Normal file
765
templates/available_projects_new.html
Normal file
@@ -0,0 +1,765 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Verfügbare Apps - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="fas fa-cloud-download-alt me-2"></i>Verfügbare Apps</h2>
|
||||
<div>
|
||||
<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">
|
||||
<i class="fas fa-sync-alt"></i> Liste aktualisieren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter und Suche -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label for="searchFilter" class="form-label">Suche</label>
|
||||
<input type="text" class="form-control" id="searchFilter" placeholder="Name, Beschreibung, Tags...">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label for="languageFilter" class="form-label">Sprache</label>
|
||||
<select class="form-control" id="languageFilter">
|
||||
<option value="">Alle</option>
|
||||
{% set languages = [] %}
|
||||
{% for project in projects %}
|
||||
{% if project.language and project.language not in languages %}
|
||||
{% set _ = languages.append(project.language) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for language in languages|sort %}
|
||||
<option value="{{ language }}">{{ language }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label for="categoryFilter" class="form-label">Kategorie</label>
|
||||
<select class="form-control" id="categoryFilter">
|
||||
<option value="">Alle</option>
|
||||
{% set categories = [] %}
|
||||
{% for project in projects %}
|
||||
{% if project.category and project.category not in categories %}
|
||||
{% set _ = categories.append(project.category) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for category in categories|sort %}
|
||||
<option value="{{ category }}">{{ category }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label for="sortBy" class="form-label">Sortierung</label>
|
||||
<select class="form-control" id="sortBy">
|
||||
<option value="name">Name</option>
|
||||
<option value="updated">Letzte Aktualisierung</option>
|
||||
<option value="rating">Bewertung</option>
|
||||
<option value="downloads">Downloads</option>
|
||||
<option value="size">Größe</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Optionen</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="showInstalledFilter">
|
||||
<label class="form-check-label" for="showInstalledFilter">
|
||||
Installierte anzeigen
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not projects %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-exclamation-triangle fa-3x text-warning mb-3"></i>
|
||||
<h3 class="text-muted">Keine Projekte verfügbar</h3>
|
||||
<p class="text-muted mb-4">
|
||||
Stellen Sie sicher, dass eine gültige Projekt-URL in den
|
||||
<a href="{{ url_for('config') }}">Einstellungen</a> konfiguriert ist.
|
||||
</p>
|
||||
<a href="{{ url_for('config') }}" class="btn btn-primary">
|
||||
<i class="fas fa-cog"></i> Einstellungen öffnen
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row" id="projectContainer">
|
||||
{% for project in projects %}
|
||||
<div class="col-lg-6 col-xl-4 mb-4 project-card-wrapper"
|
||||
data-name="{{ project.name|lower }}"
|
||||
data-language="{{ project.language|lower if project.language else '' }}"
|
||||
data-category="{{ project.category|lower if project.category else '' }}"
|
||||
data-tags="{{ project.tags|join(',')|lower if project.tags else '' }}"
|
||||
data-rating="{{ project.rating.score if project.rating and project.rating.score else 0 }}"
|
||||
data-downloads="{{ project.rating.downloads if project.rating and project.rating.downloads else 0 }}"
|
||||
data-size="{{ project.size_mb if project.size_mb else 0 }}"
|
||||
data-updated="{{ project.metadata.last_updated if project.metadata and project.metadata.last_updated else '' }}">
|
||||
<div class="card h-100 project-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h5 class="card-title mb-1">
|
||||
<i class="fas fa-cube me-2"></i>{{ project.name or 'Unbekannt' }}
|
||||
{% if project.metadata and project.metadata.version %}
|
||||
<small class="text-muted">v{{ project.metadata.version }}</small>
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% if project.category %}
|
||||
<span class="badge bg-primary">{{ project.category }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<div class="project-status" data-project-name="{{ project.name }}">
|
||||
<span class="badge bg-secondary">Prüfung...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body d-flex flex-column">
|
||||
<!-- Bewertung und Statistiken -->
|
||||
{% if project.rating %}
|
||||
<div class="mb-2 d-flex justify-content-between">
|
||||
<div class="rating">
|
||||
{% for i in range(5) %}
|
||||
{% if i < (project.rating.score|round|int) %}
|
||||
<i class="fas fa-star text-warning"></i>
|
||||
{% else %}
|
||||
<i class="far fa-star text-muted"></i>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<small class="text-muted ms-1">({{ project.rating.reviews or 0 }})</small>
|
||||
</div>
|
||||
<small class="text-muted">{{ project.rating.downloads or 0 }} Downloads</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Beschreibung -->
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">Repository URL:</small>
|
||||
<p class="card-text small font-monospace text-truncate">{{ project.url }}</p>
|
||||
</div>
|
||||
|
||||
{% if project.description %}
|
||||
<div class="mb-3">
|
||||
<p class="card-text">{{ project.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tags -->
|
||||
{% if project.tags %}
|
||||
<div class="mb-3">
|
||||
{% for tag in project.tags[:6] %}
|
||||
<span class="badge bg-secondary me-1 mb-1">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
{% if project.tags|length > 6 %}
|
||||
<span class="badge bg-light text-dark">+{{ project.tags|length - 6 }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Technische Details -->
|
||||
<div class="row mb-3 small">
|
||||
<div class="col-6">
|
||||
<strong>Sprache:</strong><br>
|
||||
<span class="badge bg-info">{{ project.language or 'Unbekannt' }}</span>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<strong>Größe:</strong><br>
|
||||
<span class="text-muted">
|
||||
{% if project.size_mb %}
|
||||
{{ project.size_mb }} MB
|
||||
{% else %}
|
||||
Unbekannt
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if project.min_resources %}
|
||||
<div class="row mb-3 small">
|
||||
<div class="col-4">
|
||||
<strong>RAM:</strong><br>
|
||||
<span class="text-muted">{{ project.min_resources.ram_mb or 512 }} MB</span>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<strong>CPU:</strong><br>
|
||||
<span class="text-muted">{{ project.min_resources.cpu_cores or 1 }} Core(s)</span>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<strong>Port:</strong><br>
|
||||
<span class="text-muted">{{ project.docker_port or 8080 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Features -->
|
||||
{% if project.features %}
|
||||
<div class="mb-3">
|
||||
<strong class="small">Features:</strong>
|
||||
<ul class="list-unstyled small mt-1">
|
||||
{% for feature in project.features[:3] %}
|
||||
<li><i class="fas fa-check text-success me-1"></i>{{ feature }}</li>
|
||||
{% endfor %}
|
||||
{% if project.features|length > 3 %}
|
||||
<li class="text-muted">... und {{ project.features|length - 3 }} weitere</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Metadaten -->
|
||||
{% if project.metadata %}
|
||||
<div class="mb-3 small text-muted">
|
||||
{% if project.metadata.last_updated %}
|
||||
<div>Letzte Aktualisierung: {{ project.metadata.last_updated }}</div>
|
||||
{% endif %}
|
||||
{% if project.metadata.author %}
|
||||
<div>Autor: {{ project.metadata.author }}</div>
|
||||
{% endif %}
|
||||
{% if project.install_time %}
|
||||
<div>Installationszeit: {{ project.install_time }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="mt-auto">
|
||||
<div class="d-grid gap-2">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-success install-btn"
|
||||
data-url="{{ project.url }}"
|
||||
data-name="{{ project.name }}"
|
||||
onclick="installProject('{{ project.url }}', '{{ project.name }}')">
|
||||
<i class="fas fa-download"></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>
|
||||
|
||||
<!-- Zusätzliche Buttons -->
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
{% if project.metadata and project.metadata.documentation %}
|
||||
<a href="{{ project.metadata.documentation }}" target="_blank" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-book"></i> Docs
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if project.metadata and project.metadata.homepage %}
|
||||
<a href="{{ project.metadata.homepage }}" target="_blank" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-home"></i> Home
|
||||
</a>
|
||||
{% endif %}
|
||||
<button class="btn btn-outline-secondary" onclick="showProjectDetails('{{ project.name }}')">
|
||||
<i class="fas fa-info-circle"></i> Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Installationsstatistiken -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-chart-bar me-2"></i>Installationsstatistiken
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">{{ projects|length }}</span>
|
||||
<span>Verfügbare Apps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number" id="installedCount">0</span>
|
||||
<span>Installierte Apps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">{{ projects|selectattr('language', 'equalto', 'JavaScript')|list|length }}</span>
|
||||
<span>JavaScript Apps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">{{ projects|selectattr('language', 'equalto', 'Python')|list|length }}</span>
|
||||
<span>Python Apps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Bulk Installation Modal -->
|
||||
<div class="modal fade" id="bulkInstallModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-download me-2"></i>Masseninstallation
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Wählen Sie die Apps aus, die Sie installieren möchten:</p>
|
||||
<div class="row" id="bulkInstallList">
|
||||
{% for project in projects %}
|
||||
<div class="col-md-6 mb-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="{{ project.url }}"
|
||||
id="bulk_{{ loop.index }}" data-name="{{ project.name }}">
|
||||
<label class="form-check-label" for="bulk_{{ loop.index }}">
|
||||
{{ project.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="button" class="btn btn-primary" onclick="startBulkInstall()">
|
||||
<i class="fas fa-download"></i> Ausgewählte installieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Details Modal -->
|
||||
<div class="modal fade" id="projectDetailsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="projectDetailsContent">
|
||||
<!-- Content wird per JavaScript gefüllt -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||
<button type="button" class="btn btn-success" id="installFromDetails">
|
||||
<i class="fas fa-download"></i> Installieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let installedProjects = [];
|
||||
|
||||
// Lade installierte Projekte beim Seitenaufruf
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadInstalledProjects();
|
||||
setupFilters();
|
||||
});
|
||||
|
||||
// Lade installierte Projekte
|
||||
function loadInstalledProjects() {
|
||||
fetch('/')
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
// Parse HTML um installierte Projekte zu finden
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const projectCards = doc.querySelectorAll('.project-card .card-title');
|
||||
|
||||
installedProjects = Array.from(projectCards).map(title => {
|
||||
const projectName = title.textContent.trim().replace('🚀', '').trim();
|
||||
return projectName;
|
||||
});
|
||||
|
||||
updateProjectStatus();
|
||||
updateInstalledCount();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der installierten Projekte:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Update Projekt-Status Badges
|
||||
function updateProjectStatus() {
|
||||
document.querySelectorAll('.project-status').forEach(statusEl => {
|
||||
const projectName = statusEl.dataset.projectName;
|
||||
const isInstalled = installedProjects.includes(projectName);
|
||||
|
||||
if (isInstalled) {
|
||||
statusEl.innerHTML = '<span class="badge bg-success">Installiert</span>';
|
||||
|
||||
// Update Button
|
||||
const installBtn = statusEl.closest('.card').querySelector('.install-btn');
|
||||
if (installBtn) {
|
||||
installBtn.innerHTML = '<i class="fas fa-sync-alt"></i> Update';
|
||||
installBtn.className = 'btn btn-warning';
|
||||
installBtn.onclick = () => updateProject(installBtn.dataset.url, installBtn.dataset.name);
|
||||
}
|
||||
} else {
|
||||
statusEl.innerHTML = '<span class="badge bg-secondary">Verfügbar</span>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update Anzahl installierter Apps
|
||||
function updateInstalledCount() {
|
||||
const installedCount = document.getElementById('installedCount');
|
||||
if (installedCount) {
|
||||
installedCount.textContent = installedProjects.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Projekt installieren
|
||||
function installProject(url, name) {
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installiere...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch('/install_project', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `project_url=${encodeURIComponent(url)}&project_name=${encodeURIComponent(name)}`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
button.innerHTML = '<i class="fas fa-check"></i> Installiert';
|
||||
button.className = 'btn btn-success';
|
||||
|
||||
// Update installierte Projekte Liste
|
||||
if (!installedProjects.includes(name)) {
|
||||
installedProjects.push(name);
|
||||
updateProjectStatus();
|
||||
updateInstalledCount();
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
alert('Fehler: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
alert('Netzwerkfehler: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
// Projekt updaten
|
||||
function updateProject(url, name) {
|
||||
if (!confirm(`Möchten Sie das Projekt "${name}" wirklich aktualisieren? Dies überschreibt lokale Änderungen.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Aktualisiere...';
|
||||
button.disabled = true;
|
||||
|
||||
// Verwende die neue Update-API
|
||||
fetch('/update_project', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `project_name=${encodeURIComponent(name)}`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
button.innerHTML = '<i class="fas fa-check"></i> Aktualisiert';
|
||||
button.className = 'btn btn-success';
|
||||
|
||||
// Aktualisiere installierte Projekte Liste
|
||||
setTimeout(() => {
|
||||
loadInstalledProjects();
|
||||
updateProjectStatus();
|
||||
}, 1000);
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.className = 'btn btn-warning';
|
||||
button.disabled = false;
|
||||
}, 3000);
|
||||
} else {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
alert('Update-Fehler: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
alert('Netzwerkfehler beim Update: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
})
|
||||
.catch(error => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
alert('Fehler beim Update: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter Setup
|
||||
function setupFilters() {
|
||||
document.getElementById('searchFilter').addEventListener('input', filterProjects);
|
||||
document.getElementById('languageFilter').addEventListener('change', filterProjects);
|
||||
document.getElementById('categoryFilter').addEventListener('change', filterProjects);
|
||||
document.getElementById('sortBy').addEventListener('change', sortProjects);
|
||||
document.getElementById('showInstalledFilter').addEventListener('change', filterProjects);
|
||||
}
|
||||
|
||||
// Projekte filtern
|
||||
function filterProjects() {
|
||||
const searchTerm = document.getElementById('searchFilter').value.toLowerCase();
|
||||
const languageFilter = document.getElementById('languageFilter').value.toLowerCase();
|
||||
const categoryFilter = document.getElementById('categoryFilter').value.toLowerCase();
|
||||
const showInstalled = document.getElementById('showInstalledFilter').checked;
|
||||
|
||||
const projectCards = document.querySelectorAll('.project-card-wrapper');
|
||||
|
||||
projectCards.forEach(card => {
|
||||
const name = card.dataset.name;
|
||||
const language = card.dataset.language;
|
||||
const category = card.dataset.category;
|
||||
const tags = card.dataset.tags;
|
||||
const projectName = card.querySelector('.card-title').textContent.toLowerCase();
|
||||
const isInstalled = installedProjects.includes(card.querySelector('[data-name]').dataset.name);
|
||||
|
||||
let show = true;
|
||||
|
||||
// Textsuche
|
||||
if (searchTerm && !name.includes(searchTerm) && !tags.includes(searchTerm) && !projectName.includes(searchTerm)) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// Sprachfilter
|
||||
if (languageFilter && language !== languageFilter) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// Kategoriefilter
|
||||
if (categoryFilter && category !== categoryFilter) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// Installierte anzeigen Filter
|
||||
if (showInstalled && !isInstalled) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
card.style.display = show ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Projekte sortieren
|
||||
function sortProjects() {
|
||||
const sortBy = document.getElementById('sortBy').value;
|
||||
const container = document.getElementById('projectContainer');
|
||||
const cards = Array.from(container.querySelectorAll('.project-card-wrapper'));
|
||||
|
||||
cards.sort((a, b) => {
|
||||
let aVal, bVal;
|
||||
|
||||
switch(sortBy) {
|
||||
case 'name':
|
||||
aVal = a.dataset.name;
|
||||
bVal = b.dataset.name;
|
||||
break;
|
||||
case 'updated':
|
||||
aVal = a.dataset.updated;
|
||||
bVal = b.dataset.updated;
|
||||
break;
|
||||
case 'rating':
|
||||
aVal = parseFloat(a.dataset.rating);
|
||||
bVal = parseFloat(b.dataset.rating);
|
||||
return bVal - aVal; // Absteigend
|
||||
case 'downloads':
|
||||
aVal = parseInt(a.dataset.downloads);
|
||||
bVal = parseInt(b.dataset.downloads);
|
||||
return bVal - aVal; // Absteigend
|
||||
case 'size':
|
||||
aVal = parseInt(a.dataset.size);
|
||||
bVal = parseInt(b.dataset.size);
|
||||
return aVal - bVal; // Aufsteigend
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
return aVal.localeCompare(bVal);
|
||||
});
|
||||
|
||||
// Sortierte Cards wieder einfügen
|
||||
cards.forEach(card => container.appendChild(card));
|
||||
}
|
||||
|
||||
// Masseninstallation
|
||||
function showBulkInstall() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('bulkInstallModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function startBulkInstall() {
|
||||
const checkboxes = document.querySelectorAll('#bulkInstallList input[type="checkbox"]:checked');
|
||||
const selectedApps = Array.from(checkboxes).map(cb => ({
|
||||
url: cb.value,
|
||||
name: cb.dataset.name
|
||||
}));
|
||||
|
||||
if (selectedApps.length === 0) {
|
||||
alert('Bitte wählen Sie mindestens eine App aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`${selectedApps.length} Apps werden installiert. Fortfahren?`)) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('bulkInstallModal')).hide();
|
||||
installAppsSequentially(selectedApps, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function installAppsSequentially(apps, index) {
|
||||
if (index >= apps.length) {
|
||||
alert('Alle Apps wurden installiert!');
|
||||
loadInstalledProjects(); // Refresh
|
||||
return;
|
||||
}
|
||||
|
||||
const app = apps[index];
|
||||
console.log(`Installiere ${index + 1}/${apps.length}: ${app.name}`);
|
||||
|
||||
fetch('/install_project', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `project_url=${encodeURIComponent(app.url)}&project_name=${encodeURIComponent(app.name)}`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(`${app.name}: ${data.success ? 'Erfolgreich' : 'Fehler'} - ${data.message}`);
|
||||
setTimeout(() => installAppsSequentially(apps, index + 1), 1000);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Fehler bei ${app.name}:`, error);
|
||||
setTimeout(() => installAppsSequentially(apps, index + 1), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Projektdetails anzeigen
|
||||
function showProjectDetails(projectName) {
|
||||
// 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;">
|
||||
</div>
|
||||
`).join('')}
|
||||
</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>
|
||||
</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>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('projectDetailsContent').innerHTML = content;
|
||||
|
||||
// Install Button Setup
|
||||
const installBtn = document.getElementById('installFromDetails');
|
||||
installBtn.onclick = () => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('projectDetailsModal')).hide();
|
||||
installProject(project.url, project.name);
|
||||
};
|
||||
|
||||
// Modal anzeigen
|
||||
const modal = new bootstrap.Modal(document.getElementById('projectDetailsModal'));
|
||||
modal.show();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="fas fa-cube me-2"></i>Docker Status & Diagnose</h2>
|
||||
<h2><i class="fas fa-docker me-2"></i>Docker Status & Diagnose</h2>
|
||||
<div>
|
||||
<button class="btn btn-outline-secondary me-2" onclick="reconnectDocker()">
|
||||
<i class="fas fa-sync-alt"></i> Neu verbinden
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="stat-item">
|
||||
<i class="fas fa-cube fa-2x mb-2" id="dockerIcon"></i>
|
||||
<i class="fas fa-docker fa-2x mb-2" id="dockerIcon"></i>
|
||||
<div class="stat-number" id="dockerStatus">Prüfung...</div>
|
||||
<div class="text-muted">Docker Status</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user