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:
SimolZimol
2025-07-08 17:53:27 +02:00
parent d11af08f32
commit d6ae4853d8
5 changed files with 554 additions and 57 deletions

91
app.py
View File

@@ -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)

View File

@@ -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,

View File

@@ -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 %}

View File

@@ -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 => {

View 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 %}