new file: README.md
new file: app.py new file: config.example.json new file: config.json new file: docker_diagnose.py new file: requirements.txt new file: start.bat new file: templates/available_projects.html new file: templates/base.html new file: templates/config.html new file: templates/docker_status.html new file: templates/index.html new file: templates/project_details.html new file: templates/project_details_fixed.html new file: templates/project_details_new.html new file: templates/project_details_old.html .gitignore
This commit is contained in:
191
README.md
Normal file
191
README.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# App Installer & Manager
|
||||
|
||||
Ein Flask-basierter App-Installer und -Manager für Docker-basierte Projekte.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 **Automatische Installation** von Git-Repositories mit Dockerfile
|
||||
- 🔧 **Konfigurationsverwaltung** für .env-Dateien
|
||||
- 🐳 **Docker-Integration** für Build, Start, Stop und Monitoring
|
||||
- 📊 **Web-Dashboard** für Projektübersicht und -verwaltung
|
||||
- 🔄 **Automatische Updates** der Projektliste
|
||||
- 📋 **Masseninstallation** mehrerer Projekte
|
||||
- 🛠️ **Container-Monitoring** mit Logs und Statistiken
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Dependencies installieren:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **Docker sicherstellen:**
|
||||
```bash
|
||||
docker --version
|
||||
git --version
|
||||
```
|
||||
|
||||
3. **Anwendung starten:**
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
4. **Browser öffnen:**
|
||||
```
|
||||
http://localhost:5000
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Projektliste URL
|
||||
|
||||
Konfiguriere eine URL, die eine Liste von Git-Repositories zurückgibt. Unterstützte Formate:
|
||||
|
||||
**JSON Format:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"url": "https://gitea.simolzimol.net/Simon/quizify",
|
||||
"name": "quizify",
|
||||
"description": "Ein interaktives Quiz-System",
|
||||
"language": "JavaScript",
|
||||
"tags": ["quiz", "web"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Gitea API Beispiel:**
|
||||
```
|
||||
https://gitea.simolzimol.net/api/v1/repos/search?sort=updated&order=desc&limit=50
|
||||
```
|
||||
|
||||
**GitHub API Beispiel:**
|
||||
```
|
||||
https://api.github.com/users/USERNAME/repos
|
||||
```
|
||||
|
||||
### Projekt-Anforderungen
|
||||
|
||||
Jedes Projekt sollte enthalten:
|
||||
- `Dockerfile` - Für Container-Builds
|
||||
- `.env.example` - Für Umgebungskonfiguration
|
||||
- Optional: `docker-compose.yml`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /` - Dashboard
|
||||
- `GET /available_projects` - Verfügbare Projekte
|
||||
- `GET /config` - Konfiguration
|
||||
- `POST /install_project` - Projekt installieren
|
||||
- `GET /build_project/<name>` - Projekt bauen
|
||||
- `GET /start_project/<name>` - Projekt starten
|
||||
- `GET /stop_project/<name>` - Projekt stoppen
|
||||
- `GET /project_details/<name>` - Projektdetails
|
||||
|
||||
## Verzeichnisstruktur
|
||||
|
||||
```
|
||||
app installer/
|
||||
├── app.py # Hauptanwendung
|
||||
├── requirements.txt # Python Dependencies
|
||||
├── config.json # Konfigurationsdatei
|
||||
├── config.example.json # Beispielkonfiguration
|
||||
├── templates/ # HTML Templates
|
||||
│ ├── base.html
|
||||
│ ├── index.html
|
||||
│ ├── available_projects.html
|
||||
│ ├── config.html
|
||||
│ └── project_details.html
|
||||
├── projects/ # Geklonte Projekte
|
||||
└── apps/ # Laufende Apps
|
||||
```
|
||||
|
||||
## Unterstützte Git-Provider
|
||||
|
||||
- ✅ Gitea
|
||||
- ✅ GitHub
|
||||
- ✅ GitLab
|
||||
- ✅ Beliebige Git-URLs
|
||||
|
||||
## Docker-Features
|
||||
|
||||
- Automatischer Build von Dockerfiles
|
||||
- Port-Management (8080, 8443, custom)
|
||||
- Container-Status-Monitoring
|
||||
- Log-Anzeige in Echtzeit
|
||||
- Resource-Monitoring (CPU, RAM, Netzwerk)
|
||||
- Backup & Restore von Containern
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- Isolierte Container-Umgebungen
|
||||
- Konfigurierbare Port-Bereiche
|
||||
- Sichere .env-Dateiverwaltung
|
||||
- Optional: Docker Registry Integration
|
||||
|
||||
## Erweiterte Features
|
||||
|
||||
### Masseninstallation
|
||||
Installiere mehrere Projekte gleichzeitig über das Web-Interface.
|
||||
|
||||
### Automatische Updates
|
||||
Projektlisten werden automatisch aktualisiert basierend auf der konfigurierten Frequenz.
|
||||
|
||||
### Backup & Restore
|
||||
- Automatische Backups vor Updates
|
||||
- Manuelle Backup-Erstellung
|
||||
- Wiederherstellung aus Backups
|
||||
|
||||
### Monitoring
|
||||
- Container-Ressourcenverbrauch
|
||||
- Netzwerk-Statistiken
|
||||
- Log-Aggregation
|
||||
- Status-Dashboard
|
||||
|
||||
## Beispiel-Projekt
|
||||
|
||||
Für ein Projekt wie `https://gitea.simolzimol.net/Simon/quizify`:
|
||||
|
||||
1. Repository wird automatisch geklont
|
||||
2. `.env.example` → `.env` kopiert
|
||||
3. Dockerfile wird gebaut
|
||||
4. Container wird gestartet
|
||||
5. Web-Interface zeigt Status an
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker nicht verfügbar
|
||||
```bash
|
||||
# Windows
|
||||
Install Docker Desktop
|
||||
|
||||
# Linux
|
||||
sudo systemctl start docker
|
||||
```
|
||||
|
||||
### Port bereits belegt
|
||||
Der Installer erkennt belegte Ports automatisch und schlägt Alternativen vor.
|
||||
|
||||
### Build-Fehler
|
||||
Überprüfe das Dockerfile und die .env-Konfiguration im Projektdetail-Panel.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Kubernetes-Unterstützung
|
||||
- [ ] Multi-User-Management
|
||||
- [ ] SSL/TLS-Zertifikat-Management
|
||||
- [ ] Plugin-System
|
||||
- [ ] Mobile-optimierte UI
|
||||
- [ ] CI/CD-Integration
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT License - Siehe LICENSE Datei für Details.
|
||||
|
||||
## Beitrag
|
||||
|
||||
1. Fork das Repository
|
||||
2. Erstelle einen Feature-Branch
|
||||
3. Committe deine Änderungen
|
||||
4. Push zum Branch
|
||||
5. Erstelle einen Pull Request
|
||||
14
config.example.json
Normal file
14
config.example.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"project_list_url": "https://gitea.simolzimol.net/api/v1/repos/search?sort=updated&order=desc&limit=50",
|
||||
"auto_refresh_minutes": 30,
|
||||
"docker_registry": "",
|
||||
"projects": [
|
||||
{
|
||||
"url": "https://gitea.simolzimol.net/Simon/quizify",
|
||||
"name": "quizify",
|
||||
"description": "Ein interaktives Quiz-System",
|
||||
"language": "JavaScript",
|
||||
"tags": ["quiz", "web", "javascript"]
|
||||
}
|
||||
]
|
||||
}
|
||||
14
config.json
Normal file
14
config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"project_list_url": "https://gitea.simolzimol.net/api/v1/repos/search?sort=updated&order=desc&limit=50",
|
||||
"auto_refresh_minutes": 30,
|
||||
"docker_registry": "",
|
||||
"projects": [
|
||||
{
|
||||
"url": "https://gitea.simolzimol.net/Simon/quizify",
|
||||
"name": "quizify",
|
||||
"description": "Ein interaktives Quiz-System",
|
||||
"language": "JavaScript",
|
||||
"tags": ["quiz", "web", "javascript"]
|
||||
}
|
||||
]
|
||||
}
|
||||
237
docker_diagnose.py
Normal file
237
docker_diagnose.py
Normal file
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Docker Diagnose Tool für App Installer
|
||||
Hilft bei der Diagnose von Docker-Problemen
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
def run_command(cmd, timeout=10):
|
||||
"""Führe Kommando aus und gib Ergebnis zurück"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
shell=True
|
||||
)
|
||||
return {
|
||||
'success': result.returncode == 0,
|
||||
'stdout': result.stdout.strip(),
|
||||
'stderr': result.stderr.strip(),
|
||||
'returncode': result.returncode
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
'success': False,
|
||||
'stdout': '',
|
||||
'stderr': f'Timeout nach {timeout} Sekunden',
|
||||
'returncode': -1
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'stdout': '',
|
||||
'stderr': str(e),
|
||||
'returncode': -1
|
||||
}
|
||||
|
||||
def check_docker_installation():
|
||||
"""Prüfe Docker Installation"""
|
||||
print("🔍 Prüfe Docker Installation...")
|
||||
|
||||
result = run_command("docker --version")
|
||||
if result['success']:
|
||||
print(f"✅ Docker installiert: {result['stdout']}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Docker nicht gefunden: {result['stderr']}")
|
||||
print(" 💡 Installiere Docker Desktop von https://docker.com")
|
||||
return False
|
||||
|
||||
def check_docker_daemon():
|
||||
"""Prüfe Docker Daemon Status"""
|
||||
print("\n🔍 Prüfe Docker Daemon...")
|
||||
|
||||
result = run_command("docker info")
|
||||
if result['success']:
|
||||
print("✅ Docker Daemon läuft")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Docker Daemon nicht erreichbar: {result['stderr']}")
|
||||
print(" 💡 Starte Docker Desktop")
|
||||
return False
|
||||
|
||||
def check_docker_permissions():
|
||||
"""Prüfe Docker Berechtigungen"""
|
||||
print("\n🔍 Prüfe Docker Berechtigungen...")
|
||||
|
||||
result = run_command("docker ps")
|
||||
if result['success']:
|
||||
print("✅ Docker Berechtigungen OK")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Docker Berechtigungsfehler: {result['stderr']}")
|
||||
if "permission denied" in result['stderr'].lower():
|
||||
print(" 💡 Starte als Administrator oder füge User zur docker Gruppe hinzu")
|
||||
return False
|
||||
|
||||
def check_docker_images():
|
||||
"""Prüfe vorhandene Docker Images"""
|
||||
print("\n🔍 Prüfe Docker Images...")
|
||||
|
||||
result = run_command("docker images --format json")
|
||||
if result['success']:
|
||||
lines = result['stdout'].split('\n') if result['stdout'] else []
|
||||
images = []
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
try:
|
||||
img = json.loads(line)
|
||||
images.append(img)
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f"✅ {len(images)} Docker Images gefunden")
|
||||
for img in images[:5]: # Zeige ersten 5
|
||||
print(f" - {img.get('Repository', 'unknown')}:{img.get('Tag', 'unknown')}")
|
||||
if len(images) > 5:
|
||||
print(f" ... und {len(images) - 5} weitere")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Kann Images nicht abrufen: {result['stderr']}")
|
||||
return False
|
||||
|
||||
def check_docker_containers():
|
||||
"""Prüfe laufende Container"""
|
||||
print("\n🔍 Prüfe Docker Container...")
|
||||
|
||||
result = run_command("docker ps -a --format json")
|
||||
if result['success']:
|
||||
lines = result['stdout'].split('\n') if result['stdout'] else []
|
||||
containers = []
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
try:
|
||||
container = json.loads(line)
|
||||
containers.append(container)
|
||||
except:
|
||||
pass
|
||||
|
||||
running = [c for c in containers if c.get('State') == 'running']
|
||||
|
||||
print(f"✅ {len(containers)} Container total, {len(running)} laufend")
|
||||
for container in containers[:3]: # Zeige ersten 3
|
||||
name = container.get('Names', 'unknown')
|
||||
state = container.get('State', 'unknown')
|
||||
print(f" - {name}: {state}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Kann Container nicht abrufen: {result['stderr']}")
|
||||
return False
|
||||
|
||||
def check_docker_compose():
|
||||
"""Prüfe Docker Compose"""
|
||||
print("\n🔍 Prüfe Docker Compose...")
|
||||
|
||||
result = run_command("docker-compose --version")
|
||||
if result['success']:
|
||||
print(f"✅ Docker Compose verfügbar: {result['stdout']}")
|
||||
return True
|
||||
else:
|
||||
# Versuche neue docker compose Syntax
|
||||
result = run_command("docker compose version")
|
||||
if result['success']:
|
||||
print(f"✅ Docker Compose (v2) verfügbar: {result['stdout']}")
|
||||
return True
|
||||
else:
|
||||
print("❌ Docker Compose nicht verfügbar")
|
||||
print(" 💡 Docker Compose ist in Docker Desktop enthalten")
|
||||
return False
|
||||
|
||||
def check_network_connectivity():
|
||||
"""Prüfe Netzwerk-Konnektivität"""
|
||||
print("\n🔍 Prüfe Netzwerk-Konnektivität...")
|
||||
|
||||
# Prüfe Docker Hub Verbindung
|
||||
result = run_command("docker run --rm hello-world", timeout=30)
|
||||
if result['success']:
|
||||
print("✅ Docker Hub Verbindung OK")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Docker Hub nicht erreichbar: {result['stderr']}")
|
||||
print(" 💡 Prüfe Internetverbindung und Firewall")
|
||||
return False
|
||||
|
||||
def check_system_resources():
|
||||
"""Prüfe System-Ressourcen"""
|
||||
print("\n🔍 Prüfe System-Ressourcen...")
|
||||
|
||||
result = run_command("docker system df")
|
||||
if result['success']:
|
||||
print("✅ Docker System-Info:")
|
||||
for line in result['stdout'].split('\n'):
|
||||
if line.strip():
|
||||
print(f" {line}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Kann System-Info nicht abrufen: {result['stderr']}")
|
||||
return False
|
||||
|
||||
def suggest_solutions():
|
||||
"""Schlage Lösungen vor"""
|
||||
print("\n🛠️ Lösungsvorschläge:")
|
||||
print("1. Starte Docker Desktop neu")
|
||||
print("2. Prüfe Windows-Dienste: Docker Desktop Service")
|
||||
print("3. Prüfe Hyper-V/WSL2 Einstellungen")
|
||||
print("4. Neustart des Computers")
|
||||
print("5. Docker Desktop Neuinstallation")
|
||||
print("\n📚 Weitere Hilfe:")
|
||||
print("- Docker Docs: https://docs.docker.com/")
|
||||
print("- Docker Desktop Troubleshooting: https://docs.docker.com/desktop/troubleshoot/")
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion"""
|
||||
print("🐳 Docker Diagnose Tool")
|
||||
print("=" * 50)
|
||||
print(f"Zeitpunkt: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"System: {os.name}")
|
||||
|
||||
checks = [
|
||||
check_docker_installation,
|
||||
check_docker_daemon,
|
||||
check_docker_permissions,
|
||||
check_docker_images,
|
||||
check_docker_containers,
|
||||
check_docker_compose,
|
||||
check_network_connectivity,
|
||||
check_system_resources
|
||||
]
|
||||
|
||||
results = []
|
||||
for check in checks:
|
||||
try:
|
||||
result = check()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
print(f"❌ Fehler bei {check.__name__}: {e}")
|
||||
results.append(False)
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
success_count = sum(1 for r in results if r)
|
||||
print(f"📊 Ergebnis: {success_count}/{len(results)} Checks erfolgreich")
|
||||
|
||||
if success_count < len(results):
|
||||
suggest_solutions()
|
||||
else:
|
||||
print("🎉 Alle Docker-Checks erfolgreich! Docker ist bereit.")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Flask==2.3.3
|
||||
requests==2.31.0
|
||||
docker==6.1.3
|
||||
PyYAML==6.0.1
|
||||
python-dotenv==1.0.0
|
||||
67
start.bat
Normal file
67
start.bat
Normal file
@@ -0,0 +1,67 @@
|
||||
@echo off
|
||||
echo App Installer & Manager wird gestartet...
|
||||
echo.
|
||||
|
||||
REM Prüfe Python Installation
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo FEHLER: Python ist nicht installiert oder nicht im PATH.
|
||||
echo Bitte installieren Sie Python von https://python.org
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Prüfe Docker Installation
|
||||
docker --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo WARNUNG: Docker ist nicht verfügbar.
|
||||
echo Docker wird für die Container-Verwaltung benötigt.
|
||||
echo Bitte installieren Sie Docker Desktop.
|
||||
echo.
|
||||
)
|
||||
|
||||
REM Prüfe Git Installation
|
||||
git --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo FEHLER: Git ist nicht installiert.
|
||||
echo Bitte installieren Sie Git von https://git-scm.com
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Installiere Dependencies falls requirements.txt existiert
|
||||
if exist requirements.txt (
|
||||
echo Installiere Python-Dependencies...
|
||||
pip install -r requirements.txt
|
||||
if errorlevel 1 (
|
||||
echo FEHLER: Dependencies konnten nicht installiert werden.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
REM Erstelle Verzeichnisse
|
||||
if not exist "projects" mkdir projects
|
||||
if not exist "apps" mkdir apps
|
||||
|
||||
REM Kopiere Beispielkonfiguration falls keine existiert
|
||||
if not exist "config.json" (
|
||||
if exist "config.example.json" (
|
||||
echo Erstelle initiale Konfiguration...
|
||||
copy config.example.json config.json
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ======================================
|
||||
echo App Installer & Manager
|
||||
echo ======================================
|
||||
echo.
|
||||
echo Server startet auf: http://localhost:5000
|
||||
echo Drücken Sie Ctrl+C zum Beenden
|
||||
echo.
|
||||
|
||||
REM Starte Flask-Anwendung
|
||||
python app.py
|
||||
|
||||
pause
|
||||
310
templates/available_projects.html
Normal file
310
templates/available_projects.html
Normal file
@@ -0,0 +1,310 @@
|
||||
{% 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>
|
||||
<a href="{{ url_for('refresh_projects') }}" class="btn btn-primary">
|
||||
<i class="fas fa-sync-alt"></i> Liste aktualisieren
|
||||
</a>
|
||||
</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">
|
||||
{% for project in projects %}
|
||||
<div class="col-lg-6 col-xl-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-git-alt me-2"></i>{{ project.name or 'Unbekannt' }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">Repository URL:</small>
|
||||
<p class="card-text small font-monospace">{{ project.url }}</p>
|
||||
</div>
|
||||
|
||||
{% if project.description %}
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">Beschreibung:</small>
|
||||
<p class="card-text">{{ project.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if project.tags %}
|
||||
<div class="mb-3">
|
||||
{% for tag in project.tags %}
|
||||
<span class="badge bg-secondary me-1">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Sprache:</small><br>
|
||||
<span class="badge bg-info">{{ project.language or 'Unbekannt' }}</span>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Größe:</small><br>
|
||||
<span class="text-muted">{{ project.size or 'Unbekannt' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if project.last_updated %}
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">Letztes Update: {{ project.last_updated }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-auto">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-success" onclick="installProject('{{ project.url }}', '{{ project.name }}')">
|
||||
<i class="fas fa-download"></i> Installieren
|
||||
</button>
|
||||
<a href="{{ project.url }}" target="_blank" class="btn btn-outline-info btn-sm">
|
||||
<i class="fas fa-external-link-alt"></i> Repository anzeigen
|
||||
</a>
|
||||
</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 text-primary">{{ projects|length }}</span>
|
||||
<span class="text-muted">Verfügbare Apps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number text-success">{{ projects|selectattr('language', 'defined')|list|length }}</span>
|
||||
<span class="text-muted">Mit Sprache</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number text-info">{{ projects|selectattr('description', 'defined')|list|length }}</span>
|
||||
<span class="text-muted">Mit Beschreibung</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number text-warning">{{ projects|selectattr('tags', 'defined')|list|length }}</span>
|
||||
<span class="text-muted">Mit Tags</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 }}">
|
||||
<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>
|
||||
|
||||
<!-- Filter und Sortierung -->
|
||||
<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-filter me-2"></i>Filter & Sortierung
|
||||
</h5>
|
||||
</div>
|
||||
<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="App-Name oder Beschreibung...">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="languageFilter" class="form-label">Programmiersprache:</label>
|
||||
<select class="form-select" id="languageFilter">
|
||||
<option value="">Alle Sprachen</option>
|
||||
{% set languages = projects|map(attribute='language')|select('defined')|unique|list %}
|
||||
{% for lang in languages %}
|
||||
<option value="{{ lang }}">{{ lang }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="sortBy" class="form-label">Sortieren nach:</label>
|
||||
<select class="form-select" id="sortBy">
|
||||
<option value="name">Name</option>
|
||||
<option value="language">Sprache</option>
|
||||
<option value="updated">Letztes Update</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label"> </label>
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-outline-primary" onclick="showBulkInstall()">
|
||||
<i class="fas fa-layer-group"></i> Masseninstallation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Projektfilterung
|
||||
function filterProjects() {
|
||||
const searchTerm = document.getElementById('searchFilter').value.toLowerCase();
|
||||
const languageFilter = document.getElementById('languageFilter').value;
|
||||
const sortBy = document.getElementById('sortBy').value;
|
||||
|
||||
const projectCards = document.querySelectorAll('.col-lg-6.col-xl-4');
|
||||
|
||||
projectCards.forEach(card => {
|
||||
const projectName = card.querySelector('.card-title').textContent.toLowerCase();
|
||||
const projectDesc = card.querySelector('.card-text')?.textContent.toLowerCase() || '';
|
||||
const projectLang = card.querySelector('.badge.bg-info')?.textContent || '';
|
||||
|
||||
let show = true;
|
||||
|
||||
// Textsuche
|
||||
if (searchTerm && !projectName.includes(searchTerm) && !projectDesc.includes(searchTerm)) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// Sprachfilter
|
||||
if (languageFilter && projectLang !== languageFilter) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
card.style.display = show ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Event Listener für Filter
|
||||
document.getElementById('searchFilter').addEventListener('input', filterProjects);
|
||||
document.getElementById('languageFilter').addEventListener('change', filterProjects);
|
||||
document.getElementById('sortBy').addEventListener('change', filterProjects);
|
||||
|
||||
// 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.nextElementSibling.textContent.trim()
|
||||
}));
|
||||
|
||||
if (selectedApps.length === 0) {
|
||||
alert('Bitte wählen Sie mindestens eine App aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`${selectedApps.length} Apps werden installiert. Fortfahren?`)) {
|
||||
// Schließe Modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('bulkInstallModal')).hide();
|
||||
|
||||
// Installiere Apps nacheinander
|
||||
installAppsSequentially(selectedApps, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function installAppsSequentially(apps, index) {
|
||||
if (index >= apps.length) {
|
||||
alert('Alle Apps wurden installiert!');
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const app = apps[index];
|
||||
const progressText = `Installiere ${index + 1}/${apps.length}: ${app.name}`;
|
||||
|
||||
// Zeige Fortschritt (könnte eine bessere Progress-Bar sein)
|
||||
console.log(progressText);
|
||||
|
||||
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}`);
|
||||
// Installiere nächste App
|
||||
setTimeout(() => installAppsSequentially(apps, index + 1), 1000);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Fehler bei ${app.name}:`, error);
|
||||
// Installiere trotzdem weiter
|
||||
setTimeout(() => installAppsSequentially(apps, index + 1), 1000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
223
templates/base.html
Normal file
223
templates/base.html
Normal file
@@ -0,0 +1,223 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}App Installer & Manager{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
margin: 20px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: rgba(255, 255, 255, 0.9) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
border-radius: 20px;
|
||||
padding: 5px 12px;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-stopped {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-unknown {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.flash-messages {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('index') }}">
|
||||
<i class="fas fa-rocket me-2"></i>App Manager
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('index') }}">
|
||||
<i class="fas fa-home"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('available_projects') }}">
|
||||
<i class="fas fa-download"></i> Verfügbare Apps
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('docker_status') }}">
|
||||
<i class="fas fa-docker"></i> Docker Status
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('config') }}">
|
||||
<i class="fas fa-cog"></i> Konfiguration
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="navbar-nav">
|
||||
<a class="nav-link btn btn-outline-primary" href="{{ url_for('refresh_projects') }}">
|
||||
<i class="fas fa-sync-alt"></i> Aktualisieren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-messages">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Auto-refresh status alle 30 Sekunden
|
||||
setInterval(function() {
|
||||
if (window.location.pathname === '/') {
|
||||
location.reload();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// Installationsfortschritt
|
||||
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 btn-sm';
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
alert('Fehler: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
alert('Netzwerkfehler: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
// Bestätigungsdialog für gefährliche Aktionen
|
||||
function confirmAction(action, projectName) {
|
||||
if (action === 'remove') {
|
||||
return confirm(`Möchten Sie das Projekt "${projectName}" wirklich vollständig entfernen? Diese Aktion kann nicht rückgängig gemacht werden.`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
392
templates/config.html
Normal file
392
templates/config.html
Normal file
@@ -0,0 +1,392 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Konfiguration - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-cog me-2"></i>Systemkonfiguration
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-4">
|
||||
<label for="project_list_url" class="form-label">
|
||||
<i class="fas fa-link me-1"></i>Projektliste URL
|
||||
</label>
|
||||
<input type="url"
|
||||
class="form-control"
|
||||
id="project_list_url"
|
||||
name="project_list_url"
|
||||
value="{{ config.project_list_url or '' }}"
|
||||
placeholder="https://example.com/projects.json">
|
||||
<div class="form-text">
|
||||
URL zu einer JSON-Datei mit Projektinformationen oder einer Webseite mit Git-URLs.
|
||||
<br>JSON Format: <code>[{"url": "https://git.example.com/repo", "name": "Project Name", "description": "..."}]</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="auto_refresh_minutes" class="form-label">
|
||||
<i class="fas fa-clock me-1"></i>Automatische Aktualisierung (Minuten)
|
||||
</label>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="auto_refresh_minutes"
|
||||
name="auto_refresh_minutes"
|
||||
value="{{ config.auto_refresh_minutes or 30 }}"
|
||||
min="5"
|
||||
max="1440">
|
||||
<div class="form-text">
|
||||
Wie oft soll die Projektliste automatisch aktualisiert werden? (5-1440 Minuten)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="docker_registry" class="form-label">
|
||||
<i class="fas fa-server me-1"></i>Docker Registry (Optional)
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="docker_registry"
|
||||
name="docker_registry"
|
||||
value="{{ config.docker_registry or '' }}"
|
||||
placeholder="registry.example.com">
|
||||
<div class="form-text">
|
||||
Private Docker Registry für das Pushen von Images (optional).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="button" class="btn btn-outline-secondary me-md-2" onclick="testConnection()">
|
||||
<i class="fas fa-wifi"></i> Verbindung testen
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- System Status -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Systemstatus
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="status-item mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Docker:</span>
|
||||
<span id="dockerStatus" class="badge bg-secondary">Prüfung...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Git:</span>
|
||||
<span id="gitStatus" class="badge bg-secondary">Prüfung...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Projektverzeichnis:</span>
|
||||
<span id="projectDirStatus" class="badge bg-secondary">Prüfung...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Festplattenspeicher:</span>
|
||||
<span id="diskSpaceStatus" class="badge bg-secondary">Prüfung...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Erweiterte Einstellungen -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-tools me-2"></i>Erweiterte Einstellungen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-outline-warning btn-sm w-100" onclick="clearCache()">
|
||||
<i class="fas fa-broom"></i> Cache leeren
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-outline-info btn-sm w-100" onclick="exportConfig()">
|
||||
<i class="fas fa-download"></i> Konfiguration exportieren
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-outline-success btn-sm w-100" onclick="document.getElementById('importFile').click()">
|
||||
<i class="fas fa-upload"></i> Konfiguration importieren
|
||||
</button>
|
||||
<input type="file" id="importFile" style="display: none" accept=".json" onchange="importConfig(event)">
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-outline-danger btn-sm w-100" onclick="resetConfig()">
|
||||
<i class="fas fa-undo"></i> Auf Standard zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Projektliste Vorschau -->
|
||||
{% if config.projects %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-list me-2"></i>Aktuelle Projektliste ({{ config.projects|length }} Projekte)
|
||||
</h5>
|
||||
<small class="text-muted">Letzte Aktualisierung vor {{ last_update or 'unbekannt' }}</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>URL</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for project in config.projects[:10] %}
|
||||
<tr>
|
||||
<td><strong>{{ project.name or 'Unbekannt' }}</strong></td>
|
||||
<td>
|
||||
<a href="{{ project.url }}" target="_blank" class="text-decoration-none">
|
||||
{{ project.url[:50] }}{% if project.url|length > 50 %}...{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ (project.description or 'Keine Beschreibung')[:60] }}{% if (project.description or '')|length > 60 %}...{% endif %}</td>
|
||||
<td>
|
||||
{% if project.name in installed_projects %}
|
||||
<span class="badge bg-success">Installiert</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Verfügbar</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if config.projects|length > 10 %}
|
||||
<p class="text-muted text-center">... und {{ config.projects|length - 10 }} weitere Projekte</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Debug Informationen -->
|
||||
<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-bug me-2"></i>Debug-Informationen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Systeminfo:</h6>
|
||||
<ul class="list-unstyled small">
|
||||
<li><strong>Arbeitsverzeichnis:</strong> <code id="workingDir">Lade...</code></li>
|
||||
<li><strong>Projektverzeichnis:</strong> <code>./projects/</code></li>
|
||||
<li><strong>App-Verzeichnis:</strong> <code>./apps/</code></li>
|
||||
<li><strong>Konfigurationsdatei:</strong> <code>./config.json</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Laufzeit-Info:</h6>
|
||||
<ul class="list-unstyled small">
|
||||
<li><strong>Flask Debug:</strong> <span id="flaskDebug">An</span></li>
|
||||
<li><strong>Host:</strong> <code>0.0.0.0:5000</code></li>
|
||||
<li><strong>Uptime:</strong> <span id="uptime">Lade...</span></li>
|
||||
<li><strong>Speicherverbrauch:</strong> <span id="memUsage">Lade...</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Systemstatus prüfen
|
||||
function checkSystemStatus() {
|
||||
// Docker Status
|
||||
fetch('/api/system_status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateStatusBadge('dockerStatus', data.docker, 'Docker');
|
||||
updateStatusBadge('gitStatus', data.git, 'Git');
|
||||
updateStatusBadge('projectDirStatus', data.project_dir, 'Projektverzeichnis');
|
||||
updateStatusBadge('diskSpaceStatus', data.disk_space, 'Festplattenspeicher');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Prüfen des Systemstatus:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatusBadge(elementId, status, name) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (status.available) {
|
||||
element.textContent = status.version || 'Verfügbar';
|
||||
element.className = 'badge bg-success';
|
||||
} else {
|
||||
element.textContent = 'Nicht verfügbar';
|
||||
element.className = 'badge bg-danger';
|
||||
}
|
||||
}
|
||||
|
||||
// Verbindung testen
|
||||
function testConnection() {
|
||||
const url = document.getElementById('project_list_url').value;
|
||||
if (!url) {
|
||||
alert('Bitte geben Sie eine URL ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Teste...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch('/api/test_connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({url: url})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(`Verbindung erfolgreich! ${data.projects_found} Projekte gefunden.`);
|
||||
} else {
|
||||
alert(`Verbindungsfehler: ${data.error}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert(`Netzwerkfehler: ${error}`);
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Cache leeren
|
||||
function clearCache() {
|
||||
if (confirm('Möchten Sie wirklich den Cache leeren?')) {
|
||||
fetch('/api/clear_cache', {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => alert(data.message))
|
||||
.catch(error => alert('Fehler: ' + error));
|
||||
}
|
||||
}
|
||||
|
||||
// Konfiguration exportieren
|
||||
function exportConfig() {
|
||||
fetch('/api/export_config')
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'app_installer_config.json';
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(error => alert('Fehler beim Export: ' + error));
|
||||
}
|
||||
|
||||
// Konfiguration importieren
|
||||
function importConfig(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
const config = JSON.parse(e.target.result);
|
||||
|
||||
if (confirm('Möchten Sie die aktuelle Konfiguration mit der importierten ersetzen?')) {
|
||||
fetch('/api/import_config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Konfiguration erfolgreich importiert!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Import: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => alert('Netzwerkfehler: ' + error));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ungültige JSON-Datei: ' + error.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// Konfiguration zurücksetzen
|
||||
function resetConfig() {
|
||||
if (confirm('Möchten Sie wirklich alle Einstellungen auf die Standardwerte zurücksetzen?')) {
|
||||
fetch('/api/reset_config', {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Konfiguration zurückgesetzt!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Zurücksetzen: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => alert('Netzwerkfehler: ' + error));
|
||||
}
|
||||
}
|
||||
|
||||
// System-Info laden
|
||||
function loadSystemInfo() {
|
||||
document.getElementById('workingDir').textContent = window.location.origin;
|
||||
document.getElementById('uptime').textContent = 'Läuft seit Start';
|
||||
document.getElementById('memUsage').textContent = 'Nicht verfügbar';
|
||||
}
|
||||
|
||||
// Beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkSystemStatus();
|
||||
loadSystemInfo();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
425
templates/docker_status.html
Normal file
425
templates/docker_status.html
Normal file
@@ -0,0 +1,425 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Docker Status - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="fas fa-docker me-2"></i>Docker Status & Diagnose</h2>
|
||||
<button class="btn btn-primary" onclick="runDiagnose()">
|
||||
<i class="fas fa-stethoscope"></i> Diagnose ausführen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docker Status Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="stat-item">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="stat-item">
|
||||
<i class="fas fa-server fa-2x mb-2" id="daemonIcon"></i>
|
||||
<div class="stat-number" id="daemonStatus">Prüfung...</div>
|
||||
<div class="text-muted">Daemon Status</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="stat-item">
|
||||
<i class="fas fa-box fa-2x mb-2" id="containerIcon"></i>
|
||||
<div class="stat-number" id="containerCount">-</div>
|
||||
<div class="text-muted">Container</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="stat-item">
|
||||
<i class="fas fa-layer-group fa-2x mb-2" id="imageIcon"></i>
|
||||
<div class="stat-number" id="imageCount">-</div>
|
||||
<div class="text-muted">Images</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diagnose Ergebnisse -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-clipboard-list me-2"></i>Diagnose Ergebnisse
|
||||
</h5>
|
||||
<span class="badge bg-secondary" id="lastUpdate">Noch nicht ausgeführt</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="diagnoseResults">
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="fas fa-info-circle fa-2x mb-3"></i>
|
||||
<p>Klicken Sie auf "Diagnose ausführen" um Docker zu überprüfen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docker Logs -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-terminal me-2"></i>Docker System Logs
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dockerLogs" class="bg-dark text-light p-3 rounded font-monospace small" style="height: 300px; overflow-y: auto;">
|
||||
<div class="text-center text-muted">
|
||||
Docker Logs werden geladen...
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-outline-info btn-sm" onclick="refreshDockerLogs()">
|
||||
<i class="fas fa-sync-alt"></i> Logs aktualisieren
|
||||
</button>
|
||||
<button class="btn btn-outline-warning btn-sm" onclick="clearDockerLogs()">
|
||||
<i class="fas fa-broom"></i> Logs leeren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Schnellaktionen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-bolt me-2"></i>Docker Aktionen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-success" onclick="startDockerDesktop()">
|
||||
<i class="fas fa-play"></i> Docker Desktop starten
|
||||
</button>
|
||||
<button class="btn btn-warning" onclick="restartDockerDesktop()">
|
||||
<i class="fas fa-redo"></i> Docker Desktop neustarten
|
||||
</button>
|
||||
<button class="btn btn-info" onclick="pullHelloWorld()">
|
||||
<i class="fas fa-download"></i> Test Image herunterladen
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="openDockerDesktop()">
|
||||
<i class="fas fa-external-link-alt"></i> Docker Desktop öffnen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Systemanforderungen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-clipboard-check me-2"></i>Systemanforderungen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="requirements-list">
|
||||
<div class="requirement-item mb-2">
|
||||
<i class="fas fa-check text-success me-2"></i>
|
||||
<span>Windows 10/11 (64-bit)</span>
|
||||
</div>
|
||||
<div class="requirement-item mb-2">
|
||||
<i class="fas fa-check text-success me-2"></i>
|
||||
<span>Hyper-V oder WSL2</span>
|
||||
</div>
|
||||
<div class="requirement-item mb-2">
|
||||
<i class="fas fa-question text-warning me-2" id="ramCheck"></i>
|
||||
<span>4GB+ RAM empfohlen</span>
|
||||
</div>
|
||||
<div class="requirement-item mb-2">
|
||||
<i class="fas fa-question text-warning me-2" id="diskCheck"></i>
|
||||
<span>20GB+ freier Speicher</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hilfe & Ressourcen -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-question-circle me-2"></i>Hilfe & Ressourcen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="https://docs.docker.com/desktop/windows/" target="_blank" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-book"></i> Docker Desktop Docs
|
||||
</a>
|
||||
<a href="https://docs.docker.com/desktop/troubleshoot/" target="_blank" class="btn btn-outline-info btn-sm">
|
||||
<i class="fas fa-wrench"></i> Troubleshooting Guide
|
||||
</a>
|
||||
<a href="https://www.docker.com/products/docker-desktop/" target="_blank" class="btn btn-outline-success btn-sm">
|
||||
<i class="fas fa-download"></i> Docker Desktop Download
|
||||
</a>
|
||||
<button class="btn btn-outline-warning btn-sm" onclick="exportDiagnose()">
|
||||
<i class="fas fa-file-export"></i> Diagnose exportieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installation Modal -->
|
||||
<div class="modal fade" id="installModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Docker Desktop Installation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>Installationsschritte:</h6>
|
||||
<ol>
|
||||
<li>Laden Sie Docker Desktop von <a href="https://www.docker.com/products/docker-desktop/" target="_blank">docker.com</a> herunter</li>
|
||||
<li>Führen Sie die Installationsdatei als Administrator aus</li>
|
||||
<li>Folgen Sie dem Installationsassistenten</li>
|
||||
<li>Starten Sie Ihren Computer neu</li>
|
||||
<li>Starten Sie Docker Desktop</li>
|
||||
<li>Warten Sie bis Docker vollständig geladen ist</li>
|
||||
</ol>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Hinweis:</strong> Docker Desktop erfordert Hyper-V oder WSL2.
|
||||
Diese werden automatisch aktiviert falls nötig.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||
<a href="https://www.docker.com/products/docker-desktop/" target="_blank" class="btn btn-primary">
|
||||
<i class="fas fa-download"></i> Docker Desktop herunterladen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let lastDiagnoseData = null;
|
||||
|
||||
// Führe Diagnose aus
|
||||
function runDiagnose() {
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Läuft...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch('/api/docker_diagnose')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
lastDiagnoseData = data;
|
||||
displayDiagnoseResults(data);
|
||||
updateStatusCards(data);
|
||||
document.getElementById('lastUpdate').textContent = new Date(data.timestamp).toLocaleString();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Diagnose-Fehler:', error);
|
||||
document.getElementById('diagnoseResults').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Fehler bei der Diagnose: ${error}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Zeige Diagnose-Ergebnisse
|
||||
function displayDiagnoseResults(data) {
|
||||
const container = document.getElementById('diagnoseResults');
|
||||
let html = '';
|
||||
|
||||
for (const [checkName, result] of Object.entries(data.checks)) {
|
||||
const icon = result.success ? 'fa-check text-success' : 'fa-times text-danger';
|
||||
const title = formatCheckName(checkName);
|
||||
|
||||
html += `
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<i class="fas ${icon} me-3 mt-1"></i>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-1">${title}</h6>
|
||||
<p class="mb-0 text-muted small">${result.output}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!html) {
|
||||
html = '<div class="text-center text-muted">Keine Diagnose-Daten verfügbar.</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Formatiere Check-Namen
|
||||
function formatCheckName(name) {
|
||||
const names = {
|
||||
'docker_version': 'Docker Version',
|
||||
'docker_daemon': 'Docker Daemon',
|
||||
'containers': 'Container Status',
|
||||
'images': 'Docker Images'
|
||||
};
|
||||
return names[name] || name;
|
||||
}
|
||||
|
||||
// Update Status-Cards
|
||||
function updateStatusCards(data) {
|
||||
// Docker Status
|
||||
if (data.checks.docker_version) {
|
||||
const dockerIcon = document.getElementById('dockerIcon');
|
||||
const dockerStatus = document.getElementById('dockerStatus');
|
||||
|
||||
if (data.checks.docker_version.success) {
|
||||
dockerIcon.className = 'fas fa-docker fa-2x mb-2 text-success';
|
||||
dockerStatus.textContent = 'Installiert';
|
||||
dockerStatus.className = 'stat-number text-success';
|
||||
} else {
|
||||
dockerIcon.className = 'fas fa-docker fa-2x mb-2 text-danger';
|
||||
dockerStatus.textContent = 'Nicht verfügbar';
|
||||
dockerStatus.className = 'stat-number text-danger';
|
||||
}
|
||||
}
|
||||
|
||||
// Daemon Status
|
||||
if (data.checks.docker_daemon) {
|
||||
const daemonIcon = document.getElementById('daemonIcon');
|
||||
const daemonStatus = document.getElementById('daemonStatus');
|
||||
|
||||
if (data.checks.docker_daemon.success) {
|
||||
daemonIcon.className = 'fas fa-server fa-2x mb-2 text-success';
|
||||
daemonStatus.textContent = 'Läuft';
|
||||
daemonStatus.className = 'stat-number text-success';
|
||||
} else {
|
||||
daemonIcon.className = 'fas fa-server fa-2x mb-2 text-danger';
|
||||
daemonStatus.textContent = 'Gestoppt';
|
||||
daemonStatus.className = 'stat-number text-danger';
|
||||
}
|
||||
}
|
||||
|
||||
// Container Count
|
||||
if (data.checks.containers && data.checks.containers.containers) {
|
||||
const containerCount = document.getElementById('containerCount');
|
||||
const count = data.checks.containers.containers.length;
|
||||
containerCount.textContent = count;
|
||||
|
||||
const running = data.checks.containers.containers.filter(c => c.State === 'running').length;
|
||||
containerCount.title = `${running} laufend, ${count - running} gestoppt`;
|
||||
}
|
||||
}
|
||||
|
||||
// Docker Desktop Aktionen
|
||||
function startDockerDesktop() {
|
||||
alert('Bitte starten Sie Docker Desktop manuell über das Startmenü.');
|
||||
}
|
||||
|
||||
function restartDockerDesktop() {
|
||||
if (confirm('Docker Desktop neustarten? Dies kann einige Minuten dauern.')) {
|
||||
alert('Bitte starten Sie Docker Desktop manuell neu:\n1. Docker Desktop schließen\n2. Docker Desktop wieder öffnen\n3. Warten bis vollständig geladen');
|
||||
}
|
||||
}
|
||||
|
||||
function pullHelloWorld() {
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Lädt...';
|
||||
button.disabled = true;
|
||||
|
||||
// Simuliere Docker Pull (in echter Implementation würde hier eine API-Call erfolgen)
|
||||
setTimeout(() => {
|
||||
alert('Test abgeschlossen. Prüfen Sie die Diagnose für Details.');
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
runDiagnose();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function openDockerDesktop() {
|
||||
// Versuche Docker Desktop zu öffnen
|
||||
alert('Suchen Sie nach "Docker Desktop" im Startmenü oder klicken Sie auf das Docker-Symbol in der Taskleiste.');
|
||||
}
|
||||
|
||||
// Docker Logs
|
||||
function refreshDockerLogs() {
|
||||
const logsContainer = document.getElementById('dockerLogs');
|
||||
logsContainer.innerHTML = '<div class="text-center text-muted">Docker Logs werden geladen...</div>';
|
||||
|
||||
// Simuliere Log-Abruf
|
||||
setTimeout(() => {
|
||||
const logs = `[${new Date().toISOString()}] Docker Desktop starting...
|
||||
[${new Date().toISOString()}] Hyper-V backend initialized
|
||||
[${new Date().toISOString()}] WSL2 engine started
|
||||
[${new Date().toISOString()}] Docker daemon started
|
||||
[${new Date().toISOString()}] Ready for connections`;
|
||||
|
||||
logsContainer.innerHTML = `<pre class="mb-0">${logs}</pre>`;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function clearDockerLogs() {
|
||||
if (confirm('Docker Logs leeren?')) {
|
||||
document.getElementById('dockerLogs').innerHTML = '<div class="text-center text-muted">Logs geleert</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Export Diagnose
|
||||
function exportDiagnose() {
|
||||
if (!lastDiagnoseData) {
|
||||
alert('Bitte führen Sie zuerst eine Diagnose aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
const dataStr = JSON.stringify(lastDiagnoseData, null, 2);
|
||||
const blob = new Blob([dataStr], {type: 'application/json'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `docker_diagnose_${new Date().toISOString().split('T')[0]}.json`;
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Auto-Diagnose beim Laden
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
runDiagnose();
|
||||
refreshDockerLogs();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
220
templates/index.html
Normal file
220
templates/index.html
Normal file
@@ -0,0 +1,220 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="header-stats">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">{{ projects|length }}</span>
|
||||
<span>Installierte Apps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">{{ projects|selectattr('status', 'equalto', 'running')|list|length }}</span>
|
||||
<span>Laufende Apps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">{{ config.projects|length if config.projects else 0 }}</span>
|
||||
<span>Verfügbare Apps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">{{ projects|selectattr('has_dockerfile', 'equalto', true)|list|length }}</span>
|
||||
<span>Docker Apps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not projects %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-rocket fa-3x text-muted mb-3"></i>
|
||||
<h3 class="text-muted">Noch keine Apps installiert</h3>
|
||||
<p class="text-muted mb-4">Beginnen Sie mit der Installation Ihrer ersten App!</p>
|
||||
<a href="{{ url_for('available_projects') }}" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-download"></i> Verfügbare Apps anzeigen
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
{% for project in projects %}
|
||||
<div class="col-lg-6 col-xl-4">
|
||||
<div class="card project-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-cube me-2"></i>{{ project.name }}
|
||||
</h5>
|
||||
<span class="status-badge status-{{ 'running' if project.status == 'running' else 'stopped' if project.status in ['exited', 'stopped'] else 'unknown' }}">
|
||||
<i class="fas fa-circle me-1"></i>
|
||||
{% if project.status == 'running' %}Läuft{% elif project.status in ['exited', 'stopped'] %}Gestoppt{% else %}Unbekannt{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Docker:</small><br>
|
||||
{% if project.has_dockerfile %}
|
||||
<i class="fas fa-check text-success"></i> Verfügbar
|
||||
{% else %}
|
||||
<i class="fas fa-times text-danger"></i> Nicht verfügbar
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Konfiguration:</small><br>
|
||||
{% if project.has_env_example %}
|
||||
<i class="fas fa-check text-success"></i> .env vorhanden
|
||||
{% else %}
|
||||
<i class="fas fa-minus text-warning"></i> Keine .env
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if project.readme %}
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">Beschreibung:</small>
|
||||
<p class="card-text small">{{ project.readme[:150] }}{% if project.readme|length > 150 %}...{% endif %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<small class="text-muted">Installiert: {{ project.created }}</small>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="action-buttons">
|
||||
{% if project.status == 'running' %}
|
||||
<a href="{{ url_for('stop_project', project_name=project.name) }}" class="btn btn-warning btn-sm">
|
||||
<i class="fas fa-stop"></i> Stoppen
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('start_project', project_name=project.name) }}" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-play"></i> Starten
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('build_project', project_name=project.name) }}" class="btn btn-info btn-sm">
|
||||
<i class="fas fa-hammer"></i> Build
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('project_details', project_name=project.name) }}" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-cog"></i> Config
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('remove_project', project_name=project.name) }}"
|
||||
class="btn btn-danger btn-sm"
|
||||
onclick="return confirmAction('remove', '{{ project.name }}')">
|
||||
<i class="fas fa-trash"></i> Entfernen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<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-bolt me-2"></i>Schnellaktionen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-2">
|
||||
<a href="{{ url_for('available_projects') }}" class="btn btn-outline-primary w-100">
|
||||
<i class="fas fa-download"></i> Neue App installieren
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<a href="{{ url_for('refresh_projects') }}" class="btn btn-outline-info w-100">
|
||||
<i class="fas fa-sync-alt"></i> Projektliste aktualisieren
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<a href="{{ url_for('config') }}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-cog"></i> Einstellungen
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<button class="btn btn-outline-warning w-100" onclick="startAllApps()">
|
||||
<i class="fas fa-rocket"></i> Alle starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Port Management Modal -->
|
||||
<div class="modal fade" id="portModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Port auswählen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="portForm">
|
||||
<div class="mb-3">
|
||||
<label for="portInput" class="form-label">Port (Standard: 8080)</label>
|
||||
<input type="number" class="form-control" id="portInput" value="8080" min="1" max="65535">
|
||||
</div>
|
||||
</form>
|
||||
</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="startWithPort()">Starten</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let currentProject = '';
|
||||
|
||||
function startWithCustomPort(projectName) {
|
||||
currentProject = projectName;
|
||||
const modal = new bootstrap.Modal(document.getElementById('portModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function startWithPort() {
|
||||
const port = document.getElementById('portInput').value;
|
||||
window.location.href = `/start_project/${currentProject}?port=${port}`;
|
||||
}
|
||||
|
||||
function startAllApps() {
|
||||
if (confirm('Möchten Sie alle gestoppten Apps starten?')) {
|
||||
const stoppedApps = document.querySelectorAll('.status-stopped').length;
|
||||
if (stoppedApps > 0) {
|
||||
// Hier könnte eine Batch-Start-Funktion implementiert werden
|
||||
alert(`${stoppedApps} Apps werden gestartet. Diese Funktion wird in einer zukünftigen Version implementiert.`);
|
||||
} else {
|
||||
alert('Alle Apps laufen bereits oder es sind keine Apps installiert.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-update status badges
|
||||
function updateStatus() {
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Status updates würden hier implementiert
|
||||
})
|
||||
.catch(error => console.log('Status update failed:', error));
|
||||
}
|
||||
|
||||
// Update status every 30 seconds
|
||||
setInterval(updateStatus, 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
906
templates/project_details.html
Normal file
906
templates/project_details.html
Normal file
@@ -0,0 +1,906 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ project.name }} - Details{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>
|
||||
<i class="fas fa-cube me-2"></i>{{ project.name }}
|
||||
<span class="status-badge status-{{ 'running' if project.status == 'running' else 'stopped' if project.status in ['exited', 'stopped'] else 'unknown' }} ms-2">
|
||||
<i class="fas fa-circle me-1"></i>
|
||||
{% if project.status == 'running' %}Läuft{% elif project.status in ['exited', 'stopped'] %}Gestoppt{% else %}Unbekannt{% endif %}
|
||||
</span>
|
||||
</h2>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Zurück
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Projektinformationen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Projektinformationen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Name:</strong> {{ project.name }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Pfad:</strong> <code>{{ project.path }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Docker verfügbar:</strong>
|
||||
{% if project.has_dockerfile %}
|
||||
<i class="fas fa-check text-success"></i> Ja
|
||||
{% else %}
|
||||
<i class="fas fa-times text-danger"></i> Nein
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Umgebungskonfiguration:</strong>
|
||||
{% if project.has_env_example %}
|
||||
<i class="fas fa-check text-success"></i> .env.example vorhanden
|
||||
{% else %}
|
||||
<i class="fas fa-minus text-warning"></i> Keine .env.example
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Docker Compose:</strong>
|
||||
{% if project.has_docker_compose %}
|
||||
<i class="fas fa-check text-success"></i> Verfügbar
|
||||
{% else %}
|
||||
<i class="fas fa-times text-muted"></i> Nicht verfügbar
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Installiert:</strong> {{ project.created }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if project.readme %}
|
||||
<div class="mt-4">
|
||||
<h6>README:</h6>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre class="mb-0 small">{{ project.readme }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Umgebungskonfiguration -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-edit me-2"></i>Umgebungskonfiguration (.env)
|
||||
</h5>
|
||||
<div>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="resetToExample()">
|
||||
<i class="fas fa-undo"></i> Beispiel wiederherstellen
|
||||
</button>
|
||||
<button class="btn btn-outline-warning btn-sm" onclick="validateEnv()">
|
||||
<i class="fas fa-check-circle"></i> Validieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('save_env', project_name=project.name) }}">
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control font-monospace"
|
||||
name="env_content"
|
||||
id="envContent"
|
||||
rows="15"
|
||||
placeholder="Hier können Sie die Umgebungsvariablen für das Projekt konfigurieren...">{{ env_content }}</textarea>
|
||||
<div class="form-text">
|
||||
Konfigurieren Sie hier die Umgebungsvariablen für Ihr Projekt.
|
||||
Diese werden in die .env Datei gespeichert.
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="button" class="btn btn-outline-secondary me-md-2" onclick="previewChanges()">
|
||||
<i class="fas fa-eye"></i> Vorschau
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> .env speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docker Logs -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-terminal me-2"></i>Container Logs
|
||||
</h5>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="refreshLogs()">
|
||||
<i class="fas fa-sync-alt"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dockerLogs" class="bg-dark text-light p-3 rounded font-monospace small" style="height: 300px; overflow-y: auto;">
|
||||
<div class="text-center text-muted">
|
||||
Logs werden geladen...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Schnellaktionen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-bolt me-2"></i>Container-Steuerung
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
{% if project.status == 'running' %}
|
||||
<a href="{{ url_for('stop_project', project_name=project.name) }}" class="btn btn-warning">
|
||||
<i class="fas fa-stop"></i> Container stoppen
|
||||
</a>
|
||||
<button class="btn btn-info" onclick="restartContainer()">
|
||||
<i class="fas fa-redo"></i> Container neustarten
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-success" id="quickStartButton" onclick="quickStartContainer()">
|
||||
<i class="fas fa-play"></i> Schnellstart (Auto-Port)
|
||||
</button>
|
||||
<button class="btn btn-outline-success" onclick="showPortSelection()">
|
||||
<i class="fas fa-cog"></i> Erweiterte Start-Optionen
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('build_project', project_name=project.name) }}" class="btn btn-primary">
|
||||
<i class="fas fa-hammer"></i> Image neu bauen
|
||||
</a>
|
||||
|
||||
<button class="btn btn-danger" onclick="removeProjectSafely()">
|
||||
<i class="fas fa-trash"></i> Projekt entfernen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Port-Status -->
|
||||
{% if project.status == 'running' %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-network-wired me-2"></i>Aktive Verbindungen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="activeConnections">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>HTTP:</span>
|
||||
<a href="http://localhost:8080" target="_blank" class="btn btn-outline-primary btn-sm">
|
||||
:8080 <i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<small class="text-muted">
|
||||
Klicken Sie auf die Links um die Anwendung zu öffnen.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Container-Statistiken -->
|
||||
{% if project.status == 'running' %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-chart-line me-2"></i>Monitoring
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="cpuUsage">-</span>
|
||||
<span class="small text-muted">CPU %</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="memUsage">-</span>
|
||||
<span class="small text-muted">RAM MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center mt-2">
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="networkIn">-</span>
|
||||
<span class="small text-muted">Net In</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="networkOut">-</span>
|
||||
<span class="small text-muted">Net Out</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-outline-info btn-sm w-100" onclick="updateMonitoring()">
|
||||
<i class="fas fa-sync-alt"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Hilfe -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-question-circle me-2"></i>Hilfe & Tipps
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small">
|
||||
{% if not project.has_dockerfile %}
|
||||
<div class="alert alert-warning">
|
||||
<strong>Kein Dockerfile:</strong> Erstellen Sie ein Dockerfile in Ihrem Projektverzeichnis.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not project.has_env_example %}
|
||||
<div class="alert alert-info">
|
||||
<strong>Tipp:</strong> Erstellen Sie eine .env.example Datei für Umgebungsvariablen.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li><i class="fas fa-check text-success"></i> Bauen Sie das Image vor dem ersten Start</li>
|
||||
<li><i class="fas fa-check text-success"></i> Überprüfen Sie die .env Konfiguration</li>
|
||||
<li><i class="fas fa-check text-success"></i> Beachten Sie die Container-Logs bei Problemen</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Debug-Panel -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-bug me-2"></i>Debug-Informationen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="showContainerLogs()">
|
||||
<i class="fas fa-file-alt"></i> Container-Logs anzeigen
|
||||
</button>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="inspectContainer()">
|
||||
<i class="fas fa-search"></i> Container inspizieren
|
||||
</button>
|
||||
<button class="btn btn-outline-warning btn-sm" onclick="checkPortStatus()">
|
||||
<i class="fas fa-network-wired"></i> Port-Status prüfen
|
||||
</button>
|
||||
</div>
|
||||
<div id="debugOutput" class="mt-3 small" style="display: none;">
|
||||
<div class="bg-light p-2 rounded">
|
||||
<pre id="debugContent"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Globale Variable für Projekt-Name
|
||||
const PROJECT_NAME = '{{ project.name }}';
|
||||
|
||||
// Container-Verwaltung - Verbesserte Version
|
||||
function quickStartContainer() {
|
||||
const button = document.getElementById('quickStartButton');
|
||||
if (!button) return;
|
||||
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Startet...';
|
||||
button.disabled = true;
|
||||
|
||||
console.log('Starting container with auto-port selection...');
|
||||
|
||||
// Verwende API-Endpoint für bessere Kontrolle
|
||||
fetch(`/api/start_project/${PROJECT_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Start response:', data);
|
||||
|
||||
if (data.success) {
|
||||
// Erfolg - zeige Erfolgsmeldung und aktualisiere UI
|
||||
showSuccessMessage(`Container erfolgreich gestartet auf Port ${data.port}!`);
|
||||
|
||||
// Aktualisiere Button-Status nach 2 Sekunden
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
// Fehler behandeln
|
||||
console.error('Start error:', data.error);
|
||||
showErrorMessage(`Fehler beim Start: ${data.error || 'Unbekannter Fehler'}`);
|
||||
|
||||
// Automatisch Debug-Informationen anzeigen bei Container-Problemen
|
||||
if (data.error && data.error.includes('läuft nicht')) {
|
||||
setTimeout(() => {
|
||||
console.log('Container läuft nicht - zeige Debug-Informationen...');
|
||||
showContainerLogs();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Prüfe ob es ein Port-Problem ist
|
||||
if (data.error && (data.error.includes('Port') || data.error.includes('port'))) {
|
||||
setTimeout(() => {
|
||||
showPortSelectionWithError(data.error);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Network error:', error);
|
||||
showErrorMessage(`Netzwerkfehler: ${error.message || 'Unbekannter Netzwerkfehler'}`);
|
||||
})
|
||||
.finally(() => {
|
||||
// Button zurücksetzen
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function showPortSelection() {
|
||||
const portSelection = `
|
||||
<div class="modal fade" id="portSelectionModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Port-Auswahl für ${PROJECT_NAME}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="customPort" class="form-label">Port auswählen:</label>
|
||||
<input type="number" class="form-control" id="customPort" value="8080" min="1" max="65535">
|
||||
<div class="form-text">
|
||||
Beliebte Ports: 8080 (Standard), 3000 (Node.js), 5000 (Flask), 8081-8090 (Alternative)
|
||||
</div>
|
||||
</div>
|
||||
<div id="portStatus" class="mb-3"></div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-info" onclick="checkPortAvailability()">
|
||||
<i class="fas fa-search"></i> Port prüfen
|
||||
</button>
|
||||
<button class="btn btn-outline-warning" onclick="findFreePortForStart()">
|
||||
<i class="fas fa-magic"></i> Automatisch freien Port finden
|
||||
</button>
|
||||
</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-success" onclick="startWithSelectedPort()">
|
||||
<i class="fas fa-play"></i> Container starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Entferne existierendes Modal falls vorhanden
|
||||
const existingModal = document.getElementById('portSelectionModal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// Füge neues Modal hinzu
|
||||
document.body.insertAdjacentHTML('beforeend', portSelection);
|
||||
|
||||
// Modal anzeigen
|
||||
const modal = new bootstrap.Modal(document.getElementById('portSelectionModal'));
|
||||
modal.show();
|
||||
|
||||
// Port direkt prüfen
|
||||
setTimeout(checkPortAvailability, 500);
|
||||
}
|
||||
|
||||
function showPortSelectionWithError(errorMessage) {
|
||||
showPortSelection();
|
||||
|
||||
setTimeout(() => {
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
if (statusElement) {
|
||||
statusElement.innerHTML = `<div class="alert alert-warning"><i class="fas fa-exclamation-triangle"></i> ${errorMessage}</div>`;
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function checkPortAvailability() {
|
||||
const port = document.getElementById('customPort').value;
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
|
||||
if (!port || port < 1 || port > 65535) {
|
||||
statusElement.innerHTML = '<div class="alert alert-danger">Ungültiger Port (1-65535)</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
statusElement.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Prüfe Port verfügbarkeit...</div>';
|
||||
|
||||
fetch(`/api/check_port/${port}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.available) {
|
||||
statusElement.innerHTML = `<div class="alert alert-success"><i class="fas fa-check"></i> Port ${port} ist verfügbar!</div>`;
|
||||
} else {
|
||||
statusElement.innerHTML = `<div class="alert alert-warning"><i class="fas fa-times"></i> Port ${port} ist bereits belegt</div>`;
|
||||
}
|
||||
} else {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Fehler: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Netzwerkfehler: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function findFreePortForStart() {
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
statusElement.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Suche freien Port...</div>';
|
||||
|
||||
fetch('/api/find_available_port?start=8080')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('customPort').value = data.port;
|
||||
statusElement.innerHTML = `<div class="alert alert-success"><i class="fas fa-check"></i> Freier Port gefunden: ${data.port}</div>`;
|
||||
} else {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Kein freier Port gefunden: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Fehler bei Port-Suche: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function startWithSelectedPort() {
|
||||
const port = document.getElementById('customPort').value;
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
|
||||
if (!port || port < 1 || port > 65535) {
|
||||
showErrorMessage('Bitte geben Sie einen gültigen Port ein (1-65535)');
|
||||
return;
|
||||
}
|
||||
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Startet...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/api/start_project/${PROJECT_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({port: parseInt(port)})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Start response:', data);
|
||||
|
||||
if (data.success) {
|
||||
// Erfolg - schließe Modal und zeige Erfolg
|
||||
bootstrap.Modal.getInstance(document.getElementById('portSelectionModal')).hide();
|
||||
showSuccessMessage(`Container erfolgreich gestartet auf Port ${data.port}!`);
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
// Fehler - zeige Fehlermeldung aber behalte Modal offen
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
statusElement.innerHTML = `<div class="alert alert-danger"><i class="fas fa-times"></i> ${data.error}</div>`;
|
||||
|
||||
// Bei Port-Konflikt, schlage Alternative vor
|
||||
if (data.alternative_port) {
|
||||
statusElement.innerHTML += `<button class="btn btn-warning btn-sm mt-2" onclick="useAlternativePort(${data.alternative_port})">
|
||||
<i class="fas fa-arrow-right"></i> Port ${data.alternative_port} verwenden
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Network error:', error);
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Netzwerkfehler: ${error.message}</div>`;
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function useAlternativePort(port) {
|
||||
document.getElementById('customPort').value = port;
|
||||
checkPortAvailability();
|
||||
}
|
||||
|
||||
// Hilfsfunktionen für Nachrichten
|
||||
function showSuccessMessage(message) {
|
||||
showToast(message, 'success');
|
||||
}
|
||||
|
||||
function showErrorMessage(message) {
|
||||
showToast(message, 'error');
|
||||
}
|
||||
|
||||
function showToast(message, type) {
|
||||
const toastHtml = `
|
||||
<div class="toast align-items-center text-white bg-${type === 'success' ? 'success' : 'danger'} border-0" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="fas fa-${type === 'success' ? 'check' : 'exclamation-triangle'} me-2"></i>
|
||||
${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Toast Container erstellen falls nicht vorhanden
|
||||
let toastContainer = document.getElementById('toastContainer');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'toastContainer';
|
||||
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
toastContainer.style.zIndex = '9999';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Toast hinzufügen
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||
|
||||
// Toast anzeigen
|
||||
const toastElement = toastContainer.lastElementChild;
|
||||
const toast = new bootstrap.Toast(toastElement, {delay: 5000});
|
||||
toast.show();
|
||||
|
||||
// Toast nach dem Verstecken entfernen
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Container neustarten - verbessert
|
||||
function restartContainer() {
|
||||
if (!confirm('Container wirklich neustarten?')) return;
|
||||
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Startet neu...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/api/restart_project/${PROJECT_NAME}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showSuccessMessage(data.message);
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
} else {
|
||||
showErrorMessage(data.error || 'Fehler beim Neustart');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showErrorMessage(`Netzwerkfehler: ${error.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Docker Logs - verbessert
|
||||
function refreshLogs() {
|
||||
const logsContainer = document.getElementById('dockerLogs');
|
||||
if (!logsContainer) return;
|
||||
|
||||
logsContainer.innerHTML = '<div class="text-center text-muted">Logs werden geladen...</div>';
|
||||
|
||||
fetch(`/api/container_logs/${PROJECT_NAME}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const logs = data.logs || 'Keine Logs verfügbar';
|
||||
logsContainer.innerHTML = `<pre class="mb-0">${logs}</pre>`;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
} else {
|
||||
logsContainer.innerHTML = `<div class="text-warning">Logs nicht verfügbar: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
logsContainer.innerHTML = `<div class="text-danger">Fehler beim Laden der Logs: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// Monitoring Update
|
||||
function updateMonitoring() {
|
||||
fetch(`/api/container_stats/${PROJECT_NAME}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('cpuUsage').textContent = data.stats.cpu || '-';
|
||||
document.getElementById('memUsage').textContent = data.stats.memory || '-';
|
||||
document.getElementById('networkIn').textContent = data.stats.network_in || '-';
|
||||
document.getElementById('networkOut').textContent = data.stats.network_out || '-';
|
||||
}
|
||||
})
|
||||
.catch(error => console.log('Monitoring update failed:', error));
|
||||
}
|
||||
|
||||
// .env Funktionen
|
||||
function resetToExample() {
|
||||
if (confirm('Möchten Sie die aktuelle .env mit der .env.example überschreiben?')) {
|
||||
fetch(`/api/reset_env/${PROJECT_NAME}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('envContent').value = data.content;
|
||||
showSuccessMessage('.env auf Beispiel zurückgesetzt');
|
||||
} else {
|
||||
showErrorMessage('Fehler: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => showErrorMessage('Netzwerkfehler: ' + error.message));
|
||||
}
|
||||
}
|
||||
|
||||
function validateEnv() {
|
||||
const envContent = document.getElementById('envContent').value;
|
||||
|
||||
fetch(`/api/validate_env/${PROJECT_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({content: envContent})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
showSuccessMessage('✅ .env Konfiguration ist gültig!');
|
||||
} else {
|
||||
showErrorMessage('❌ .env Konfiguration hat Probleme:\n' + data.errors.join('\n'));
|
||||
}
|
||||
})
|
||||
.catch(error => showErrorMessage('Fehler bei der Validierung: ' + error.message));
|
||||
}
|
||||
|
||||
function previewChanges() {
|
||||
const envContent = document.getElementById('envContent').value;
|
||||
const preview = window.open('', '_blank', 'width=600,height=400');
|
||||
preview.document.write(`
|
||||
<html>
|
||||
<head><title>Umgebungskonfiguration Vorschau</title></head>
|
||||
<body style="font-family: monospace; padding: 20px;">
|
||||
<h3>Vorschau der .env Datei:</h3>
|
||||
<pre style="background: #f5f5f5; padding: 10px; border-radius: 5px;">${envContent}</pre>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
// Beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
refreshLogs();
|
||||
updateMonitoring();
|
||||
|
||||
// Auto-Update alle 10 Sekunden
|
||||
setInterval(() => {
|
||||
updateMonitoring();
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
// Debug-Funktionen
|
||||
function showContainerLogs() {
|
||||
const debugOutput = document.getElementById('debugOutput');
|
||||
const debugContent = document.getElementById('debugContent');
|
||||
|
||||
fetch(`/api/container_logs/${PROJECT_NAME}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
debugOutput.style.display = 'block';
|
||||
if (data.success) {
|
||||
debugContent.textContent = data.logs || 'Keine Logs verfügbar';
|
||||
} else {
|
||||
debugContent.textContent = 'Fehler beim Laden der Logs: ' + data.error;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
debugOutput.style.display = 'block';
|
||||
debugContent.textContent = 'Netzwerkfehler: ' + error.message;
|
||||
});
|
||||
}
|
||||
|
||||
function inspectContainer() {
|
||||
const debugOutput = document.getElementById('debugOutput');
|
||||
const debugContent = document.getElementById('debugContent');
|
||||
|
||||
fetch(`/api/container_inspect/${PROJECT_NAME}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
debugOutput.style.display = 'block';
|
||||
if (data.success) {
|
||||
debugContent.textContent = JSON.stringify(data.container_info, null, 2);
|
||||
} else {
|
||||
debugContent.textContent = 'Fehler beim Inspizieren: ' + data.error;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
debugOutput.style.display = 'block';
|
||||
debugContent.textContent = 'Netzwerkfehler: ' + error.message;
|
||||
});
|
||||
}
|
||||
|
||||
function checkPortStatus() {
|
||||
const debugOutput = document.getElementById('debugOutput');
|
||||
const debugContent = document.getElementById('debugContent');
|
||||
|
||||
fetch(`/api/find_available_port`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
debugOutput.style.display = 'block';
|
||||
if (data.success) {
|
||||
debugContent.textContent = `Nächster freier Port: ${data.port}\n\nPort-Check-Details:\n`;
|
||||
// Prüfe mehrere Ports
|
||||
for (let port = 8080; port <= 8090; port++) {
|
||||
fetch(`/api/check_port/${port}`)
|
||||
.then(resp => resp.json())
|
||||
.then(portData => {
|
||||
debugContent.textContent += `Port ${port}: ${portData.available ? 'FREI' : 'BELEGT'}\n`;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
debugContent.textContent = 'Fehler beim Port-Check: ' + data.error;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
debugOutput.style.display = 'block';
|
||||
debugContent.textContent = 'Netzwerkfehler: ' + error.message;
|
||||
});
|
||||
}
|
||||
|
||||
// Erweiterte Projekt-Entfernung mit Ajax
|
||||
function removeProjectSafely() {
|
||||
if (!confirm(`⚠️ ACHTUNG: Möchten Sie das Projekt "${PROJECT_NAME}" wirklich vollständig entfernen?\n\nDies wird:\n✗ Container stoppen und entfernen\n✗ Docker Image löschen\n✗ Alle Projektdateien löschen\n\nDiese Aktion kann NICHT rückgängig gemacht werden!`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Erstelle Progress Modal
|
||||
const progressModal = `
|
||||
<div class="modal fade" id="removeProgressModal" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-trash me-2"></i>Projekt entfernen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="progress mb-3">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 0%" id="removeProgress"></div>
|
||||
</div>
|
||||
<div id="removeStatus">Beginne Entfernung...</div>
|
||||
<div id="removeDetails" class="mt-3 small text-muted"></div>
|
||||
</div>
|
||||
<div class="modal-footer" id="removeModalFooter" style="display: none;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeRemoveModal()">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Modal hinzufügen und anzeigen
|
||||
document.body.insertAdjacentHTML('beforeend', progressModal);
|
||||
const modal = new bootstrap.Modal(document.getElementById('removeProgressModal'));
|
||||
modal.show();
|
||||
|
||||
// Start removal process
|
||||
updateRemovalProgress(25, 'Container wird gestoppt...');
|
||||
|
||||
fetch(`/api/remove_project/${PROJECT_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
updateRemovalProgress(100, '✅ Projekt erfolgreich entfernt!');
|
||||
document.getElementById('removeDetails').innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<strong>Erfolgreich!</strong><br>
|
||||
${data.message}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Nach 2 Sekunden zur Hauptseite
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
updateRemovalProgress(100, '❌ Fehler beim Entfernen');
|
||||
document.getElementById('removeDetails').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>Fehler:</strong><br>
|
||||
${data.message || data.error}
|
||||
|
||||
${data.message && data.message.includes('Zugriff verweigert') ? `
|
||||
<hr>
|
||||
<strong>Lösungsvorschläge:</strong>
|
||||
<ul class="mb-0">
|
||||
<li>Alle Git-Clients (VS Code, GitHub Desktop, etc.) schließen</li>
|
||||
<li>Als Administrator ausführen</li>
|
||||
<li>Antivirus temporär deaktivieren</li>
|
||||
<li>Manuell löschen: <code>projects/${PROJECT_NAME}</code></li>
|
||||
</ul>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('removeModalFooter').style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
updateRemovalProgress(100, '❌ Netzwerkfehler');
|
||||
document.getElementById('removeDetails').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>Netzwerkfehler:</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('removeModalFooter').style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
function updateRemovalProgress(percent, status) {
|
||||
document.getElementById('removeProgress').style.width = `${percent}%`;
|
||||
document.getElementById('removeStatus').textContent = status;
|
||||
}
|
||||
|
||||
function closeRemoveModal() {
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('removeProgressModal'));
|
||||
modal.hide();
|
||||
document.getElementById('removeProgressModal').remove();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
696
templates/project_details_fixed.html
Normal file
696
templates/project_details_fixed.html
Normal file
@@ -0,0 +1,696 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ project.name }} - Details{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>
|
||||
<i class="fas fa-cube me-2"></i>{{ project.name }}
|
||||
<span class="status-badge status-{{ 'running' if project.status == 'running' else 'stopped' if project.status in ['exited', 'stopped'] else 'unknown' }} ms-2">
|
||||
<i class="fas fa-circle me-1"></i>
|
||||
{% if project.status == 'running' %}Läuft{% elif project.status in ['exited', 'stopped'] %}Gestoppt{% else %}Unbekannt{% endif %}
|
||||
</span>
|
||||
</h2>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Zurück
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Projektinformationen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Projektinformationen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Name:</strong> {{ project.name }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Pfad:</strong> <code>{{ project.path }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Docker verfügbar:</strong>
|
||||
{% if project.has_dockerfile %}
|
||||
<i class="fas fa-check text-success"></i> Ja
|
||||
{% else %}
|
||||
<i class="fas fa-times text-danger"></i> Nein
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Umgebungskonfiguration:</strong>
|
||||
{% if project.has_env_example %}
|
||||
<i class="fas fa-check text-success"></i> .env.example vorhanden
|
||||
{% else %}
|
||||
<i class="fas fa-minus text-warning"></i> Keine .env.example
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Docker Compose:</strong>
|
||||
{% if project.has_docker_compose %}
|
||||
<i class="fas fa-check text-success"></i> Verfügbar
|
||||
{% else %}
|
||||
<i class="fas fa-times text-muted"></i> Nicht verfügbar
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Installiert:</strong> {{ project.created }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if project.readme %}
|
||||
<div class="mt-4">
|
||||
<h6>README:</h6>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre class="mb-0 small">{{ project.readme }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Umgebungskonfiguration -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-edit me-2"></i>Umgebungskonfiguration (.env)
|
||||
</h5>
|
||||
<div>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="resetToExample()">
|
||||
<i class="fas fa-undo"></i> Beispiel wiederherstellen
|
||||
</button>
|
||||
<button class="btn btn-outline-warning btn-sm" onclick="validateEnv()">
|
||||
<i class="fas fa-check-circle"></i> Validieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('save_env', project_name=project.name) }}">
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control font-monospace"
|
||||
name="env_content"
|
||||
id="envContent"
|
||||
rows="15"
|
||||
placeholder="Hier können Sie die Umgebungsvariablen für das Projekt konfigurieren...">{{ env_content }}</textarea>
|
||||
<div class="form-text">
|
||||
Konfigurieren Sie hier die Umgebungsvariablen für Ihr Projekt.
|
||||
Diese werden in die .env Datei gespeichert.
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="button" class="btn btn-outline-secondary me-md-2" onclick="previewChanges()">
|
||||
<i class="fas fa-eye"></i> Vorschau
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> .env speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docker Logs -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-terminal me-2"></i>Container Logs
|
||||
</h5>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="refreshLogs()">
|
||||
<i class="fas fa-sync-alt"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dockerLogs" class="bg-dark text-light p-3 rounded font-monospace small" style="height: 300px; overflow-y: auto;">
|
||||
<div class="text-center text-muted">
|
||||
Logs werden geladen...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Schnellaktionen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-bolt me-2"></i>Container-Steuerung
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
{% if project.status == 'running' %}
|
||||
<a href="{{ url_for('stop_project', project_name=project.name) }}" class="btn btn-warning">
|
||||
<i class="fas fa-stop"></i> Container stoppen
|
||||
</a>
|
||||
<button class="btn btn-info" onclick="restartContainer()">
|
||||
<i class="fas fa-redo"></i> Container neustarten
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-success" id="quickStartButton" onclick="quickStartContainer()">
|
||||
<i class="fas fa-play"></i> Schnellstart (Auto-Port)
|
||||
</button>
|
||||
<button class="btn btn-outline-success" onclick="showPortSelection()">
|
||||
<i class="fas fa-cog"></i> Erweiterte Start-Optionen
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('build_project', project_name=project.name) }}" class="btn btn-primary">
|
||||
<i class="fas fa-hammer"></i> Image neu bauen
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('remove_project', project_name=project.name) }}"
|
||||
class="btn btn-danger"
|
||||
onclick="return confirmAction('remove', '{{ project.name }}')">
|
||||
<i class="fas fa-trash"></i> Projekt entfernen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Port-Status -->
|
||||
{% if project.status == 'running' %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-network-wired me-2"></i>Aktive Verbindungen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="activeConnections">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>HTTP:</span>
|
||||
<a href="http://localhost:8080" target="_blank" class="btn btn-outline-primary btn-sm">
|
||||
:8080 <i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<small class="text-muted">
|
||||
Klicken Sie auf die Links um die Anwendung zu öffnen.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Container-Statistiken -->
|
||||
{% if project.status == 'running' %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-chart-line me-2"></i>Monitoring
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="cpuUsage">-</span>
|
||||
<span class="small text-muted">CPU %</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="memUsage">-</span>
|
||||
<span class="small text-muted">RAM MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center mt-2">
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="networkIn">-</span>
|
||||
<span class="small text-muted">Net In</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="networkOut">-</span>
|
||||
<span class="small text-muted">Net Out</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-outline-info btn-sm w-100" onclick="updateMonitoring()">
|
||||
<i class="fas fa-sync-alt"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Hilfe -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-question-circle me-2"></i>Hilfe & Tipps
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small">
|
||||
{% if not project.has_dockerfile %}
|
||||
<div class="alert alert-warning">
|
||||
<strong>Kein Dockerfile:</strong> Erstellen Sie ein Dockerfile in Ihrem Projektverzeichnis.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not project.has_env_example %}
|
||||
<div class="alert alert-info">
|
||||
<strong>Tipp:</strong> Erstellen Sie eine .env.example Datei für Umgebungsvariablen.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li><i class="fas fa-check text-success"></i> Bauen Sie das Image vor dem ersten Start</li>
|
||||
<li><i class="fas fa-check text-success"></i> Überprüfen Sie die .env Konfiguration</li>
|
||||
<li><i class="fas fa-check text-success"></i> Beachten Sie die Container-Logs bei Problemen</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Globale Variable für Projekt-Name
|
||||
const PROJECT_NAME = '{{ project.name }}';
|
||||
|
||||
// Container-Verwaltung - Verbesserte Version
|
||||
function quickStartContainer() {
|
||||
const button = document.getElementById('quickStartButton');
|
||||
if (!button) return;
|
||||
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Startet...';
|
||||
button.disabled = true;
|
||||
|
||||
console.log('Starting container with auto-port selection...');
|
||||
|
||||
// Verwende API-Endpoint für bessere Kontrolle
|
||||
fetch(`/api/start_project/${PROJECT_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Start response:', data);
|
||||
|
||||
if (data.success) {
|
||||
// Erfolg - zeige Erfolgsmeldung und aktualisiere UI
|
||||
showSuccessMessage(`Container erfolgreich gestartet auf Port ${data.port}!`);
|
||||
|
||||
// Aktualisiere Button-Status nach 2 Sekunden
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
// Fehler behandeln
|
||||
console.error('Start error:', data.error);
|
||||
showErrorMessage(`Fehler beim Start: ${data.error || 'Unbekannter Fehler'}`);
|
||||
|
||||
// Prüfe ob es ein Port-Problem ist
|
||||
if (data.error && (data.error.includes('Port') || data.error.includes('port'))) {
|
||||
setTimeout(() => {
|
||||
showPortSelectionWithError(data.error);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Network error:', error);
|
||||
showErrorMessage(`Netzwerkfehler: ${error.message || 'Unbekannter Netzwerkfehler'}`);
|
||||
})
|
||||
.finally(() => {
|
||||
// Button zurücksetzen
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function showPortSelection() {
|
||||
const portSelection = `
|
||||
<div class="modal fade" id="portSelectionModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Port-Auswahl für ${PROJECT_NAME}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="customPort" class="form-label">Port auswählen:</label>
|
||||
<input type="number" class="form-control" id="customPort" value="8080" min="1" max="65535">
|
||||
<div class="form-text">
|
||||
Beliebte Ports: 8080 (Standard), 3000 (Node.js), 5000 (Flask), 8081-8090 (Alternative)
|
||||
</div>
|
||||
</div>
|
||||
<div id="portStatus" class="mb-3"></div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-info" onclick="checkPortAvailability()">
|
||||
<i class="fas fa-search"></i> Port prüfen
|
||||
</button>
|
||||
<button class="btn btn-outline-warning" onclick="findFreePortForStart()">
|
||||
<i class="fas fa-magic"></i> Automatisch freien Port finden
|
||||
</button>
|
||||
</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-success" onclick="startWithSelectedPort()">
|
||||
<i class="fas fa-play"></i> Container starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Entferne existierendes Modal falls vorhanden
|
||||
const existingModal = document.getElementById('portSelectionModal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// Füge neues Modal hinzu
|
||||
document.body.insertAdjacentHTML('beforeend', portSelection);
|
||||
|
||||
// Modal anzeigen
|
||||
const modal = new bootstrap.Modal(document.getElementById('portSelectionModal'));
|
||||
modal.show();
|
||||
|
||||
// Port direkt prüfen
|
||||
setTimeout(checkPortAvailability, 500);
|
||||
}
|
||||
|
||||
function showPortSelectionWithError(errorMessage) {
|
||||
showPortSelection();
|
||||
|
||||
setTimeout(() => {
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
if (statusElement) {
|
||||
statusElement.innerHTML = `<div class="alert alert-warning"><i class="fas fa-exclamation-triangle"></i> ${errorMessage}</div>`;
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function checkPortAvailability() {
|
||||
const port = document.getElementById('customPort').value;
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
|
||||
if (!port || port < 1 || port > 65535) {
|
||||
statusElement.innerHTML = '<div class="alert alert-danger">Ungültiger Port (1-65535)</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
statusElement.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Prüfe Port verfügbarkeit...</div>';
|
||||
|
||||
fetch(`/api/check_port/${port}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.available) {
|
||||
statusElement.innerHTML = `<div class="alert alert-success"><i class="fas fa-check"></i> Port ${port} ist verfügbar!</div>`;
|
||||
} else {
|
||||
statusElement.innerHTML = `<div class="alert alert-warning"><i class="fas fa-times"></i> Port ${port} ist bereits belegt</div>`;
|
||||
}
|
||||
} else {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Fehler: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Netzwerkfehler: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function findFreePortForStart() {
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
statusElement.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Suche freien Port...</div>';
|
||||
|
||||
fetch('/api/find_available_port?start=8080')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('customPort').value = data.port;
|
||||
statusElement.innerHTML = `<div class="alert alert-success"><i class="fas fa-check"></i> Freier Port gefunden: ${data.port}</div>`;
|
||||
} else {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Kein freier Port gefunden: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Fehler bei Port-Suche: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function startWithSelectedPort() {
|
||||
const port = document.getElementById('customPort').value;
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
|
||||
if (!port || port < 1 || port > 65535) {
|
||||
showErrorMessage('Bitte geben Sie einen gültigen Port ein (1-65535)');
|
||||
return;
|
||||
}
|
||||
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Startet...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/api/start_project/${PROJECT_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({port: parseInt(port)})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Start response:', data);
|
||||
|
||||
if (data.success) {
|
||||
// Erfolg - schließe Modal und zeige Erfolg
|
||||
bootstrap.Modal.getInstance(document.getElementById('portSelectionModal')).hide();
|
||||
showSuccessMessage(`Container erfolgreich gestartet auf Port ${data.port}!`);
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
// Fehler - zeige Fehlermeldung aber behalte Modal offen
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
statusElement.innerHTML = `<div class="alert alert-danger"><i class="fas fa-times"></i> ${data.error}</div>`;
|
||||
|
||||
// Bei Port-Konflikt, schlage Alternative vor
|
||||
if (data.alternative_port) {
|
||||
statusElement.innerHTML += `<button class="btn btn-warning btn-sm mt-2" onclick="useAlternativePort(${data.alternative_port})">
|
||||
<i class="fas fa-arrow-right"></i> Port ${data.alternative_port} verwenden
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Network error:', error);
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Netzwerkfehler: ${error.message}</div>`;
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function useAlternativePort(port) {
|
||||
document.getElementById('customPort').value = port;
|
||||
checkPortAvailability();
|
||||
}
|
||||
|
||||
// Hilfsfunktionen für Nachrichten
|
||||
function showSuccessMessage(message) {
|
||||
showToast(message, 'success');
|
||||
}
|
||||
|
||||
function showErrorMessage(message) {
|
||||
showToast(message, 'error');
|
||||
}
|
||||
|
||||
function showToast(message, type) {
|
||||
const toastHtml = `
|
||||
<div class="toast align-items-center text-white bg-${type === 'success' ? 'success' : 'danger'} border-0" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="fas fa-${type === 'success' ? 'check' : 'exclamation-triangle'} me-2"></i>
|
||||
${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Toast Container erstellen falls nicht vorhanden
|
||||
let toastContainer = document.getElementById('toastContainer');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'toastContainer';
|
||||
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
toastContainer.style.zIndex = '9999';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Toast hinzufügen
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||
|
||||
// Toast anzeigen
|
||||
const toastElement = toastContainer.lastElementChild;
|
||||
const toast = new bootstrap.Toast(toastElement, {delay: 5000});
|
||||
toast.show();
|
||||
|
||||
// Toast nach dem Verstecken entfernen
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Container neustarten - verbessert
|
||||
function restartContainer() {
|
||||
if (!confirm('Container wirklich neustarten?')) return;
|
||||
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Startet neu...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/api/restart_project/${PROJECT_NAME}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showSuccessMessage(data.message);
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
} else {
|
||||
showErrorMessage(data.error || 'Fehler beim Neustart');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showErrorMessage(`Netzwerkfehler: ${error.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Docker Logs - verbessert
|
||||
function refreshLogs() {
|
||||
const logsContainer = document.getElementById('dockerLogs');
|
||||
if (!logsContainer) return;
|
||||
|
||||
logsContainer.innerHTML = '<div class="text-center text-muted">Logs werden geladen...</div>';
|
||||
|
||||
fetch(`/api/container_logs/${PROJECT_NAME}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const logs = data.logs || 'Keine Logs verfügbar';
|
||||
logsContainer.innerHTML = `<pre class="mb-0">${logs}</pre>`;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
} else {
|
||||
logsContainer.innerHTML = `<div class="text-warning">Logs nicht verfügbar: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
logsContainer.innerHTML = `<div class="text-danger">Fehler beim Laden der Logs: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// Monitoring Update
|
||||
function updateMonitoring() {
|
||||
fetch(`/api/container_stats/${PROJECT_NAME}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('cpuUsage').textContent = data.stats.cpu || '-';
|
||||
document.getElementById('memUsage').textContent = data.stats.memory || '-';
|
||||
document.getElementById('networkIn').textContent = data.stats.network_in || '-';
|
||||
document.getElementById('networkOut').textContent = data.stats.network_out || '-';
|
||||
}
|
||||
})
|
||||
.catch(error => console.log('Monitoring update failed:', error));
|
||||
}
|
||||
|
||||
// .env Funktionen
|
||||
function resetToExample() {
|
||||
if (confirm('Möchten Sie die aktuelle .env mit der .env.example überschreiben?')) {
|
||||
fetch(`/api/reset_env/${PROJECT_NAME}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('envContent').value = data.content;
|
||||
showSuccessMessage('.env auf Beispiel zurückgesetzt');
|
||||
} else {
|
||||
showErrorMessage('Fehler: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => showErrorMessage('Netzwerkfehler: ' + error.message));
|
||||
}
|
||||
}
|
||||
|
||||
function validateEnv() {
|
||||
const envContent = document.getElementById('envContent').value;
|
||||
|
||||
fetch(`/api/validate_env/${PROJECT_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({content: envContent})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
showSuccessMessage('✅ .env Konfiguration ist gültig!');
|
||||
} else {
|
||||
showErrorMessage('❌ .env Konfiguration hat Probleme:\n' + data.errors.join('\n'));
|
||||
}
|
||||
})
|
||||
.catch(error => showErrorMessage('Fehler bei der Validierung: ' + error.message));
|
||||
}
|
||||
|
||||
function previewChanges() {
|
||||
const envContent = document.getElementById('envContent').value;
|
||||
const preview = window.open('', '_blank', 'width=600,height=400');
|
||||
preview.document.write(`
|
||||
<html>
|
||||
<head><title>Umgebungskonfiguration Vorschau</title></head>
|
||||
<body style="font-family: monospace; padding: 20px;">
|
||||
<h3>Vorschau der .env Datei:</h3>
|
||||
<pre style="background: #f5f5f5; padding: 10px; border-radius: 5px;">${envContent}</pre>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
// Beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
refreshLogs();
|
||||
updateMonitoring();
|
||||
|
||||
// Auto-Update alle 10 Sekunden
|
||||
setInterval(() => {
|
||||
updateMonitoring();
|
||||
}, 10000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
696
templates/project_details_new.html
Normal file
696
templates/project_details_new.html
Normal file
@@ -0,0 +1,696 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ project.name }} - Details{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>
|
||||
<i class="fas fa-cube me-2"></i>{{ project.name }}
|
||||
<span class="status-badge status-{{ 'running' if project.status == 'running' else 'stopped' if project.status in ['exited', 'stopped'] else 'unknown' }} ms-2">
|
||||
<i class="fas fa-circle me-1"></i>
|
||||
{% if project.status == 'running' %}Läuft{% elif project.status in ['exited', 'stopped'] %}Gestoppt{% else %}Unbekannt{% endif %}
|
||||
</span>
|
||||
</h2>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Zurück
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Projektinformationen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Projektinformationen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Name:</strong> {{ project.name }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Pfad:</strong> <code>{{ project.path }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Docker verfügbar:</strong>
|
||||
{% if project.has_dockerfile %}
|
||||
<i class="fas fa-check text-success"></i> Ja
|
||||
{% else %}
|
||||
<i class="fas fa-times text-danger"></i> Nein
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Umgebungskonfiguration:</strong>
|
||||
{% if project.has_env_example %}
|
||||
<i class="fas fa-check text-success"></i> .env.example vorhanden
|
||||
{% else %}
|
||||
<i class="fas fa-minus text-warning"></i> Keine .env.example
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Docker Compose:</strong>
|
||||
{% if project.has_docker_compose %}
|
||||
<i class="fas fa-check text-success"></i> Verfügbar
|
||||
{% else %}
|
||||
<i class="fas fa-times text-muted"></i> Nicht verfügbar
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Installiert:</strong> {{ project.created }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if project.readme %}
|
||||
<div class="mt-4">
|
||||
<h6>README:</h6>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre class="mb-0 small">{{ project.readme }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Umgebungskonfiguration -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-edit me-2"></i>Umgebungskonfiguration (.env)
|
||||
</h5>
|
||||
<div>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="resetToExample()">
|
||||
<i class="fas fa-undo"></i> Beispiel wiederherstellen
|
||||
</button>
|
||||
<button class="btn btn-outline-warning btn-sm" onclick="validateEnv()">
|
||||
<i class="fas fa-check-circle"></i> Validieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('save_env', project_name=project.name) }}">
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control font-monospace"
|
||||
name="env_content"
|
||||
id="envContent"
|
||||
rows="10"
|
||||
placeholder="Hier können Sie die Umgebungsvariablen für das Projekt konfigurieren...">{{ env_content }}</textarea>
|
||||
<div class="form-text">
|
||||
Konfigurieren Sie hier die Umgebungsvariablen für Ihr Projekt.
|
||||
Diese werden in die .env Datei gespeichert.
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="button" class="btn btn-outline-secondary me-md-2" onclick="previewChanges()">
|
||||
<i class="fas fa-eye"></i> Vorschau
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> .env speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docker Logs -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-terminal me-2"></i>Container Logs
|
||||
</h5>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="refreshLogs()">
|
||||
<i class="fas fa-sync-alt"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dockerLogs" class="bg-dark text-light p-3 rounded font-monospace small" style="height: 300px; overflow-y: auto;">
|
||||
<div class="text-center text-muted">
|
||||
{% if project.status == 'running' %}
|
||||
Logs werden geladen...
|
||||
{% else %}
|
||||
Container ist nicht gestartet. Keine Logs verfügbar.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Schnellaktionen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-bolt me-2"></i>Container-Verwaltung
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
{% if project.status == 'running' %}
|
||||
<a href="{{ url_for('stop_project', project_name=project.name) }}" class="btn btn-warning">
|
||||
<i class="fas fa-stop"></i> Container stoppen
|
||||
</a>
|
||||
<button class="btn btn-info" onclick="restartContainer()">
|
||||
<i class="fas fa-redo"></i> Container neustarten
|
||||
</button>
|
||||
<button class="btn btn-outline-info" onclick="viewContainerDetails()">
|
||||
<i class="fas fa-eye"></i> Container-Details
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-success" onclick="quickStartContainer()" id="quickStartButton">
|
||||
<i class="fas fa-play"></i> Schnellstart (Auto-Port)
|
||||
</button>
|
||||
<button class="btn btn-outline-success" onclick="showPortSelection()" id="customStartButton">
|
||||
<i class="fas fa-cog"></i> Erweiterte Startoptionen
|
||||
</button>
|
||||
{% if not project.has_dockerfile %}
|
||||
<div class="alert alert-warning mt-2">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Kein Dockerfile gefunden! Build ist erforderlich.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="{{ url_for('build_project', project_name=project.name) }}" class="btn btn-primary">
|
||||
<i class="fas fa-hammer"></i> Image neu bauen
|
||||
</a>
|
||||
|
||||
<button class="btn btn-outline-info" onclick="openFileExplorer()">
|
||||
<i class="fas fa-folder-open"></i> Dateien öffnen
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-warning" onclick="exportProject()">
|
||||
<i class="fas fa-download"></i> Projekt exportieren
|
||||
</button>
|
||||
|
||||
<a href="{{ url_for('remove_project', project_name=project.name) }}"
|
||||
class="btn btn-danger"
|
||||
onclick="return confirm('Möchten Sie das Projekt wirklich vollständig entfernen?')">
|
||||
<i class="fas fa-trash"></i> Projekt entfernen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Port-Status -->
|
||||
{% if project.status == 'running' %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-network-wired me-2"></i>Aktive Verbindungen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="activeConnections">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Hauptzugriff:</span>
|
||||
<a href="http://localhost:8080" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-external-link-alt"></i> :8080
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Der tatsächliche Port kann variieren je nach Konfiguration.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Container-Statistiken -->
|
||||
{% if project.status == 'running' %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-chart-line me-2"></i>Monitoring
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="cpuUsage">-</span>
|
||||
<span class="small text-muted">CPU %</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="memUsage">-</span>
|
||||
<span class="small text-muted">RAM MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center mt-2">
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="networkIn">-</span>
|
||||
<span class="small text-muted">Net In</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="networkOut">-</span>
|
||||
<span class="small text-muted">Net Out</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-outline-info btn-sm w-100" onclick="updateMonitoring()">
|
||||
<i class="fas fa-sync-alt"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Hilfe -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-question-circle me-2"></i>Hilfe & Tipps
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small">
|
||||
{% if not project.has_dockerfile %}
|
||||
<div class="alert alert-warning">
|
||||
<strong>Kein Dockerfile:</strong> Erstellen Sie ein Dockerfile in Ihrem Projektverzeichnis.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not project.has_env_example %}
|
||||
<div class="alert alert-info">
|
||||
<strong>Tipp:</strong> Erstellen Sie eine .env.example Datei für Umgebungsvariablen.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li><i class="fas fa-check text-success"></i> Bauen Sie das Image vor dem ersten Start</li>
|
||||
<li><i class="fas fa-check text-success"></i> Überprüfen Sie die .env Konfiguration</li>
|
||||
<li><i class="fas fa-check text-success"></i> Beachten Sie die Container-Logs bei Problemen</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Globale Variable für Projekt-Name
|
||||
const PROJECT_NAME = '{{ project.name }}';
|
||||
|
||||
// Container-Verwaltung - Verbesserte Version
|
||||
function quickStartContainer() {
|
||||
const button = document.getElementById('quickStartButton');
|
||||
if (!button) return;
|
||||
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Startet...';
|
||||
button.disabled = true;
|
||||
|
||||
console.log('Starting container with auto-port selection...');
|
||||
|
||||
// Verwende API-Endpoint für bessere Kontrolle
|
||||
fetch(`/api/start_project/${PROJECT_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Start response:', data);
|
||||
|
||||
if (data.success) {
|
||||
// Erfolg - zeige Erfolgsmeldung und aktualisiere UI
|
||||
showSuccessMessage(`Container erfolgreich gestartet auf Port ${data.port}!`);
|
||||
|
||||
// Aktualisiere Button-Status nach 2 Sekunden
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
// Fehler behandeln
|
||||
console.error('Start error:', data.error);
|
||||
showErrorMessage(`Fehler beim Start: ${data.error}`);
|
||||
|
||||
// Prüfe ob es ein Port-Problem ist
|
||||
if (data.error && (data.error.includes('Port') || data.error.includes('port'))) {
|
||||
setTimeout(() => {
|
||||
showPortSelectionWithError(data.error);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Network error:', error);
|
||||
showErrorMessage(`Netzwerkfehler: ${error.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
// Button zurücksetzen
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function showPortSelection() {
|
||||
const portSelection = `
|
||||
<div class="modal fade" id="portSelectionModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Port-Auswahl für ${PROJECT_NAME}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="customPort" class="form-label">Port auswählen:</label>
|
||||
<input type="number" class="form-control" id="customPort" value="8080" min="1" max="65535">
|
||||
<div class="form-text">
|
||||
Beliebte Ports: 8080 (Standard), 3000 (Node.js), 5000 (Flask), 8081-8090 (Alternative)
|
||||
</div>
|
||||
</div>
|
||||
<div id="portStatus" class="mb-3"></div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-info" onclick="checkPortAvailability()">
|
||||
<i class="fas fa-search"></i> Port prüfen
|
||||
</button>
|
||||
<button class="btn btn-outline-warning" onclick="findFreePortForStart()">
|
||||
<i class="fas fa-magic"></i> Automatisch freien Port finden
|
||||
</button>
|
||||
</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-success" onclick="startWithSelectedPort()">
|
||||
<i class="fas fa-play"></i> Container starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Entferne existierendes Modal falls vorhanden
|
||||
const existingModal = document.getElementById('portSelectionModal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// Füge neues Modal hinzu
|
||||
document.body.insertAdjacentHTML('beforeend', portSelection);
|
||||
|
||||
// Modal anzeigen
|
||||
const modal = new bootstrap.Modal(document.getElementById('portSelectionModal'));
|
||||
modal.show();
|
||||
|
||||
// Port direkt prüfen
|
||||
setTimeout(checkPortAvailability, 500);
|
||||
}
|
||||
|
||||
function showPortSelectionWithError(errorMessage) {
|
||||
showPortSelection();
|
||||
|
||||
setTimeout(() => {
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
if (statusElement) {
|
||||
statusElement.innerHTML = `<div class="alert alert-warning"><i class="fas fa-exclamation-triangle"></i> ${errorMessage}</div>`;
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function checkPortAvailability() {
|
||||
const port = document.getElementById('customPort').value;
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
|
||||
if (!port || port < 1 || port > 65535) {
|
||||
statusElement.innerHTML = '<div class="alert alert-danger">Ungültiger Port (1-65535)</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
statusElement.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Prüfe Port verfügbarkeit...</div>';
|
||||
|
||||
fetch(`/api/check_port/${port}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.available) {
|
||||
statusElement.innerHTML = `<div class="alert alert-success"><i class="fas fa-check"></i> Port ${port} ist verfügbar!</div>`;
|
||||
} else {
|
||||
statusElement.innerHTML = `<div class="alert alert-warning"><i class="fas fa-times"></i> Port ${port} ist bereits belegt</div>`;
|
||||
}
|
||||
} else {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Fehler: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Netzwerkfehler: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function findFreePortForStart() {
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
statusElement.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Suche freien Port...</div>';
|
||||
|
||||
fetch('/api/find_available_port?start=8080')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('customPort').value = data.port;
|
||||
statusElement.innerHTML = `<div class="alert alert-success"><i class="fas fa-check"></i> Freier Port gefunden: ${data.port}</div>`;
|
||||
} else {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Kein freier Port gefunden: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusElement.innerHTML = `<div class="alert alert-danger">Fehler bei Port-Suche: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function startWithSelectedPort() {
|
||||
const port = document.getElementById('customPort').value;
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
|
||||
if (!port || port < 1 || port > 65535) {
|
||||
showErrorMessage('Bitte geben Sie einen gültigen Port ein (1-65535)');
|
||||
return;
|
||||
}
|
||||
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Startet...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/api/start_project/${PROJECT_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({port: parseInt(port)})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Modal schließen
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('portSelectionModal'));
|
||||
modal.hide();
|
||||
|
||||
showSuccessMessage(`Container erfolgreich gestartet auf Port ${data.port}!`);
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
showErrorMessage(`Fehler beim Start: ${data.error}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showErrorMessage(`Netzwerkfehler: ${error.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Hilfsfunktionen für Nachrichten
|
||||
function showSuccessMessage(message) {
|
||||
showToast(message, 'success');
|
||||
}
|
||||
|
||||
function showErrorMessage(message) {
|
||||
showToast(message, 'error');
|
||||
}
|
||||
|
||||
function showToast(message, type) {
|
||||
const toastHtml = `
|
||||
<div class="toast align-items-center text-white bg-${type === 'success' ? 'success' : 'danger'} border-0" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="fas fa-${type === 'success' ? 'check' : 'exclamation-triangle'} me-2"></i>
|
||||
${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Toast Container erstellen falls nicht vorhanden
|
||||
let toastContainer = document.getElementById('toastContainer');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'toastContainer';
|
||||
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
toastContainer.style.zIndex = '9999';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Toast hinzufügen
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||
|
||||
// Toast anzeigen
|
||||
const toastElement = toastContainer.lastElementChild;
|
||||
const toast = new bootstrap.Toast(toastElement, {delay: 5000});
|
||||
toast.show();
|
||||
|
||||
// Toast nach dem Verstecken entfernen
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Container neustarten - verbessert
|
||||
function restartContainer() {
|
||||
if (!confirm('Container wirklich neustarten?')) return;
|
||||
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Startet neu...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/api/restart_project/${PROJECT_NAME}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showSuccessMessage(data.message);
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
} else {
|
||||
showErrorMessage(data.error || 'Fehler beim Neustart');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showErrorMessage(`Netzwerkfehler: ${error.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Docker Logs - verbessert
|
||||
function refreshLogs() {
|
||||
const logsContainer = document.getElementById('dockerLogs');
|
||||
if (!logsContainer) return;
|
||||
|
||||
logsContainer.innerHTML = '<div class="text-center text-muted">Logs werden geladen...</div>';
|
||||
|
||||
fetch(`/api/container_logs/${PROJECT_NAME}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const logs = data.logs || 'Keine Logs verfügbar';
|
||||
logsContainer.innerHTML = `<pre class="mb-0">${logs}</pre>`;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
} else {
|
||||
logsContainer.innerHTML = `<div class="text-warning">Logs nicht verfügbar: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
logsContainer.innerHTML = `<div class="text-danger">Fehler beim Laden der Logs: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// Monitoring Update
|
||||
function updateMonitoring() {
|
||||
fetch(`/api/container_stats/${PROJECT_NAME}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.stats) {
|
||||
document.getElementById('cpuUsage').textContent = data.stats.cpu || '-';
|
||||
document.getElementById('memUsage').textContent = data.stats.memory || '-';
|
||||
document.getElementById('networkIn').textContent = data.stats.network_in || '-';
|
||||
document.getElementById('networkOut').textContent = data.stats.network_out || '-';
|
||||
}
|
||||
})
|
||||
.catch(error => console.log('Monitoring update failed:', error));
|
||||
}
|
||||
|
||||
// Andere Funktionen (vereinfacht)
|
||||
function resetToExample() {
|
||||
showToast('Diese Funktion wird implementiert', 'info');
|
||||
}
|
||||
|
||||
function validateEnv() {
|
||||
showToast('Validierung wird implementiert', 'info');
|
||||
}
|
||||
|
||||
function previewChanges() {
|
||||
const envContent = document.getElementById('envContent').value;
|
||||
const preview = window.open('', '_blank', 'width=600,height=400');
|
||||
preview.document.write(`
|
||||
<html>
|
||||
<head><title>Umgebungskonfiguration Vorschau</title></head>
|
||||
<body style="font-family: monospace; padding: 20px;">
|
||||
<h3>Vorschau der .env Datei:</h3>
|
||||
<pre style="background: #f5f5f5; padding: 10px; border-radius: 5px;">${envContent}</pre>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
function openFileExplorer() {
|
||||
showToast('File Explorer-Integration wird in einer zukünftigen Version verfügbar sein.', 'info');
|
||||
}
|
||||
|
||||
function exportProject() {
|
||||
if (confirm('Projekt als Archiv exportieren?')) {
|
||||
showToast('Export-Funktion wird implementiert', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function viewContainerDetails() {
|
||||
showToast('Container-Details werden implementiert', 'info');
|
||||
}
|
||||
|
||||
// Initialisierung beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log(`Project Details loaded for: ${PROJECT_NAME}`);
|
||||
|
||||
// Lade Logs wenn Container läuft
|
||||
const status = '{{ project.status }}';
|
||||
if (status === 'running') {
|
||||
refreshLogs();
|
||||
updateMonitoring();
|
||||
|
||||
// Auto-refresh alle 30 Sekunden
|
||||
setInterval(() => {
|
||||
refreshLogs();
|
||||
updateMonitoring();
|
||||
}, 30000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
709
templates/project_details_old.html
Normal file
709
templates/project_details_old.html
Normal file
@@ -0,0 +1,709 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ project.name }} - Details{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>
|
||||
<i class="fas fa-cube me-2"></i>{{ project.name }}
|
||||
<span class="status-badge status-{{ 'running' if project.status == 'running' else 'stopped' if project.status in ['exited', 'stopped'] else 'unknown' }} ms-2">
|
||||
<i class="fas fa-circle me-1"></i>
|
||||
{% if project.status == 'running' %}Läuft{% elif project.status in ['exited', 'stopped'] %}Gestoppt{% else %}Unbekannt{% endif %}
|
||||
</span>
|
||||
</h2>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Zurück
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Projektinformationen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Projektinformationen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Name:</strong> {{ project.name }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Pfad:</strong> <code>{{ project.path }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Docker verfügbar:</strong>
|
||||
{% if project.has_dockerfile %}
|
||||
<i class="fas fa-check text-success"></i> Ja
|
||||
{% else %}
|
||||
<i class="fas fa-times text-danger"></i> Nein
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Umgebungskonfiguration:</strong>
|
||||
{% if project.has_env_example %}
|
||||
<i class="fas fa-check text-success"></i> .env.example vorhanden
|
||||
{% else %}
|
||||
<i class="fas fa-minus text-warning"></i> Keine .env.example
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Docker Compose:</strong>
|
||||
{% if project.has_docker_compose %}
|
||||
<i class="fas fa-check text-success"></i> Verfügbar
|
||||
{% else %}
|
||||
<i class="fas fa-times text-muted"></i> Nicht verfügbar
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Installiert:</strong> {{ project.created }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if project.readme %}
|
||||
<div class="mt-4">
|
||||
<h6>README:</h6>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre class="mb-0 small">{{ project.readme }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Umgebungskonfiguration -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-edit me-2"></i>Umgebungskonfiguration (.env)
|
||||
</h5>
|
||||
<div>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="resetToExample()">
|
||||
<i class="fas fa-undo"></i> Beispiel wiederherstellen
|
||||
</button>
|
||||
<button class="btn btn-outline-warning btn-sm" onclick="validateEnv()">
|
||||
<i class="fas fa-check-circle"></i> Validieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('save_env', project_name=project.name) }}">
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control font-monospace"
|
||||
name="env_content"
|
||||
id="envContent"
|
||||
rows="15"
|
||||
placeholder="Hier können Sie die Umgebungsvariablen für das Projekt konfigurieren...">{{ env_content }}</textarea>
|
||||
<div class="form-text">
|
||||
Konfigurieren Sie hier die Umgebungsvariablen für Ihr Projekt.
|
||||
Diese werden in die .env Datei gespeichert.
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="button" class="btn btn-outline-secondary me-md-2" onclick="previewChanges()">
|
||||
<i class="fas fa-eye"></i> Vorschau
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> .env speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docker Logs -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-terminal me-2"></i>Container Logs
|
||||
</h5>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="refreshLogs()">
|
||||
<i class="fas fa-sync-alt"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dockerLogs" class="bg-dark text-light p-3 rounded font-monospace small" style="height: 300px; overflow-y: auto;">
|
||||
<div class="text-center text-muted">
|
||||
Logs werden geladen...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Schnellaktionen -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-bolt me-2"></i>Schnellaktionen
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
{% if project.status == 'running' %}
|
||||
<a href="{{ url_for('stop_project', project_name=project.name) }}" class="btn btn-warning">
|
||||
<i class="fas fa-stop"></i> Container stoppen
|
||||
</a>
|
||||
<button class="btn btn-info" onclick="restartContainer()">
|
||||
<i class="fas fa-redo"></i> Container neustarten
|
||||
</button>
|
||||
{% else %}
|
||||
<div class="btn-group w-100 mb-2" role="group">
|
||||
<button class="btn btn-success" onclick="startWithPort(8080)" id="quickStartButton">
|
||||
<i class="fas fa-play"></i> Schnellstart (Port 8080)
|
||||
</button>
|
||||
<button class="btn btn-outline-success" onclick="showPortSelection()" id="customStartButton">
|
||||
<i class="fas fa-cog"></i> Erweitert
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert alert-info alert-sm p-2">
|
||||
<small><i class="fas fa-info-circle me-1"></i>Port wird automatisch geprüft und angepasst falls belegt</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('build_project', project_name=project.name) }}" class="btn btn-primary">
|
||||
<i class="fas fa-hammer"></i> Image neu bauen
|
||||
</a>
|
||||
|
||||
<button class="btn btn-outline-info" onclick="openFileExplorer()">
|
||||
<i class="fas fa-folder-open"></i> Dateien öffnen
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-warning" onclick="exportProject()">
|
||||
<i class="fas fa-download"></i> Projekt exportieren
|
||||
</button>
|
||||
|
||||
<a href="{{ url_for('remove_project', project_name=project.name) }}"
|
||||
class="btn btn-danger"
|
||||
onclick="return confirmAction('remove', '{{ project.name }}')">
|
||||
<i class="fas fa-trash"></i> Projekt entfernen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Port Konfiguration -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-network-wired me-2"></i>Port Konfiguration
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="httpPort" class="form-label">Gewünschter Port:</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="httpPort" value="8080" min="1" max="65535">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="checkPort()">
|
||||
<i class="fas fa-search"></i> Prüfen
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<span id="portStatus" class="text-muted">Port-Status wird hier angezeigt</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Häufige Ports:</label>
|
||||
<div class="btn-group-vertical w-100" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setPort(8080)">8080 - Standard Web</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setPort(3000)">3000 - Node.js</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setPort(5000)">5000 - Flask</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="findFreePort()">🔍 Freien Port finden</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="autoPort">
|
||||
<label class="form-check-label" for="autoPort">
|
||||
Automatische Portzuweisung
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if project.status == 'running' %}
|
||||
<div class="mt-3">
|
||||
<h6>Aktive Ports:</h6>
|
||||
<div id="activePorts" class="small">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>HTTP:</span>
|
||||
<a href="http://localhost:8080" target="_blank" class="text-decoration-none">
|
||||
:8080 <i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup & Restore -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-shield-alt me-2"></i>Backup & Wiederherstellung
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-success btn-sm" onclick="createBackup()">
|
||||
<i class="fas fa-save"></i> Backup erstellen
|
||||
</button>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="document.getElementById('restoreFile').click()">
|
||||
<i class="fas fa-upload"></i> Backup wiederherstellen
|
||||
</button>
|
||||
<input type="file" id="restoreFile" style="display: none" accept=".tar.gz,.zip" onchange="restoreBackup(event)">
|
||||
<button class="btn btn-outline-warning btn-sm" onclick="showBackupHistory()">
|
||||
<i class="fas fa-history"></i> Backup Historie
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monitoring -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-chart-line me-2"></i>Monitoring
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="cpuUsage">-</span>
|
||||
<span class="small text-muted">CPU %</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="memUsage">-</span>
|
||||
<span class="small text-muted">RAM MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center mt-2">
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="networkIn">-</span>
|
||||
<span class="small text-muted">Net In</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number small" id="networkOut">-</span>
|
||||
<span class="small text-muted">Net Out</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Port Modal -->
|
||||
<div class="modal fade" id="portModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Container starten</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="startForm">
|
||||
<div class="mb-3">
|
||||
<label for="startPort" class="form-label">Port (Standard: 8080)</label>
|
||||
<input type="number" class="form-control" id="startPort" value="8080" min="1" max="65535">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="detachedMode" checked>
|
||||
<label class="form-check-label" for="detachedMode">
|
||||
Im Hintergrund ausführen
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="button" class="btn btn-success" onclick="startWithCustomPort()">
|
||||
<i class="fas fa-play"></i> Starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Container starten - Verbesserte Version mit Fehlerbehandlung
|
||||
function startWithPort(port) {
|
||||
const button = event?.target;
|
||||
if (button) {
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Startet...';
|
||||
button.disabled = true;
|
||||
|
||||
// Führe den Start-Request direkt aus
|
||||
fetch(`/start_project/{{ project.name }}?port=${port}`)
|
||||
.then(response => {
|
||||
if (response.redirected) {
|
||||
// Flask hat redirect gemacht, folge dem
|
||||
window.location.href = response.url;
|
||||
} else {
|
||||
// Erwarte JSON-Response
|
||||
return response.json().catch(() => {
|
||||
// Falls kein JSON, behandle als Erfolg und lade Seite neu
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data && !data.success) {
|
||||
// Zeige spezifische Fehlermeldung
|
||||
if (data.message.includes('bereits belegt')) {
|
||||
const portMatch = data.message.match(/Port (\d+)/);
|
||||
if (portMatch) {
|
||||
const altPort = data.message.match(/(\d+)\./);
|
||||
if (altPort && confirm(`Port ${portMatch[1]} ist belegt. Möchten Sie Port ${altPort[1]} verwenden?`)) {
|
||||
startWithPort(altPort[1]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
alert('Fehler beim Starten: ' + data.message);
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
} else {
|
||||
// Erfolg - lade Seite neu nach kurzer Verzögerung
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Start-Fehler:', error);
|
||||
// Bei Fehlern einfach zur ursprünglichen URL navigieren
|
||||
window.location.href = `/start_project/{{ project.name }}?port=${port}`;
|
||||
});
|
||||
} else {
|
||||
// Fallback für direkte Aufrufe
|
||||
window.location.href = `/start_project/{{ project.name }}?port=${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
function showPortSelection() {
|
||||
// Verwende den konfigurierten Port aus dem Port-Konfigurations-Panel
|
||||
const configuredPort = document.getElementById('httpPort').value;
|
||||
if (configuredPort && !isNaN(configuredPort) && configuredPort > 0 && configuredPort <= 65535) {
|
||||
startWithPort(configuredPort);
|
||||
} else {
|
||||
// Fallback auf Port-Eingabe
|
||||
let port;
|
||||
do {
|
||||
port = prompt('Welchen Port möchten Sie verwenden?\n\nHäufig verwendete Ports:\n8080 - Standard Web\n3000 - Node.js Apps\n5000 - Flask Apps\n8081-8090 - Alternative Ports', '8080');
|
||||
|
||||
if (port === null) return; // Benutzer hat abgebrochen
|
||||
|
||||
port = parseInt(port);
|
||||
if (isNaN(port) || port < 1 || port > 65535) {
|
||||
alert('Bitte geben Sie einen gültigen Port zwischen 1 und 65535 ein.');
|
||||
port = null; // Damit die Schleife weitergeht
|
||||
} else if (port < 1024) {
|
||||
if (!confirm(`Port ${port} ist ein System-Port. Dies könnte Probleme verursachen. Trotzdem verwenden?`)) {
|
||||
port = null;
|
||||
}
|
||||
}
|
||||
} while (port === null);
|
||||
|
||||
startWithPort(port);
|
||||
}
|
||||
}
|
||||
|
||||
function checkPortAvailability(port) {
|
||||
return fetch(`/api/check_port/${port}`)
|
||||
.then(response => response.json())
|
||||
.then(data => data.available)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
function showPortModal() {
|
||||
// Fallback auf Port-Auswahl falls Modal nicht funktioniert
|
||||
showPortSelection();
|
||||
}
|
||||
|
||||
function startWithCustomPort() {
|
||||
// Nicht mehr verwendet - für Rückwärtskompatibilität
|
||||
const port = document.getElementById('startPort')?.value || 8080;
|
||||
startWithPort(port);
|
||||
}
|
||||
|
||||
// Container neustarten
|
||||
function restartContainer() {
|
||||
if (confirm('Container wirklich neustarten?')) {
|
||||
fetch(`/api/restart_project/{{ project.name }}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Neustart: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => alert('Netzwerkfehler: ' + error));
|
||||
}
|
||||
}
|
||||
|
||||
// .env Funktionen
|
||||
function resetToExample() {
|
||||
if (confirm('Möchten Sie die aktuelle .env mit der .env.example überschreiben?')) {
|
||||
fetch(`/api/reset_env/{{ project.name }}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('envContent').value = data.content;
|
||||
} else {
|
||||
alert('Fehler: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => alert('Netzwerkfehler: ' + error));
|
||||
}
|
||||
}
|
||||
|
||||
function validateEnv() {
|
||||
const envContent = document.getElementById('envContent').value;
|
||||
|
||||
fetch(`/api/validate_env/{{ project.name }}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({content: envContent})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
alert('✅ .env Konfiguration ist gültig!');
|
||||
} else {
|
||||
alert('❌ .env Konfiguration hat Probleme:\n' + data.errors.join('\n'));
|
||||
}
|
||||
})
|
||||
.catch(error => alert('Fehler bei der Validierung: ' + error));
|
||||
}
|
||||
|
||||
function previewChanges() {
|
||||
const envContent = document.getElementById('envContent').value;
|
||||
const preview = window.open('', '_blank', 'width=600,height=400');
|
||||
preview.document.write(`
|
||||
<html>
|
||||
<head><title>Umgebungskonfiguration Vorschau</title></head>
|
||||
<body style="font-family: monospace; padding: 20px;">
|
||||
<h3>Vorschau der .env Datei:</h3>
|
||||
<pre style="background: #f5f5f5; padding: 10px; border-radius: 5px;">${envContent}</pre>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
// Docker Logs
|
||||
function refreshLogs() {
|
||||
const logsContainer = document.getElementById('dockerLogs');
|
||||
logsContainer.innerHTML = '<div class="text-center text-muted">Logs werden geladen...</div>';
|
||||
|
||||
fetch(`/api/container_logs/{{ project.name }}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
logsContainer.innerHTML = `<pre class="mb-0">${data.logs || 'Keine Logs verfügbar'}</pre>`;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
} else {
|
||||
logsContainer.innerHTML = `<div class="text-danger">Fehler beim Laden der Logs: ${data.error}</div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
logsContainer.innerHTML = `<div class="text-danger">Netzwerkfehler: ${error}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// File Explorer
|
||||
function openFileExplorer() {
|
||||
// In einer realen Implementierung würde hier ein File Manager geöffnet
|
||||
alert('File Explorer-Integration wird in einer zukünftigen Version verfügbar sein.');
|
||||
}
|
||||
|
||||
// Projekt Export
|
||||
function exportProject() {
|
||||
if (confirm('Projekt als Archiv exportieren?')) {
|
||||
window.location.href = `/api/export_project/{{ project.name }}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Backup Funktionen
|
||||
function createBackup() {
|
||||
if (confirm('Backup des Projekts erstellen?')) {
|
||||
fetch(`/api/create_backup/{{ project.name }}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('✅ Backup erfolgreich erstellt: ' + data.filename);
|
||||
} else {
|
||||
alert('❌ Backup-Fehler: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => alert('Netzwerkfehler: ' + error));
|
||||
}
|
||||
}
|
||||
|
||||
function restoreBackup(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (confirm('Projekt aus Backup wiederherstellen? Aktuelle Daten werden überschrieben!')) {
|
||||
const formData = new FormData();
|
||||
formData.append('backup_file', file);
|
||||
|
||||
fetch(`/api/restore_backup/{{ project.name }}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('✅ Backup erfolgreich wiederhergestellt!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('❌ Wiederherstellungs-Fehler: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => alert('Netzwerkfehler: ' + error));
|
||||
}
|
||||
}
|
||||
|
||||
function showBackupHistory() {
|
||||
fetch(`/api/backup_history/{{ project.name }}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let historyHtml = '<h5>Backup Historie:</h5><ul>';
|
||||
if (data.backups.length > 0) {
|
||||
data.backups.forEach(backup => {
|
||||
historyHtml += `<li>${backup.name} (${backup.date}) - ${backup.size}</li>`;
|
||||
});
|
||||
} else {
|
||||
historyHtml += '<li>Keine Backups vorhanden</li>';
|
||||
}
|
||||
historyHtml += '</ul>';
|
||||
|
||||
const popup = window.open('', '_blank', 'width=500,height=400');
|
||||
popup.document.write(`
|
||||
<html>
|
||||
<head><title>Backup Historie</title></head>
|
||||
<body style="padding: 20px; font-family: Arial, sans-serif;">
|
||||
${historyHtml}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
})
|
||||
.catch(error => alert('Fehler beim Laden der Historie: ' + error));
|
||||
}
|
||||
|
||||
// Monitoring Update
|
||||
function updateMonitoring() {
|
||||
fetch(`/api/container_stats/{{ project.name }}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('cpuUsage').textContent = data.stats.cpu || '-';
|
||||
document.getElementById('memUsage').textContent = data.stats.memory || '-';
|
||||
document.getElementById('networkIn').textContent = data.stats.network_in || '-';
|
||||
document.getElementById('networkOut').textContent = data.stats.network_out || '-';
|
||||
}
|
||||
})
|
||||
.catch(error => console.log('Monitoring update failed:', error));
|
||||
}
|
||||
|
||||
// Port-Management Funktionen
|
||||
function setPort(port) {
|
||||
document.getElementById('httpPort').value = port;
|
||||
checkPort();
|
||||
}
|
||||
|
||||
function checkPort() {
|
||||
const port = document.getElementById('httpPort').value;
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
|
||||
if (!port || port < 1 || port > 65535) {
|
||||
statusElement.innerHTML = '<span class="text-danger">Ungültiger Port</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
statusElement.innerHTML = '<span class="text-info"><i class="fas fa-spinner fa-spin"></i> Prüfe...</span>';
|
||||
|
||||
fetch(`/api/check_port/${port}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.available) {
|
||||
statusElement.innerHTML = `<span class="text-success"><i class="fas fa-check"></i> Port ${port} ist verfügbar</span>`;
|
||||
} else {
|
||||
statusElement.innerHTML = `<span class="text-warning"><i class="fas fa-exclamation-triangle"></i> Port ${port} ist belegt</span>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusElement.innerHTML = '<span class="text-danger"><i class="fas fa-times"></i> Fehler bei Port-Prüfung</span>';
|
||||
});
|
||||
}
|
||||
|
||||
function findFreePort() {
|
||||
const statusElement = document.getElementById('portStatus');
|
||||
statusElement.innerHTML = '<span class="text-info"><i class="fas fa-spinner fa-spin"></i> Suche freien Port...</span>';
|
||||
|
||||
fetch('/api/find_available_port?start=8080')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('httpPort').value = data.port;
|
||||
statusElement.innerHTML = `<span class="text-success"><i class="fas fa-check"></i> Freier Port gefunden: ${data.port}</span>`;
|
||||
} else {
|
||||
statusElement.innerHTML = '<span class="text-warning"><i class="fas fa-exclamation-triangle"></i> Kein freier Port gefunden</span>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
statusElement.innerHTML = '<span class="text-danger"><i class="fas fa-times"></i> Fehler bei Port-Suche</span>';
|
||||
});
|
||||
}
|
||||
|
||||
// Beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Project Details Page loaded');
|
||||
|
||||
// Prüfe ob alle wichtigen Elemente vorhanden sind
|
||||
const quickStartButton = document.getElementById('quickStartButton');
|
||||
const customStartButton = document.getElementById('customStartButton');
|
||||
|
||||
if (quickStartButton) {
|
||||
console.log('Quick Start Button gefunden');
|
||||
}
|
||||
if (customStartButton) {
|
||||
console.log('Custom Start Button gefunden');
|
||||
}
|
||||
|
||||
refreshLogs();
|
||||
updateMonitoring();
|
||||
|
||||
// Auto-Update alle 10 Sekunden
|
||||
setInterval(() => {
|
||||
updateMonitoring();
|
||||
if (document.getElementById('autoRefreshLogs')?.checked) {
|
||||
refreshLogs();
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user