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:
SimolZimol
2025-07-04 23:50:04 +02:00
commit 0f83f15588
16 changed files with 6457 additions and 0 deletions

191
README.md Normal file
View 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

1352
app.py Normal file

File diff suppressed because it is too large Load Diff

14
config.example.json Normal file
View 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
View 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
View 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
View 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
View 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

View 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">&nbsp;</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
View 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
View 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 %}

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

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

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

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

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