modified: app.py
modified: config.json modified: templates/available_projects.html modified: templates/base.html new file: templates/custom_install.html
This commit is contained in:
91
app.py
91
app.py
@@ -9,6 +9,7 @@ import shutil
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import tempfile
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
import docker
|
||||
import yaml
|
||||
@@ -2283,5 +2284,93 @@ def api_refresh_projects():
|
||||
'error': f'Fehler beim Aktualisieren: {str(e)}'
|
||||
})
|
||||
|
||||
@app.route('/custom_install/<project_name>')
|
||||
def custom_install(project_name):
|
||||
"""Custom Installation Seite für ein Projekt"""
|
||||
config = project_manager.load_config()
|
||||
|
||||
# Finde das Projekt in der Konfiguration
|
||||
project = None
|
||||
for p in config.get('projects', []):
|
||||
if p.get('name') == project_name:
|
||||
project = p
|
||||
break
|
||||
|
||||
if not project:
|
||||
flash('Projekt nicht gefunden', 'error')
|
||||
return redirect(url_for('available_projects'))
|
||||
|
||||
# Ermittle verfügbare Installationsmethoden
|
||||
available_methods = []
|
||||
preferred_method = None
|
||||
|
||||
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):
|
||||
available_methods.append((method_type, method_info))
|
||||
|
||||
# Prüfe ob dies die bevorzugte Methode ist
|
||||
if project['installation'].get('preferred') == method_type:
|
||||
preferred_method = (method_type, method_info)
|
||||
|
||||
# Falls keine bevorzugte Methode gesetzt, nimm die erste verfügbare
|
||||
if not preferred_method and available_methods:
|
||||
preferred_method = available_methods[0]
|
||||
else:
|
||||
# Fallback für alte Projekte ohne neues Schema
|
||||
if 'url' in project:
|
||||
method_info = {
|
||||
'url': project['url'],
|
||||
'description': 'Quellcode klonen und bauen',
|
||||
'available': True
|
||||
}
|
||||
available_methods = [('clone', method_info)]
|
||||
preferred_method = ('clone', method_info)
|
||||
|
||||
return render_template('custom_install.html',
|
||||
project=project,
|
||||
available_methods=available_methods,
|
||||
preferred_method=preferred_method)
|
||||
|
||||
def check_port_available(port):
|
||||
"""Prüfe ob ein Port verfügbar ist"""
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(1)
|
||||
result = sock.connect_ex(('localhost', int(port)))
|
||||
return result != 0 # Port ist frei wenn Verbindung fehlschlägt
|
||||
except:
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
|
||||
# Finde verfügbaren Port
|
||||
port = 5000
|
||||
while not check_port_available(port) and port < 5010:
|
||||
port += 1
|
||||
|
||||
if port >= 5010:
|
||||
print("❌ Keine verfügbaren Ports gefunden (5000-5009)")
|
||||
exit(1)
|
||||
|
||||
# Zeige Startup-Informationen
|
||||
print(f"📱 Flask App läuft auf: http://localhost:{port}")
|
||||
print(f"🐳 Docker Status: {'✅ Verfügbar' if project_manager.docker_available else '❌ Nicht verfügbar'}")
|
||||
print(f"📁 Projekte Ordner: {os.path.abspath(PROJECTS_DIR)}")
|
||||
print(f"⚙️ Apps Ordner: {os.path.abspath(APPS_DIR)}")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
app.run(
|
||||
host='0.0.0.0',
|
||||
port=port,
|
||||
debug=True,
|
||||
use_reloader=False # Verhindert doppelte Initialisierung
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\n" + "=" * 60)
|
||||
print("🛑 App Installer wurde beendet")
|
||||
print("=" * 60)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Fehler beim Starten der App: {e}")
|
||||
print("=" * 60)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"quiz",
|
||||
"web",
|
||||
"javascript",
|
||||
"node.js",
|
||||
"Python",
|
||||
"interactive",
|
||||
"realtime",
|
||||
"multiplayer"
|
||||
@@ -22,9 +22,9 @@
|
||||
"environment_vars": {
|
||||
"NODE_ENV": "production",
|
||||
"PORT": "3000",
|
||||
"DATABASE_URL": "sqlite:///data/quiz.db",
|
||||
"JWT_SECRET": "your-secret-key-here",
|
||||
"ADMIN_PASSWORD": "admin123"
|
||||
"DATABASE_URL": "",
|
||||
"JWT_SECRET": "",
|
||||
"ADMIN_PASSWORD": ""
|
||||
},
|
||||
"requirements": {
|
||||
"docker": true,
|
||||
|
||||
@@ -247,9 +247,11 @@
|
||||
{% endfor %}
|
||||
|
||||
{% if available_methods|length > 1 %}
|
||||
<!-- Dropdown wenn mehrere Methoden verfügbar -->
|
||||
<!-- Split-Button: Links = Preferred Install, Rechts = Custom Install -->
|
||||
<div class="btn-group w-100" role="group">
|
||||
<button class="btn btn-success install-btn flex-grow-1"
|
||||
<!-- Linke Hälfte: Preferred Installation -->
|
||||
<button class="btn btn-success install-btn"
|
||||
style="flex: 1 1 70%"
|
||||
data-url="{{ preferred.url }}"
|
||||
data-name="{{ project.name }}"
|
||||
data-method="{{ preferred_method }}"
|
||||
@@ -263,56 +265,37 @@
|
||||
<i class="fab fa-git-alt me-1"></i>Code
|
||||
{% endif %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown" aria-expanded="false" style="flex: 0 0 auto;">
|
||||
<span class="visually-hidden">Weitere Optionen</span>
|
||||
<!-- Rechte Hälfte: Custom Install -->
|
||||
<button type="button" class="btn btn-outline-success"
|
||||
style="flex: 0 0 30%"
|
||||
onclick="openCustomInstall('{{ project.name }}')">
|
||||
<i class="fas fa-cog"></i> Custom
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for method_type, method_info in available_methods %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="#"
|
||||
onclick="installProject('{{ method_info.url }}', '{{ project.name }}', '{{ method_type }}'); return false;">
|
||||
{% if method_type == 'image' %}
|
||||
<i class="fab fa-docker me-2 text-primary"></i>Docker Image herunterladen
|
||||
{% elif method_type == 'docker_registry' %}
|
||||
<i class="fab fa-docker me-2 text-info"></i>Docker Registry Pull
|
||||
{% elif method_type == 'docker_url' %}
|
||||
<i class="fas fa-cloud-download-alt me-2 text-warning"></i>Docker .tar von URL laden
|
||||
{% elif method_type == 'docker_file' %}
|
||||
<i class="fas fa-file-archive me-2 text-warning"></i>Docker-Image-Datei herunterladen
|
||||
{% elif method_type == 'docker_build' or method_type == 'dockerfile' %}
|
||||
<i class="fas fa-cog me-2 text-secondary"></i>Mit Dockerfile bauen
|
||||
{% elif method_type == 'clone' %}
|
||||
<i class="fab fa-git-alt me-2 text-success"></i>Quellcode klonen
|
||||
{% else %}
|
||||
<i class="fas fa-download me-2"></i>{{ method_type|title }}
|
||||
{% endif %}
|
||||
<small class="text-muted d-block">{{ method_info.description }}</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="showProjectDetails('{{ project.name }}'); return false;">
|
||||
<i class="fas fa-info-circle me-2 text-info"></i>Projektdetails anzeigen
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Einzelner Button wenn nur eine Methode verfügbar -->
|
||||
<button class="btn btn-success install-btn w-100"
|
||||
data-url="{{ preferred.url }}"
|
||||
data-name="{{ project.name }}"
|
||||
data-method="{{ preferred_method }}"
|
||||
onclick="installProject('{{ preferred.url }}', '{{ project.name }}', '{{ preferred_method }}')">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% if preferred_method == 'image' %}
|
||||
<i class="fab fa-docker me-1"></i>Docker installieren
|
||||
{% else %}
|
||||
<i class="fab fa-git-alt me-1"></i>Code installieren
|
||||
{% endif %}
|
||||
</button>
|
||||
<!-- Split-Button auch bei einer Methode für Konsistenz -->
|
||||
<div class="btn-group w-100" role="group">
|
||||
<!-- Linke Hälfte: Einzige verfügbare Installation -->
|
||||
<button class="btn btn-success install-btn"
|
||||
style="flex: 1 1 70%"
|
||||
data-url="{{ preferred.url }}"
|
||||
data-name="{{ project.name }}"
|
||||
data-method="{{ preferred_method }}"
|
||||
onclick="installProject('{{ preferred.url }}', '{{ project.name }}', '{{ preferred_method }}')">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% if preferred_method == 'image' %}
|
||||
<i class="fab fa-docker me-1"></i>Docker installieren
|
||||
{% else %}
|
||||
<i class="fab fa-git-alt me-1"></i>Code installieren
|
||||
{% endif %}
|
||||
</button>
|
||||
<!-- Rechte Hälfte: Details anzeigen -->
|
||||
<button type="button" class="btn btn-outline-success"
|
||||
style="flex: 0 0 30%"
|
||||
onclick="openCustomInstall('{{ project.name }}')">
|
||||
<i class="fas fa-info"></i> Info
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
@@ -555,6 +538,22 @@
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Split-Button Styling für bessere UX */
|
||||
.btn-group .btn:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child:hover {
|
||||
border-left: 1px solid rgba(255,255,255,0.4);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
let installedProjects = [];
|
||||
@@ -1389,5 +1388,10 @@ function showAlert(type, message) {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Custom Install Page öffnen
|
||||
function openCustomInstall(projectName) {
|
||||
window.location.href = `/custom_install/${encodeURIComponent(projectName)}`;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -548,10 +548,18 @@
|
||||
}, 30000);
|
||||
|
||||
// Installationsfortschritt
|
||||
function installProject(url, name) {
|
||||
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...';
|
||||
|
||||
// Spezifischer Loading-Text basierend auf Methode
|
||||
if (method === 'image' || method === 'docker_registry' || method === 'docker_url') {
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Docker lädt...';
|
||||
} else if (method === 'docker_build' || method === 'dockerfile') {
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gebaut...';
|
||||
} else {
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installiere...';
|
||||
}
|
||||
button.disabled = true;
|
||||
|
||||
fetch('/install_project', {
|
||||
@@ -559,7 +567,7 @@
|
||||
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 => {
|
||||
|
||||
396
templates/custom_install.html
Normal file
396
templates/custom_install.html
Normal file
@@ -0,0 +1,396 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ project.name }} - Installation - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2>
|
||||
<i class="fas fa-download me-2"></i>{{ project.name }} Installation
|
||||
</h2>
|
||||
<p class="text-muted mb-0">{{ project.description }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url_for('available_projects') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Zurück zur Liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Linke Spalte: Projektinfo -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Projekt-Übersicht -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Projekt-Übersicht
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if project.language %}
|
||||
<div class="mb-2">
|
||||
<strong>Sprache:</strong>
|
||||
<span class="badge bg-primary">{{ project.language }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.category %}
|
||||
<div class="mb-2">
|
||||
<strong>Kategorie:</strong>
|
||||
<span class="badge bg-info">{{ project.category }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.tags %}
|
||||
<div class="mb-2">
|
||||
<strong>Tags:</strong><br>
|
||||
{% for tag in project.tags %}
|
||||
<span class="badge bg-secondary me-1">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.requirements %}
|
||||
<div class="mb-2">
|
||||
<strong>Anforderungen:</strong>
|
||||
<ul class="small mt-1 mb-0">
|
||||
{% if project.requirements.min_memory %}
|
||||
<li>RAM: {{ project.requirements.min_memory }}</li>
|
||||
{% endif %}
|
||||
{% if project.requirements.min_disk %}
|
||||
<li>Speicher: {{ project.requirements.min_disk }}</li>
|
||||
{% endif %}
|
||||
{% if project.requirements.ports %}
|
||||
<li>Ports: {{ project.requirements.ports|join(', ') }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empfohlene Installation -->
|
||||
{% if preferred_method %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-star me-2"></i>Empfohlene Installation
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<strong>{{ preferred_method[0]|title }}</strong>
|
||||
<p class="text-muted small mb-0">{{ preferred_method[1].description }}</p>
|
||||
</div>
|
||||
<div>
|
||||
{% if preferred_method[0] in ['image', 'docker_registry', 'docker_url', 'docker_file'] %}
|
||||
<i class="fab fa-docker fa-2x text-primary"></i>
|
||||
{% elif preferred_method[0] in ['docker_build', 'dockerfile'] %}
|
||||
<i class="fas fa-cog fa-2x text-secondary"></i>
|
||||
{% else %}
|
||||
<i class="fab fa-git-alt fa-2x text-success"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-success w-100"
|
||||
onclick="installProject('{{ preferred_method[1].url }}', '{{ project.name }}', '{{ preferred_method[0] }}')">
|
||||
<i class="fas fa-download me-2"></i>Jetzt installieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Schnellinfo -->
|
||||
{% if project.metadata %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-chart-line me-2"></i>Projekt-Info
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if project.metadata.author %}
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Autor:</span>
|
||||
<span>{{ project.metadata.author }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.metadata.version %}
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Version:</span>
|
||||
<span class="badge bg-primary">{{ project.metadata.version }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.metadata.last_updated %}
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Letzte Aktualisierung:</span>
|
||||
<span>{{ project.metadata.last_updated }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.metadata.downloads %}
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Downloads:</span>
|
||||
<span class="badge bg-info">{{ project.metadata.downloads }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.metadata.stars %}
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Stars:</span>
|
||||
<span class="badge bg-warning">{{ project.metadata.stars }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte: Installationsmethoden -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-tools me-2"></i>Verfügbare Installationsmethoden
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if available_methods %}
|
||||
<div class="row">
|
||||
{% for method_type, method_info in available_methods %}
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100 {% if preferred_method and method_type == preferred_method[0] %}border-success{% endif %}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
{% if method_type == 'image' %}
|
||||
<i class="fab fa-docker fa-lg text-primary me-2"></i>
|
||||
<strong>Docker Image</strong>
|
||||
{% elif method_type == 'docker_registry' %}
|
||||
<i class="fab fa-docker fa-lg text-info me-2"></i>
|
||||
<strong>Docker Registry</strong>
|
||||
{% elif method_type == 'docker_url' %}
|
||||
<i class="fas fa-cloud-download-alt fa-lg text-warning me-2"></i>
|
||||
<strong>Docker URL</strong>
|
||||
{% elif method_type == 'docker_file' %}
|
||||
<i class="fas fa-file-archive fa-lg text-warning me-2"></i>
|
||||
<strong>Docker File</strong>
|
||||
{% elif method_type == 'docker_build' or method_type == 'dockerfile' %}
|
||||
<i class="fas fa-cog fa-lg text-secondary me-2"></i>
|
||||
<strong>Docker Build</strong>
|
||||
{% elif method_type == 'clone' %}
|
||||
<i class="fab fa-git-alt fa-lg text-success me-2"></i>
|
||||
<strong>Git Clone</strong>
|
||||
{% elif method_type == 'native_python' %}
|
||||
<i class="fab fa-python fa-lg text-success me-2"></i>
|
||||
<strong>Python Native</strong>
|
||||
{% elif method_type == 'native_nodejs' %}
|
||||
<i class="fab fa-node-js fa-lg text-success me-2"></i>
|
||||
<strong>Node.js Native</strong>
|
||||
{% elif method_type == 'native_batch' %}
|
||||
<i class="fas fa-terminal fa-lg text-info me-2"></i>
|
||||
<strong>Windows Batch</strong>
|
||||
{% elif method_type == 'native_shell' %}
|
||||
<i class="fas fa-terminal fa-lg text-success me-2"></i>
|
||||
<strong>Linux/macOS Shell</strong>
|
||||
{% else %}
|
||||
<i class="fas fa-download fa-lg me-2"></i>
|
||||
<strong>{{ method_type|title }}</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if preferred_method and method_type == preferred_method[0] %}
|
||||
<span class="badge bg-success">Empfohlen</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<p class="text-muted mb-3">{{ method_info.description }}</p>
|
||||
|
||||
<!-- Zusätzliche Infos je nach Typ -->
|
||||
{% if method_type in ['image', 'docker_registry', 'docker_url', 'docker_file'] %}
|
||||
<div class="mb-3">
|
||||
<small class="text-info">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Fertige Container-Installation - Schnell und einfach
|
||||
</small>
|
||||
</div>
|
||||
{% elif method_type in ['docker_build', 'dockerfile'] %}
|
||||
<div class="mb-3">
|
||||
<small class="text-warning">
|
||||
<i class="fas fa-clock me-1"></i>
|
||||
Erstellt Container aus Quellcode - Dauert länger
|
||||
</small>
|
||||
</div>
|
||||
{% elif method_type == 'clone' %}
|
||||
<div class="mb-3">
|
||||
<small class="text-success">
|
||||
<i class="fas fa-code me-1"></i>
|
||||
Vollständiger Quellcode - Maximale Kontrolle
|
||||
</small>
|
||||
</div>
|
||||
{% elif method_type.startswith('native_') %}
|
||||
<div class="mb-3">
|
||||
<small class="text-primary">
|
||||
<i class="fas fa-desktop me-1"></i>
|
||||
Native Ausführung - Ohne Container
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if method_info.url %}
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">
|
||||
<strong>URL:</strong>
|
||||
<code class="small">{{ method_info.url|truncate(50) }}</code>
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-auto">
|
||||
<button class="btn {% if preferred_method and method_type == preferred_method[0] %}btn-success{% else %}btn-outline-primary{% endif %} w-100"
|
||||
onclick="installProject('{{ method_info.url }}', '{{ project.name }}', '{{ method_type }}')">
|
||||
<i class="fas fa-download me-2"></i>
|
||||
{% if preferred_method and method_type == preferred_method[0] %}
|
||||
Empfohlene Installation
|
||||
{% else %}
|
||||
Mit dieser Methode installieren
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-exclamation-triangle fa-3x text-warning mb-3"></i>
|
||||
<h5 class="text-muted">Keine Installationsmethoden verfügbar</h5>
|
||||
<p class="text-muted">Für dieses Projekt sind keine Installationsmethoden konfiguriert.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Modal -->
|
||||
<div class="modal fade" id="installProgressModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-download me-2"></i>Installation läuft...
|
||||
</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5 id="installStatus">{{ project.name }} wird installiert...</h5>
|
||||
<p class="text-muted mb-0" id="installMethod">Bitte warten Sie einen Moment.</p>
|
||||
</div>
|
||||
<div class="progress mt-3">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Installation mit Progress Modal
|
||||
function installProject(url, name, method) {
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
|
||||
// Show progress modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('installProgressModal'));
|
||||
document.getElementById('installStatus').textContent = `${name} wird installiert...`;
|
||||
document.getElementById('installMethod').textContent = `Methode: ${method}`;
|
||||
modal.show();
|
||||
|
||||
// Disable all install buttons
|
||||
document.querySelectorAll('.btn').forEach(btn => {
|
||||
if (btn.textContent.includes('installieren')) {
|
||||
btn.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)}&installation_method=${encodeURIComponent(method)}`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
modal.hide();
|
||||
|
||||
if (data.success) {
|
||||
// Success notification
|
||||
showAlert('success', `${name} wurde erfolgreich installiert!`);
|
||||
|
||||
// Redirect to project details or dashboard after 2 seconds
|
||||
setTimeout(() => {
|
||||
window.location.href = '/project_details/' + encodeURIComponent(name);
|
||||
}, 2000);
|
||||
} else {
|
||||
showAlert('danger', `Installation fehlgeschlagen: ${data.message}`);
|
||||
|
||||
// Re-enable buttons
|
||||
document.querySelectorAll('.btn').forEach(btn => {
|
||||
if (btn.textContent.includes('installieren')) {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
modal.hide();
|
||||
showAlert('danger', `Netzwerkfehler bei Installation von ${name}: ${error}`);
|
||||
|
||||
// Re-enable buttons
|
||||
document.querySelectorAll('.btn').forEach(btn => {
|
||||
if (btn.textContent.includes('installieren')) {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showAlert(type, message) {
|
||||
const alertHtml = `
|
||||
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-triangle'} me-2"></i>
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert alert at top of page
|
||||
const container = document.querySelector('.container-fluid');
|
||||
container.insertAdjacentHTML('afterbegin', alertHtml);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
const alert = container.querySelector('.alert');
|
||||
if (alert) {
|
||||
alert.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user