From 0f83f15588362c358bb4d6ac76d3f0a467527bbf Mon Sep 17 00:00:00 2001 From: SimolZimol <70102430+SimolZimol@users.noreply.github.com> Date: Fri, 4 Jul 2025 23:50:04 +0200 Subject: [PATCH] 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 --- README.md | 191 ++++ app.py | 1352 ++++++++++++++++++++++++++ config.example.json | 14 + config.json | 14 + docker_diagnose.py | 237 +++++ requirements.txt | 5 + start.bat | 67 ++ templates/available_projects.html | 310 ++++++ templates/base.html | 223 +++++ templates/config.html | 392 ++++++++ templates/docker_status.html | 425 ++++++++ templates/index.html | 220 +++++ templates/project_details.html | 906 +++++++++++++++++ templates/project_details_fixed.html | 696 +++++++++++++ templates/project_details_new.html | 696 +++++++++++++ templates/project_details_old.html | 709 ++++++++++++++ 16 files changed, 6457 insertions(+) create mode 100644 README.md create mode 100644 app.py create mode 100644 config.example.json create mode 100644 config.json create mode 100644 docker_diagnose.py create mode 100644 requirements.txt create mode 100644 start.bat create mode 100644 templates/available_projects.html create mode 100644 templates/base.html create mode 100644 templates/config.html create mode 100644 templates/docker_status.html create mode 100644 templates/index.html create mode 100644 templates/project_details.html create mode 100644 templates/project_details_fixed.html create mode 100644 templates/project_details_new.html create mode 100644 templates/project_details_old.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..d64c715 --- /dev/null +++ b/README.md @@ -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/` - Projekt bauen +- `GET /start_project/` - Projekt starten +- `GET /stop_project/` - Projekt stoppen +- `GET /project_details/` - 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 diff --git a/app.py b/app.py new file mode 100644 index 0000000..f4d81f4 --- /dev/null +++ b/app.py @@ -0,0 +1,1352 @@ +from flask import Flask, render_template, request, jsonify, flash, redirect, url_for +import requests +import os +import subprocess +import json +import re +from urllib.parse import urlparse +import docker +import yaml +from datetime import datetime + +app = Flask(__name__) +app.secret_key = 'your-secret-key-change-this' + +# Configuration +CONFIG_FILE = 'config.json' +PROJECTS_DIR = 'projects' +APPS_DIR = 'apps' + +class ProjectManager: + def __init__(self): + self.docker_client = None + self.docker_available = False + try: + self.docker_client = docker.from_env() + # Teste Docker-Verbindung + self.docker_client.ping() + self.docker_available = True + print("✓ Docker erfolgreich verbunden") + except docker.errors.DockerException as e: + print(f"Docker-Verbindungsfehler: {e}") + self.docker_client = None + except Exception as e: + print(f"Docker nicht verfügbar: {e}") + self.docker_client = None + + # Erstelle notwendige Verzeichnisse + os.makedirs(PROJECTS_DIR, exist_ok=True) + os.makedirs(APPS_DIR, exist_ok=True) + + def load_config(self): + """Lade Konfiguration aus config.json""" + if os.path.exists(CONFIG_FILE): + with open(CONFIG_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + return { + 'project_list_url': '', + 'auto_refresh_minutes': 30, + 'docker_registry': '', + 'projects': [] + } + + def save_config(self, config): + """Speichere Konfiguration in config.json""" + with open(CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + def fetch_project_list(self, url): + """Hole Projektliste von einer URL""" + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + + # Versuche JSON zu parsen + try: + return response.json() + except: + # Falls kein JSON, extrahiere URLs aus HTML/Text + urls = re.findall(r'https?://[^\s<>"]+', response.text) + git_urls = [url for url in urls if any(domain in url for domain in ['github.com', 'gitlab.com', 'gitea'])] + return [{'url': url, 'name': self.extract_project_name(url)} for url in git_urls] + + except Exception as e: + print(f"Fehler beim Abrufen der Projektliste: {e}") + return [] + + def extract_project_name(self, url): + """Extrahiere Projektname aus URL""" + path = urlparse(url).path + parts = path.strip('/').split('/') + return parts[-1] if parts else 'unknown' + + def clone_project(self, project_url, project_name): + """Klone Projekt aus Git-Repository""" + project_path = os.path.join(PROJECTS_DIR, project_name) + + if os.path.exists(project_path): + return False, f"Projekt {project_name} existiert bereits" + + try: + result = subprocess.run([ + 'git', 'clone', project_url, project_path + ], capture_output=True, text=True, timeout=300) + + if result.returncode == 0: + return True, f"Projekt {project_name} erfolgreich geklont" + else: + return False, f"Fehler beim Klonen: {result.stderr}" + + except subprocess.TimeoutExpired: + return False, "Timeout beim Klonen des Projekts" + except Exception as e: + return False, f"Fehler beim Klonen: {str(e)}" + + def get_project_info(self, project_name): + """Hole Projektinformationen""" + project_path = os.path.join(PROJECTS_DIR, project_name) + + if not os.path.exists(project_path): + return None + + info = { + 'name': project_name, + 'path': project_path, + 'has_dockerfile': os.path.exists(os.path.join(project_path, 'Dockerfile')), + 'has_env_example': os.path.exists(os.path.join(project_path, '.env.example')), + 'has_docker_compose': os.path.exists(os.path.join(project_path, 'docker-compose.yml')), + 'status': self.get_container_status(project_name), + 'created': datetime.fromtimestamp(os.path.getctime(project_path)).strftime('%Y-%m-%d %H:%M') + } + + # Lese README falls vorhanden + readme_files = ['README.md', 'readme.md', 'README.txt', 'readme.txt'] + for readme in readme_files: + readme_path = os.path.join(project_path, readme) + if os.path.exists(readme_path): + with open(readme_path, 'r', encoding='utf-8', errors='ignore') as f: + info['readme'] = f.read()[:1000] # Ersten 1000 Zeichen + break + + return info + + def get_container_status(self, project_name): + """Prüfe Container-Status""" + if not self.docker_available or not self.docker_client: + return 'docker_unavailable' + + try: + containers = self.docker_client.containers.list(all=True, filters={'name': project_name}) + if containers: + return containers[0].status + return 'not_created' + except docker.errors.DockerException as e: + print(f"Docker-Fehler beim Status-Check: {e}") + return 'docker_error' + except Exception as e: + print(f"Unerwarteter Fehler beim Status-Check: {e}") + return 'unknown' + + def build_project(self, project_name): + """Baue Docker-Image für Projekt""" + project_path = os.path.join(PROJECTS_DIR, project_name) + + if not os.path.exists(os.path.join(project_path, 'Dockerfile')): + return False, "Kein Dockerfile gefunden" + + if not self.docker_available: + return False, "Docker ist nicht verfügbar. Bitte starten Sie Docker Desktop." + + try: + # Kopiere .env.example zu .env falls nicht vorhanden + env_example = os.path.join(project_path, '.env.example') + env_file = os.path.join(project_path, '.env') + + if os.path.exists(env_example) and not os.path.exists(env_file): + with open(env_example, 'r', encoding='utf-8') as src, open(env_file, 'w', encoding='utf-8') as dst: + dst.write(src.read()) + + # Docker Build + print(f"Building Docker image for {project_name}...") + + # Analysiere Dockerfile vor dem Build + dockerfile_ports = self.analyze_dockerfile_ports(project_path) + if dockerfile_ports: + print(f"Dockerfile exponiert Ports: {dockerfile_ports}") + + image, logs = self.docker_client.images.build( + path=project_path, + tag=f"{project_name}:latest", + rm=True + ) + + # Log build output for debugging + for log in logs: + if 'stream' in log: + print(log['stream'].strip()) + + # Analysiere Image nach dem Build + built_ports = self.detect_container_exposed_ports(project_name) + port_info = "" + if built_ports: + port_info = f" (Exponierte Ports: {', '.join(built_ports)})" + + return True, f"Image {project_name}:latest erfolgreich gebaut{port_info}" + + except docker.errors.BuildError as e: + error_msg = "Build-Fehler:\n" + for log in e.build_log: + if 'stream' in log: + error_msg += log['stream'] + return False, error_msg + + except docker.errors.DockerException as e: + return False, f"Docker-Fehler beim Build: {str(e)}" + + except Exception as e: + return False, f"Unerwarteter Fehler beim Build: {str(e)}" + + def check_port_availability(self, port): + """Prüfe ob ein Port verfügbar ist""" + import socket + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(1) + result = sock.connect_ex(('localhost', int(port))) + return result != 0 # Port ist frei wenn Verbindung fehlschlägt + except: + return False + + def find_available_port(self, start_port=8080, max_attempts=50): + """Finde einen verfügbaren Port beginnend ab start_port""" + for port in range(int(start_port), int(start_port) + max_attempts): + if self.check_port_availability(port): + return port + return None + + def check_container_health(self, container, port): + """Prüfe ob Container tatsächlich erreichbar ist""" + import requests + import time + + max_attempts = 10 + for attempt in range(max_attempts): + try: + # Prüfe Container-Status + container.reload() + if container.status != 'running': + return False, f"Container nicht am Laufen (Status: {container.status})" + + # Prüfe HTTP-Erreichbarkeit wenn Port definiert + if port: + try: + response = requests.get(f"http://localhost:{port}", timeout=2) + if response.status_code < 500: # Akzeptiere alle Antworten außer Server-Errors + return True, "Container erreichbar" + except requests.exceptions.RequestException: + pass # HTTP noch nicht bereit, aber das ist OK in den ersten Sekunden + + time.sleep(1) + except Exception as e: + return False, f"Health Check Fehler: {str(e)}" + + return True, "Container läuft (HTTP-Check übersprungen)" + + def analyze_app_files_for_ports(self, project_path): + """Analysiere App-Dateien um verwendete Ports zu finden""" + ports = [] + + # Typische Dateien durchsuchen + files_to_check = ['app.py', 'main.py', 'server.py', 'index.js', 'package.json'] + + for filename in files_to_check: + filepath = os.path.join(project_path, filename) + if os.path.exists(filepath): + try: + with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + # Suche nach Port-Definitionen + import re + + # Flask/Python Patterns + flask_patterns = [ + r'app\.run\([^)]*port\s*=\s*(\d+)', + r'host\s*=\s*["\'][^"\']*["\'],?\s*port\s*=\s*(\d+)', + r'port\s*=\s*(\d+)', + r'PORT\s*=\s*(\d+)', + ] + + # Node.js Patterns + node_patterns = [ + r'listen\s*\(\s*(\d+)', + r'port\s*[:=]\s*(\d+)', + r'process\.env\.PORT\s*\|\|\s*(\d+)', + ] + + all_patterns = flask_patterns + node_patterns + + for pattern in all_patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + for match in matches: + port_num = int(match) + if 1 <= port_num <= 65535: # Gültiger Port-Bereich + port_spec = f"{port_num}/tcp" + if port_spec not in ports: + ports.append(port_spec) + print(f"Found port {port_num} in {filename}") + + except Exception as e: + print(f"Warning: Could not analyze {filename}: {e}") + + return ports + + def analyze_dockerfile_ports(self, project_path): + """Analysiere Dockerfile um exponierte Ports zu finden""" + dockerfile_path = os.path.join(project_path, 'Dockerfile') + ports = [] + + if os.path.exists(dockerfile_path): + try: + with open(dockerfile_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip().upper() + if line.startswith('EXPOSE'): + # EXPOSE 8080 oder EXPOSE 8080/tcp + parts = line.split() + for part in parts[1:]: # Skip "EXPOSE" + if part.isdigit(): + ports.append(f"{part}/tcp") + elif '/' in part: + ports.append(part) + except Exception as e: + print(f"Warning: Could not analyze Dockerfile: {e}") + + return ports + + def detect_container_exposed_ports(self, image_name): + """Erkenne welche Ports das Container-Image exponiert""" + try: + if not self.docker_available: + return [] + + image = self.docker_client.images.get(f"{image_name}:latest") + config = image.attrs.get('Config', {}) + exposed_ports = config.get('ExposedPorts', {}) + + ports = [] + for port_spec in exposed_ports.keys(): + ports.append(port_spec) + + # Falls keine Ports im Image exponiert sind, analysiere Projekt-Dateien + if not ports: + print(f"Keine Ports im Image {image_name} exponiert, analysiere Projekt-Dateien...") + project_path = os.path.join(PROJECTS_DIR, image_name) + + # Analysiere Dockerfile und App-Dateien + dockerfile_ports = self.analyze_dockerfile_ports(project_path) + app_ports = self.analyze_app_files_for_ports(project_path) + + # Kombiniere gefundene Ports + all_found_ports = dockerfile_ports + app_ports + if all_found_ports: + ports = all_found_ports + print(f"Ports aus Projekt-Analyse gefunden: {ports}") + else: + # Fallback zu Standard-Ports + ports = ['5000/tcp', '8080/tcp', '80/tcp', '3000/tcp'] + print(f"Keine Ports gefunden, verwende Fallback: {ports}") + else: + print(f"Image {image_name} exponiert Ports: {ports}") + + return ports + + except Exception as e: + print(f"Warning: Could not detect exposed ports for {image_name}: {e}") + # Als letzter Fallback, analysiere Projekt-Dateien + try: + project_path = os.path.join(PROJECTS_DIR, image_name) + app_ports = self.analyze_app_files_for_ports(project_path) + if app_ports: + print(f"Fallback: Ports aus App-Dateien: {app_ports}") + return app_ports + except: + pass + + # Ultimativer Fallback + return ['5000/tcp', '8080/tcp', '80/tcp', '3000/tcp'] + + def create_smart_port_mapping(self, project_name, host_port): + """Erstelle intelligentes Port-Mapping basierend auf Image-Konfiguration""" + try: + exposed_ports = self.detect_container_exposed_ports(project_name) + + if not exposed_ports: + print(f"Keine Ports im Image {project_name} gefunden, verwende Fallback") + # Verwende nur einen Standard-Port + return {'8080/tcp': host_port} + + # Wenn nur ein Port exponiert ist, verwende diesen + if len(exposed_ports) == 1: + return {exposed_ports[0]: host_port} + + # Wenn mehrere Ports exponiert sind, priorisiere Standard-Web-Ports + web_ports = ['80/tcp', '8080/tcp', '3000/tcp', '5000/tcp'] + for web_port in web_ports: + if web_port in exposed_ports: + print(f"Verwende priorisierten Web-Port: {web_port} -> {host_port}") + return {web_port: host_port} + + # Falls kein Standard-Web-Port, verwende den ersten + first_port = exposed_ports[0] + print(f"Verwende ersten exponierten Port: {first_port} -> {host_port}") + return {first_port: host_port} + + except Exception as e: + print(f"Error creating port mapping: {e}") + # Fallback + return {'8080/tcp': host_port} + + def wait_for_container_ready(self, container, port=None, timeout=30): + """Warte bis Container bereit ist oder Timeout erreicht""" + import time + start_time = time.time() + + while time.time() - start_time < timeout: + try: + container.reload() + if container.status == 'running': + # Container läuft - mache zusätzlichen Health Check + if port: + health_ok, health_msg = self.check_container_health(container, port) + if health_ok: + return True, "Container läuft und ist erreichbar" + # Falls Health Check fehlschlägt, warte noch etwas + else: + return True, "Container läuft" + elif container.status in ['exited', 'dead']: + logs = container.logs(tail=20).decode('utf-8', errors='ignore') + return False, f"Container beendet mit Status '{container.status}'. Logs: {logs[:300]}" + + time.sleep(1) + except Exception as e: + return False, f"Fehler beim Prüfen des Container-Status: {str(e)}" + + # Timeout erreicht - prüfe finalen Status + try: + container.reload() + if container.status == 'running': + return True, f"Container läuft (Timeout nach {timeout}s erreicht, aber Container läuft)" + except: + pass + + return False, f"Timeout beim Warten auf Container-Start (>{timeout}s)" + + def start_project(self, project_name, port=None): + """Starte Projekt-Container""" + if not self.docker_available: + return False, "Docker ist nicht verfügbar. Bitte starten Sie Docker Desktop." + + try: + # Stoppe existierenden Container falls vorhanden + try: + existing = self.docker_client.containers.get(project_name) + print(f"Stopping existing container {project_name}") + existing.stop() + existing.remove() + except docker.errors.NotFound: + pass # Container existiert nicht, das ist OK + + # Prüfe ob Image existiert + try: + self.docker_client.images.get(f"{project_name}:latest") + except docker.errors.ImageNotFound: + return False, f"Image {project_name}:latest nicht gefunden. Bitte bauen Sie das Projekt zuerst." + + # Port-Verwaltung verbessert + if port: + port = int(port) + if not self.check_port_availability(port): + # Suche alternativen Port + alternative_port = self.find_available_port(port) + if alternative_port: + return False, f"Port {port} ist bereits belegt. Verfügbarer alternativer Port: {alternative_port}. Möchten Sie Port {alternative_port} verwenden?" + else: + return False, f"Port {port} ist bereits belegt und keine Alternative zwischen {port}-{port+50} gefunden." + else: + # Finde automatisch einen freien Port + port = self.find_available_port() + if not port: + return False, "Kein freier Port zwischen 8080-8130 gefunden." + + # Intelligentes Port-Mapping basierend auf Image-Konfiguration + ports = {} + if port: + ports = self.create_smart_port_mapping(project_name, port) + + # Umgebungsvariablen laden + env_vars = {} + project_path = os.path.join(PROJECTS_DIR, project_name) + env_file = os.path.join(project_path, '.env') + if os.path.exists(env_file): + try: + with open(env_file, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + env_vars[key.strip()] = value.strip() + except Exception as e: + print(f"Warning: Could not load .env file: {e}") + + print(f"Starting container {project_name} with ports: {ports}") + print(f"Environment variables: {env_vars}") + + # Erstelle Container (aber starte noch nicht) + container = self.docker_client.containers.create( + f"{project_name}:latest", + name=project_name, + ports=ports if ports else None, + environment=env_vars, + restart_policy={"Name": "unless-stopped"}, + detach=True + ) + + print(f"Container {project_name} created with ID: {container.id}") + + # Starte Container explizit + container.start() + print(f"Container {project_name} started") + + # Warte auf Container-Bereitschaft mit Timeout + success, status_msg = self.wait_for_container_ready(container, port, timeout=30) + + if success: + return True, f"Container {project_name} erfolgreich gestartet auf Port {port}. Zugriff über: http://localhost:{port}" + else: + # Container-Start fehlgeschlagen - cleanup + try: + container.stop(timeout=5) + container.remove() + except Exception as cleanup_error: + print(f"Cleanup error: {cleanup_error}") + + return False, f"Container-Start fehlgeschlagen: {status_msg}" + + except docker.errors.ContainerError as e: + return False, f"Container-Laufzeitfehler: {str(e)}" + + except docker.errors.ImageNotFound as e: + return False, f"Image nicht gefunden: {str(e)}. Bitte bauen Sie das Projekt zuerst." + + except docker.errors.APIError as e: + error_msg = str(e) + if "port is already allocated" in error_msg: + # Extrahiere Port aus Fehlermeldung + import re + port_match = re.search(r':(\d+)', error_msg) + if port_match: + blocked_port = port_match.group(1) + # Suche alternativen Port + alternative_port = self.find_available_port(int(blocked_port) + 1) + if alternative_port: + return False, f"Port {blocked_port} ist bereits belegt. Alternativer freier Port: {alternative_port}" + else: + return False, f"Port {blocked_port} ist bereits belegt und keine Alternative gefunden." + else: + return False, f"Port-Konflikt: {error_msg}" + else: + return False, f"Docker-API-Fehler: {error_msg}" + + except Exception as e: + return False, f"Unerwarteter Fehler beim Starten: {str(e)}" + + def stop_project(self, project_name): + """Stoppe Projekt-Container""" + try: + if self.docker_client: + container = self.docker_client.containers.get(project_name) + container.stop() + return True, f"Container {project_name} gestoppt" + else: + return False, "Docker Client nicht verfügbar" + except Exception as e: + return False, f"Fehler beim Stoppen: {str(e)}" + + def force_remove_directory(self, path): + """Entferne Verzeichnis robust unter Windows (behandelt schreibgeschützte Dateien)""" + import stat + import shutil + + def handle_remove_readonly(func, path, exc): + """Behandle schreibgeschützte Dateien unter Windows""" + try: + # Entferne readonly-Attribut und versuche erneut + os.chmod(path, stat.S_IWRITE) + func(path) + except Exception as e: + print(f"Warning: Could not remove {path}: {e}") + + try: + if os.path.exists(path): + print(f"Removing directory: {path}") + shutil.rmtree(path, onerror=handle_remove_readonly) + print(f"Successfully removed: {path}") + return True + except Exception as e: + print(f"Error removing directory {path}: {e}") + # Fallback: Versuche mit Windows-Kommandos + try: + import subprocess + # Windows rmdir mit Force-Parameter + result = subprocess.run(['cmd', '/c', 'rmdir', '/s', '/q', path], + capture_output=True, text=True, timeout=30) + if result.returncode == 0: + print(f"Successfully removed with Windows rmdir: {path}") + return True + else: + print(f"Windows rmdir failed: {result.stderr}") + except Exception as fallback_error: + print(f"Fallback removal failed: {fallback_error}") + + return False + + return True + + def remove_project(self, project_name): + """Entferne Projekt komplett""" + try: + print(f"Starting removal of project: {project_name}") + + # Stoppe und entferne Container + if self.docker_client: + try: + container = self.docker_client.containers.get(project_name) + print(f"Stopping container {project_name}") + container.stop(timeout=10) + container.remove() + print(f"Container {project_name} removed") + except docker.errors.NotFound: + print(f"Container {project_name} not found (already removed)") + except Exception as container_error: + print(f"Warning: Container removal failed: {container_error}") + + # Entferne Image + try: + print(f"Removing image {project_name}:latest") + self.docker_client.images.remove(f"{project_name}:latest", force=True) + print(f"Image {project_name}:latest removed") + except docker.errors.ImageNotFound: + print(f"Image {project_name}:latest not found (already removed)") + except Exception as image_error: + print(f"Warning: Image removal failed: {image_error}") + + # Entferne Projektverzeichnis mit robuster Methode + project_path = os.path.join(PROJECTS_DIR, project_name) + if os.path.exists(project_path): + print(f"Removing project directory: {project_path}") + + # Versuche zuerst normale Entfernung + success = self.force_remove_directory(project_path) + + if not success: + return False, f"Konnte Projektverzeichnis nicht vollständig entfernen. Versuchen Sie es als Administrator oder entfernen Sie {project_path} manuell." + + # Prüfe ob Verzeichnis wirklich entfernt wurde + if os.path.exists(project_path): + return False, f"Projektverzeichnis {project_path} konnte nicht vollständig entfernt werden. Bitte manuell löschen." + + print(f"Project {project_name} successfully removed") + return True, f"Projekt {project_name} vollständig entfernt" + + except Exception as e: + error_msg = f"Fehler beim Entfernen: {str(e)}" + print(f"Remove project error: {error_msg}") + + # Gebe hilfreiche Tipps für Windows-spezifische Probleme + if "Zugriff verweigert" in str(e) or "Access is denied" in str(e): + error_msg += "\n\nTipp: Versuchen Sie:\n1. Alle Git-Clients schließen\n2. Als Administrator ausführen\n3. Antivirus temporär deaktivieren\n4. Manuell löschen: " + os.path.join(PROJECTS_DIR, project_name) + + return False, error_msg + +# Globale Instanz +project_manager = ProjectManager() + +@app.route('/') +def index(): + """Hauptseite mit Projektübersicht""" + config = project_manager.load_config() + + # Lade lokale Projekte + projects = [] + if os.path.exists(PROJECTS_DIR): + for project_name in os.listdir(PROJECTS_DIR): + project_path = os.path.join(PROJECTS_DIR, project_name) + if os.path.isdir(project_path): + info = project_manager.get_project_info(project_name) + if info: + projects.append(info) + + return render_template('index.html', projects=projects, config=config) + +@app.route('/refresh_projects') +def refresh_projects(): + """Aktualisiere Projektliste von URL""" + config = project_manager.load_config() + + if not config.get('project_list_url'): + flash('Keine Projekt-URL konfiguriert', 'error') + return redirect(url_for('index')) + + projects = project_manager.fetch_project_list(config['project_list_url']) + config['projects'] = projects + project_manager.save_config(config) + + flash(f'{len(projects)} Projekte gefunden und aktualisiert', 'success') + return redirect(url_for('index')) + +@app.route('/available_projects') +def available_projects(): + """Zeige verfügbare Projekte zum Installieren""" + config = project_manager.load_config() + return render_template('available_projects.html', projects=config.get('projects', [])) + +@app.route('/install_project', methods=['POST']) +def install_project(): + """Installiere Projekt""" + project_url = request.form.get('project_url') + project_name = request.form.get('project_name') + + if not project_url or not project_name: + return jsonify({'success': False, 'message': 'URL und Name erforderlich'}) + + # Klone Projekt + success, message = project_manager.clone_project(project_url, project_name) + + if success: + # Versuche automatisch zu bauen + build_success, build_message = project_manager.build_project(project_name) + if build_success: + message += f" und gebaut: {build_message}" + else: + message += f" (Build fehlgeschlagen: {build_message})" + + return jsonify({'success': success, 'message': message}) + +@app.route('/build_project/') +def build_project(project_name): + """Baue Projekt""" + success, message = project_manager.build_project(project_name) + flash(message, 'success' if success else 'error') + return redirect(url_for('index')) + +@app.route('/start_project/') +def start_project(project_name): + """Starte Projekt""" + port = request.args.get('port', None) + + # Wenn kein Port angegeben, finde einen freien + if not port: + available_port = project_manager.find_available_port() + if available_port: + port = available_port + else: + flash('Kein freier Port verfügbar', 'error') + return redirect(url_for('project_details', project_name=project_name)) + + success, message = project_manager.start_project(project_name, port) + + # Überprüfe ob es ein API-Request ist (AJAX) + if request.headers.get('Accept') == 'application/json': + return jsonify({'success': success, 'message': message, 'port': port}) + + flash(message, 'success' if success else 'error') + + # Umleitung basierend auf Erfolg + if success: + return redirect(url_for('project_details', project_name=project_name)) + else: + return redirect(url_for('project_details', project_name=project_name)) + +@app.route('/stop_project/') +def stop_project(project_name): + """Stoppe Projekt""" + success, message = project_manager.stop_project(project_name) + flash(message, 'success' if success else 'error') + return redirect(url_for('index')) + +@app.route('/remove_project/') +def remove_project(project_name): + """Entferne Projekt""" + success, message = project_manager.remove_project(project_name) + flash(message, 'success' if success else 'error') + return redirect(url_for('index')) + +@app.route('/config', methods=['GET', 'POST']) +def config(): + """Konfigurationsseite""" + if request.method == 'POST': + config = { + 'project_list_url': request.form.get('project_list_url', ''), + 'auto_refresh_minutes': int(request.form.get('auto_refresh_minutes', 30)), + 'docker_registry': request.form.get('docker_registry', ''), + 'projects': project_manager.load_config().get('projects', []) + } + + project_manager.save_config(config) + flash('Konfiguration gespeichert', 'success') + return redirect(url_for('config')) + + config = project_manager.load_config() + return render_template('config.html', config=config) + +@app.route('/project_details/') +def project_details(project_name): + """Projektdetails und Konfiguration""" + info = project_manager.get_project_info(project_name) + if not info: + flash('Projekt nicht gefunden', 'error') + return redirect(url_for('index')) + + # Lade .env Datei falls vorhanden + env_content = '' + env_path = os.path.join(info['path'], '.env') + if os.path.exists(env_path): + with open(env_path, 'r', encoding='utf-8') as f: + env_content = f.read() + elif info['has_env_example']: + env_example_path = os.path.join(info['path'], '.env.example') + with open(env_example_path, 'r', encoding='utf-8') as f: + env_content = f.read() + + return render_template('project_details.html', project=info, env_content=env_content) + +@app.route('/save_env/', methods=['POST']) +def save_env(project_name): + """Speichere .env Konfiguration""" + env_content = request.form.get('env_content', '') + project_path = os.path.join(PROJECTS_DIR, project_name) + env_path = os.path.join(project_path, '.env') + + try: + with open(env_path, 'w', encoding='utf-8') as f: + f.write(env_content) + flash('.env Datei gespeichert', 'success') + except Exception as e: + flash(f'Fehler beim Speichern: {str(e)}', 'error') + + return redirect(url_for('project_details', project_name=project_name)) + +@app.route('/api/system_status') +def api_system_status(): + """API Endpoint für Systemstatus""" + status = { + 'docker': {'available': False, 'version': None, 'status': 'checking'}, + 'git': {'available': False, 'version': None, 'status': 'checking'}, + 'project_dir': {'available': os.path.exists(PROJECTS_DIR), 'status': 'ok'}, + 'disk_space': {'available': True, 'free': 'Unknown', 'status': 'ok'} + } + + # Docker Status + try: + result = subprocess.run(['docker', '--version'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + version = result.stdout.strip() + + # Teste Docker-Daemon + try: + subprocess.run(['docker', 'ps'], capture_output=True, text=True, timeout=5, check=True) + status['docker'] = {'available': True, 'version': version, 'status': 'running'} + except subprocess.CalledProcessError: + status['docker'] = {'available': True, 'version': version, 'status': 'daemon_stopped'} + else: + status['docker'] = {'available': False, 'version': None, 'status': 'not_installed'} + except subprocess.TimeoutExpired: + status['docker'] = {'available': False, 'version': None, 'status': 'timeout'} + except FileNotFoundError: + status['docker'] = {'available': False, 'version': None, 'status': 'not_found'} + except Exception as e: + status['docker'] = {'available': False, 'version': None, 'status': f'error: {str(e)}'} + + # Git Status + try: + result = subprocess.run(['git', '--version'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + status['git'] = {'available': True, 'version': result.stdout.strip(), 'status': 'ok'} + else: + status['git'] = {'available': False, 'version': None, 'status': 'error'} + except FileNotFoundError: + status['git'] = {'available': False, 'version': None, 'status': 'not_found'} + except Exception as e: + status['git'] = {'available': False, 'version': None, 'status': f'error: {str(e)}'} + + return jsonify(status) + +@app.route('/api/test_connection', methods=['POST']) +def api_test_connection(): + """Test Verbindung zu Projektliste URL""" + data = request.get_json() + url = data.get('url', '') + + if not url: + return jsonify({'success': False, 'error': 'Keine URL angegeben'}) + + try: + projects = project_manager.fetch_project_list(url) + return jsonify({ + 'success': True, + 'projects_found': len(projects), + 'projects': projects[:5] # Erste 5 als Beispiel + }) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/api/container_logs/') +def api_container_logs(project_name): + """Hole Container-Logs für Debugging""" + try: + if not project_manager.docker_available: + return jsonify({'success': False, 'error': 'Docker nicht verfügbar'}) + + container = project_manager.docker_client.containers.get(project_name) + logs = container.logs(tail=100).decode('utf-8', errors='ignore') + + return jsonify({ + 'success': True, + 'logs': logs, + 'status': container.status, + 'container_id': container.id + }) + + except docker.errors.NotFound: + return jsonify({'success': False, 'error': 'Container nicht gefunden'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/api/container_inspect/') +def api_container_inspect(project_name): + """Detaillierte Container-Informationen für Debugging""" + try: + if not project_manager.docker_available: + return jsonify({'success': False, 'error': 'Docker nicht verfügbar'}) + + container = project_manager.docker_client.containers.get(project_name) + + # Hole relevante Container-Details + info = { + 'id': container.id, + 'status': container.status, + 'image': container.image.tags[0] if container.image.tags else 'unknown', + 'ports': container.ports, + 'environment': container.attrs.get('Config', {}).get('Env', []), + 'restart_policy': container.attrs.get('HostConfig', {}).get('RestartPolicy', {}), + 'exit_code': container.attrs.get('State', {}).get('ExitCode'), + 'error': container.attrs.get('State', {}).get('Error'), + 'started_at': container.attrs.get('State', {}).get('StartedAt'), + 'finished_at': container.attrs.get('State', {}).get('FinishedAt') + } + + return jsonify({ + 'success': True, + 'container_info': info + }) + + except docker.errors.NotFound: + return jsonify({'success': False, 'error': 'Container nicht gefunden'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/api/container_stats/') +def api_container_stats(project_name): + """Container Statistiken abrufen""" + try: + if project_manager.docker_client: + container = project_manager.docker_client.containers.get(project_name) + stats = container.stats(stream=False) + + # Vereinfachte Statistiken + cpu_percent = 0 + memory_mb = 0 + + if 'cpu_stats' in stats and 'precpu_stats' in stats: + cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - stats['precpu_stats']['cpu_usage']['total_usage'] + system_delta = stats['cpu_stats']['system_cpu_usage'] - stats['precpu_stats']['system_cpu_usage'] + if system_delta > 0: + cpu_percent = (cpu_delta / system_delta) * 100.0 + + if 'memory_stats' in stats: + memory_mb = stats['memory_stats'].get('usage', 0) / 1024 / 1024 + + return jsonify({ + 'success': True, + 'stats': { + 'cpu': f"{cpu_percent:.1f}", + 'memory': f"{memory_mb:.0f}", + 'network_in': 'N/A', + 'network_out': 'N/A' + } + }) + else: + return jsonify({'success': False, 'error': 'Docker Client nicht verfügbar'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/api/restart_project/', methods=['POST']) +def api_restart_project(project_name): + """Projekt Container neustarten""" + try: + if project_manager.docker_client: + container = project_manager.docker_client.containers.get(project_name) + container.restart() + return jsonify({'success': True, 'message': f'Container {project_name} neu gestartet'}) + else: + return jsonify({'success': False, 'error': 'Docker Client nicht verfügbar'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/api/clear_cache', methods=['POST']) +def api_clear_cache(): + """Cache leeren""" + try: + # Hier könnte Cache-Logik implementiert werden + return jsonify({'success': True, 'message': 'Cache geleert'}) + except Exception as e: + return jsonify({'success': False, 'message': f'Fehler: {str(e)}'}) + +@app.route('/api/export_config') +def api_export_config(): + """Konfiguration exportieren""" + try: + config = project_manager.load_config() + response = app.response_class( + response=json.dumps(config, indent=2, ensure_ascii=False), + status=200, + mimetype='application/json', + headers={'Content-Disposition': 'attachment; filename=app_installer_config.json'} + ) + return response + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/api/import_config', methods=['POST']) +def api_import_config(): + """Konfiguration importieren""" + try: + config = request.get_json() + project_manager.save_config(config) + return jsonify({'success': True, 'message': 'Konfiguration importiert'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/api/reset_config', methods=['POST']) +def api_reset_config(): + """Konfiguration zurücksetzen""" + try: + default_config = { + 'project_list_url': '', + 'auto_refresh_minutes': 30, + 'docker_registry': '', + 'projects': [] + } + project_manager.save_config(default_config) + return jsonify({'success': True, 'message': 'Konfiguration zurückgesetzt'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/docker_status') +def docker_status(): + """Docker Status und Diagnose Seite""" + return render_template('docker_status.html') + +@app.route('/api/docker_diagnose') +def api_docker_diagnose(): + """Docker Diagnose API""" + try: + # Importiere Diagnose-Funktion + from docker_diagnose import diagnose_docker_status + + results = { + 'timestamp': datetime.now().isoformat(), + 'checks': {}, + 'recommendations': [] + } + + # Docker Version + try: + result = subprocess.run(['docker', '--version'], capture_output=True, text=True, timeout=10) + if result.returncode == 0: + results['checks']['docker_version'] = { + 'success': True, + 'output': result.stdout.strip(), + 'details': 'Docker ist installiert' + } + else: + results['checks']['docker_version'] = { + 'success': False, + 'output': result.stderr.strip(), + 'details': 'Docker Kommando fehlgeschlagen' + } + results['recommendations'].append('Docker Desktop installieren') + except FileNotFoundError: + results['checks']['docker_version'] = { + 'success': False, + 'output': 'Docker nicht gefunden', + 'details': 'Docker ist nicht installiert' + } + results['recommendations'].append('Docker Desktop von https://docker.com herunterladen') + + # Docker Daemon + if project_manager.docker_available: + try: + project_manager.docker_client.ping() + results['checks']['docker_daemon'] = { + 'success': True, + 'output': 'Docker Daemon läuft', + 'details': 'Verbindung erfolgreich' + } + except Exception as e: + results['checks']['docker_daemon'] = { + 'success': False, + 'output': 'Daemon nicht erreichbar', + 'details': str(e) + } + results['recommendations'].append('Docker Desktop starten') + else: + results['checks']['docker_daemon'] = { + 'success': False, + 'output': 'Daemon nicht verfügbar', + 'details': 'Docker Client nicht initialisiert' + } + results['recommendations'].append('Docker Desktop starten') + + # Container Status + if project_manager.docker_available: + try: + containers = project_manager.docker_client.containers.list(all=True) + results['checks']['containers'] = { + 'success': True, + 'output': f'{len(containers)} Container', + 'details': f'Gefunden: {len(containers)} Container', + 'containers': [{'name': c.name, 'status': c.status} for c in containers] + } + except Exception as e: + results['checks']['containers'] = { + 'success': False, + 'output': 'Container-Check fehlgeschlagen', + 'details': str(e) + } + + # Images + if project_manager.docker_available: + try: + images = project_manager.docker_client.images.list() + results['checks']['images'] = { + 'success': True, + 'output': f'{len(images)} Images', + 'details': f'Verfügbare Images: {len(images)}' + } + except Exception as e: + results['checks']['images'] = { + 'success': False, + 'output': 'Image-Check fehlgeschlagen', + 'details': str(e) + } + + return jsonify(results) + + except Exception as e: + return jsonify({ + 'error': str(e), + 'timestamp': datetime.now().isoformat() + }), 500 + +@app.route('/api/check_port/') +def api_check_port(port): + """Prüfe ob ein Port verfügbar ist""" + try: + available = project_manager.check_port_availability(port) + return jsonify({ + 'success': True, + 'port': port, + 'available': available, + 'message': f'Port {port} ist {"verfügbar" if available else "belegt"}' + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }) + +@app.route('/api/find_available_port') +def api_find_available_port(): + """Finde einen verfügbaren Port""" + try: + start_port = request.args.get('start', 8080, type=int) + available_port = project_manager.find_available_port(start_port) + + if available_port: + return jsonify({ + 'success': True, + 'port': available_port, + 'message': f'Port {available_port} ist verfügbar' + }) + else: + return jsonify({ + 'success': False, + 'error': f'Kein freier Port ab {start_port} gefunden' + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }) + +@app.route('/api/start_project/', methods=['POST']) +def api_start_project(project_name): + """API Endpoint zum Starten eines Projekts""" + try: + data = request.get_json() or {} + port = data.get('port') or request.args.get('port') + + # Wenn kein Port angegeben, finde einen freien + if not port: + available_port = project_manager.find_available_port() + if available_port: + port = available_port + else: + return jsonify({ + 'success': False, + 'error': 'Kein freier Port zwischen 8080-8130 verfügbar' + }) + + success, message = project_manager.start_project(project_name, port) + + # Konsistente API-Antwort + response = { + 'success': success, + 'project_name': project_name + } + + if success: + response['message'] = message + response['port'] = port + else: + response['error'] = message + + # Bei Port-Konflikten, versuche Alternative zu finden + if 'bereits belegt' in message or 'port is already allocated' in message: + # Extrahiere Port aus Nachricht und finde Alternative + import re + port_match = re.search(r'(\d+)', message) + if port_match: + blocked_port = int(port_match.group(1)) + alternative_port = project_manager.find_available_port(blocked_port + 1) + if alternative_port: + response['alternative_port'] = alternative_port + response['error'] = f"Port {blocked_port} ist belegt. Alternativer Port: {alternative_port}" + + return jsonify(response) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'Unerwarteter Fehler: {str(e)}' + }) + +@app.route('/api/project_status/') +def api_project_status(project_name): + """Hole aktuellen Projektstatus""" + try: + info = project_manager.get_project_info(project_name) + if info: + return jsonify({ + 'success': True, + 'status': info['status'], + 'name': info['name'], + 'has_dockerfile': info['has_dockerfile'], + 'has_env_example': info['has_env_example'] + }) + else: + return jsonify({'success': False, 'error': 'Projekt nicht gefunden'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/api/analyze_ports/') +def api_analyze_ports(project_name): + """Analysiere Port-Konfiguration eines Projekts""" + try: + project_path = os.path.join(PROJECTS_DIR, project_name) + + result = { + 'project_name': project_name, + 'dockerfile_ports': [], + 'app_file_ports': [], + 'image_ports': [], + 'recommended_mapping': None, + 'analysis': [] + } + + # Analysiere Dockerfile + if os.path.exists(os.path.join(project_path, 'Dockerfile')): + dockerfile_ports = project_manager.analyze_dockerfile_ports(project_path) + result['dockerfile_ports'] = dockerfile_ports + result['analysis'].append(f"Dockerfile definiert Ports: {', '.join(dockerfile_ports) if dockerfile_ports else 'keine'}") + else: + result['analysis'].append("Kein Dockerfile gefunden") + + # Analysiere App-Dateien + app_ports = project_manager.analyze_app_files_for_ports(project_path) + result['app_file_ports'] = app_ports + result['analysis'].append(f"App-Dateien verwenden Ports: {', '.join(app_ports) if app_ports else 'keine gefunden'}") + + # Analysiere Image (falls vorhanden) + if project_manager.docker_available: + try: + image_ports = project_manager.detect_container_exposed_ports(project_name) + result['image_ports'] = image_ports + result['analysis'].append(f"Image exponiert Ports: {', '.join(image_ports) if image_ports else 'keine'}") + + # Empfohlenes Mapping + if image_ports: + recommended_port = 8080 + mapping = project_manager.create_smart_port_mapping(project_name, recommended_port) + result['recommended_mapping'] = mapping + result['analysis'].append(f"Empfohlenes Port-Mapping: {mapping}") + + except Exception as e: + result['analysis'].append(f"Image-Analyse fehlgeschlagen: {str(e)}") + else: + result['analysis'].append("Docker nicht verfügbar für Image-Analyse") + + return jsonify({ + 'success': True, + 'port_analysis': result + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }) + +@app.route('/api/remove_project/', methods=['POST']) +def api_remove_project(project_name): + """API Endpoint zum Entfernen eines Projekts mit detailliertem Feedback""" + try: + success, message = project_manager.remove_project(project_name) + + return jsonify({ + 'success': success, + 'message': message, + 'project_name': project_name + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'Unerwarteter Fehler beim Entfernen: {str(e)}', + 'project_name': project_name + }) + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..4cb1234 --- /dev/null +++ b/config.example.json @@ -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"] + } + ] +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..4cb1234 --- /dev/null +++ b/config.json @@ -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"] + } + ] +} diff --git a/docker_diagnose.py b/docker_diagnose.py new file mode 100644 index 0000000..575f2db --- /dev/null +++ b/docker_diagnose.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8f5b356 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..17c9702 --- /dev/null +++ b/start.bat @@ -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 diff --git a/templates/available_projects.html b/templates/available_projects.html new file mode 100644 index 0000000..310dcba --- /dev/null +++ b/templates/available_projects.html @@ -0,0 +1,310 @@ +{% extends "base.html" %} + +{% block title %}Verfügbare Apps - {{ super() }}{% endblock %} + +{% block content %} +
+

Verfügbare Apps

+ + Liste aktualisieren + +
+ +{% if not projects %} +
+ +

Keine Projekte verfügbar

+

+ Stellen Sie sicher, dass eine gültige Projekt-URL in den + Einstellungen konfiguriert ist. +

+ + Einstellungen öffnen + +
+{% else %} +
+ {% for project in projects %} +
+
+
+
+ {{ project.name or 'Unbekannt' }} +
+
+
+
+ Repository URL: +

{{ project.url }}

+
+ + {% if project.description %} +
+ Beschreibung: +

{{ project.description }}

+
+ {% endif %} + + {% if project.tags %} +
+ {% for tag in project.tags %} + {{ tag }} + {% endfor %} +
+ {% endif %} + +
+
+ Sprache:
+ {{ project.language or 'Unbekannt' }} +
+
+ Größe:
+ {{ project.size or 'Unbekannt' }} +
+
+ + {% if project.last_updated %} +
+ Letztes Update: {{ project.last_updated }} +
+ {% endif %} + +
+
+ + + Repository anzeigen + +
+
+
+
+
+ {% endfor %} +
+ + +
+
+
+
+
+ Installationsstatistiken +
+
+
+
+
+
+ {{ projects|length }} + Verfügbare Apps +
+
+
+
+ {{ projects|selectattr('language', 'defined')|list|length }} + Mit Sprache +
+
+
+
+ {{ projects|selectattr('description', 'defined')|list|length }} + Mit Beschreibung +
+
+
+
+ {{ projects|selectattr('tags', 'defined')|list|length }} + Mit Tags +
+
+
+
+
+
+
+{% endif %} + + + + + +
+
+
+
+
+ Filter & Sortierung +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..4cc1b61 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,223 @@ + + + + + + {% block title %}App Installer & Manager{% endblock %} + + + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} + + {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + + + + {% block scripts %}{% endblock %} + + diff --git a/templates/config.html b/templates/config.html new file mode 100644 index 0000000..8a8c837 --- /dev/null +++ b/templates/config.html @@ -0,0 +1,392 @@ +{% extends "base.html" %} + +{% block title %}Konfiguration - {{ super() }}{% endblock %} + +{% block content %} +
+
+
+
+
+ Systemkonfiguration +
+
+
+
+
+ + +
+ URL zu einer JSON-Datei mit Projektinformationen oder einer Webseite mit Git-URLs. +
JSON Format: [{"url": "https://git.example.com/repo", "name": "Project Name", "description": "..."}] +
+
+ +
+ + +
+ Wie oft soll die Projektliste automatisch aktualisiert werden? (5-1440 Minuten) +
+
+ +
+ + +
+ Private Docker Registry für das Pushen von Images (optional). +
+
+ +
+ + +
+
+
+
+
+ +
+ +
+
+
+ Systemstatus +
+
+
+
+
+ Docker: + Prüfung... +
+
+
+
+ Git: + Prüfung... +
+
+
+
+ Projektverzeichnis: + Prüfung... +
+
+
+
+ Festplattenspeicher: + Prüfung... +
+
+
+
+ + +
+
+
+ Erweiterte Einstellungen +
+
+
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
+
+
+ + +{% if config.projects %} +
+
+
+
+
+ Aktuelle Projektliste ({{ config.projects|length }} Projekte) +
+ Letzte Aktualisierung vor {{ last_update or 'unbekannt' }} +
+
+
+ + + + + + + + + + + {% for project in config.projects[:10] %} + + + + + + + {% endfor %} + +
NameURLBeschreibungStatus
{{ project.name or 'Unbekannt' }} + + {{ project.url[:50] }}{% if project.url|length > 50 %}...{% endif %} + + {{ (project.description or 'Keine Beschreibung')[:60] }}{% if (project.description or '')|length > 60 %}...{% endif %} + {% if project.name in installed_projects %} + Installiert + {% else %} + Verfügbar + {% endif %} +
+ {% if config.projects|length > 10 %} +

... und {{ config.projects|length - 10 }} weitere Projekte

+ {% endif %} +
+
+
+
+
+{% endif %} + + +
+
+
+
+
+ Debug-Informationen +
+
+
+
+
+
Systeminfo:
+
    +
  • Arbeitsverzeichnis: Lade...
  • +
  • Projektverzeichnis: ./projects/
  • +
  • App-Verzeichnis: ./apps/
  • +
  • Konfigurationsdatei: ./config.json
  • +
+
+
+
Laufzeit-Info:
+
    +
  • Flask Debug: An
  • +
  • Host: 0.0.0.0:5000
  • +
  • Uptime: Lade...
  • +
  • Speicherverbrauch: Lade...
  • +
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/docker_status.html b/templates/docker_status.html new file mode 100644 index 0000000..c93a9b8 --- /dev/null +++ b/templates/docker_status.html @@ -0,0 +1,425 @@ +{% extends "base.html" %} + +{% block title %}Docker Status - {{ super() }}{% endblock %} + +{% block content %} +
+
+
+

Docker Status & Diagnose

+ +
+
+
+ + +
+
+
+
+
+ +
Prüfung...
+
Docker Status
+
+
+
+
+
+
+
+
+ +
Prüfung...
+
Daemon Status
+
+
+
+
+
+
+
+
+ +
-
+
Container
+
+
+
+
+
+
+
+
+ +
-
+
Images
+
+
+
+
+
+ + +
+
+
+
+
+ Diagnose Ergebnisse +
+ Noch nicht ausgeführt +
+
+
+
+ +

Klicken Sie auf "Diagnose ausführen" um Docker zu überprüfen.

+
+
+
+
+ + +
+
+
+ Docker System Logs +
+
+
+
+
+ Docker Logs werden geladen... +
+
+
+ + +
+
+
+
+ +
+ +
+
+
+ Docker Aktionen +
+
+
+
+ + + + +
+
+
+ + +
+
+
+ Systemanforderungen +
+
+
+
+
+ + Windows 10/11 (64-bit) +
+
+ + Hyper-V oder WSL2 +
+
+ + 4GB+ RAM empfohlen +
+
+ + 20GB+ freier Speicher +
+
+
+
+ + +
+
+
+ Hilfe & Ressourcen +
+
+ +
+
+
+ + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..fd29167 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,220 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+ {{ projects|length }} + Installierte Apps +
+
+
+
+ {{ projects|selectattr('status', 'equalto', 'running')|list|length }} + Laufende Apps +
+
+
+
+ {{ config.projects|length if config.projects else 0 }} + Verfügbare Apps +
+
+
+
+ {{ projects|selectattr('has_dockerfile', 'equalto', true)|list|length }} + Docker Apps +
+
+
+
+ +{% if not projects %} +
+ +

Noch keine Apps installiert

+

Beginnen Sie mit der Installation Ihrer ersten App!

+ + Verfügbare Apps anzeigen + +
+{% else %} +
+ {% for project in projects %} +
+
+
+
+ {{ project.name }} +
+ + + {% if project.status == 'running' %}Läuft{% elif project.status in ['exited', 'stopped'] %}Gestoppt{% else %}Unbekannt{% endif %} + +
+
+
+
+ Docker:
+ {% if project.has_dockerfile %} + Verfügbar + {% else %} + Nicht verfügbar + {% endif %} +
+
+ Konfiguration:
+ {% if project.has_env_example %} + .env vorhanden + {% else %} + Keine .env + {% endif %} +
+
+ + {% if project.readme %} +
+ Beschreibung: +

{{ project.readme[:150] }}{% if project.readme|length > 150 %}...{% endif %}

+
+ {% endif %} + + Installiert: {{ project.created }} +
+ +
+
+ {% endfor %} +
+{% endif %} + + +
+
+
+
+
+ Schnellaktionen +
+
+ +
+
+
+ + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/project_details.html b/templates/project_details.html new file mode 100644 index 0000000..fc2c4a7 --- /dev/null +++ b/templates/project_details.html @@ -0,0 +1,906 @@ +{% extends "base.html" %} + +{% block title %}{{ project.name }} - Details{% endblock %} + +{% block content %} +
+

+ {{ project.name }} + + + {% if project.status == 'running' %}Läuft{% elif project.status in ['exited', 'stopped'] %}Gestoppt{% else %}Unbekannt{% endif %} + +

+ + Zurück + +
+ +
+
+ +
+
+
+ Projektinformationen +
+
+
+
+
+ Name: {{ project.name }} +
+
+ Pfad: {{ project.path }} +
+
+
+
+ Docker verfügbar: + {% if project.has_dockerfile %} + Ja + {% else %} + Nein + {% endif %} +
+
+ Umgebungskonfiguration: + {% if project.has_env_example %} + .env.example vorhanden + {% else %} + Keine .env.example + {% endif %} +
+
+
+
+ Docker Compose: + {% if project.has_docker_compose %} + Verfügbar + {% else %} + Nicht verfügbar + {% endif %} +
+
+ Installiert: {{ project.created }} +
+
+ + {% if project.readme %} +
+
README:
+
+
{{ project.readme }}
+
+
+ {% endif %} +
+
+ + +
+
+
+ Umgebungskonfiguration (.env) +
+
+ + +
+
+
+
+
+ +
+ Konfigurieren Sie hier die Umgebungsvariablen für Ihr Projekt. + Diese werden in die .env Datei gespeichert. +
+
+
+ + +
+
+
+
+ + +
+
+
+ Container Logs +
+ +
+
+
+
+ Logs werden geladen... +
+
+
+
+
+ +
+ +
+
+
+ Container-Steuerung +
+
+
+
+ {% if project.status == 'running' %} + + Container stoppen + + + {% else %} + + + {% endif %} + + + Image neu bauen + + + +
+
+
+ + + {% if project.status == 'running' %} +
+
+
+ Aktive Verbindungen +
+
+
+
+
+ HTTP: + + :8080 + +
+
+
+ + Klicken Sie auf die Links um die Anwendung zu öffnen. + +
+
+
+ {% endif %} + + + {% if project.status == 'running' %} +
+
+
+ Monitoring +
+
+
+
+
+
+ - + CPU % +
+
+
+
+ - + RAM MB +
+
+
+
+
+
+ - + Net In +
+
+
+
+ - + Net Out +
+
+
+
+ +
+
+
+ {% endif %} + + +
+
+
+ Hilfe & Tipps +
+
+
+
+ {% if not project.has_dockerfile %} +
+ Kein Dockerfile: Erstellen Sie ein Dockerfile in Ihrem Projektverzeichnis. +
+ {% endif %} + + {% if not project.has_env_example %} +
+ Tipp: Erstellen Sie eine .env.example Datei für Umgebungsvariablen. +
+ {% endif %} + +
    +
  • Bauen Sie das Image vor dem ersten Start
  • +
  • Überprüfen Sie die .env Konfiguration
  • +
  • Beachten Sie die Container-Logs bei Problemen
  • +
+
+
+
+ + +
+
+
+ Debug-Informationen +
+
+
+
+ + + +
+ +
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/project_details_fixed.html b/templates/project_details_fixed.html new file mode 100644 index 0000000..f4b566b --- /dev/null +++ b/templates/project_details_fixed.html @@ -0,0 +1,696 @@ +{% extends "base.html" %} + +{% block title %}{{ project.name }} - Details{% endblock %} + +{% block content %} +
+

+ {{ project.name }} + + + {% if project.status == 'running' %}Läuft{% elif project.status in ['exited', 'stopped'] %}Gestoppt{% else %}Unbekannt{% endif %} + +

+ + Zurück + +
+ +
+
+ +
+
+
+ Projektinformationen +
+
+
+
+
+ Name: {{ project.name }} +
+
+ Pfad: {{ project.path }} +
+
+
+
+ Docker verfügbar: + {% if project.has_dockerfile %} + Ja + {% else %} + Nein + {% endif %} +
+
+ Umgebungskonfiguration: + {% if project.has_env_example %} + .env.example vorhanden + {% else %} + Keine .env.example + {% endif %} +
+
+
+
+ Docker Compose: + {% if project.has_docker_compose %} + Verfügbar + {% else %} + Nicht verfügbar + {% endif %} +
+
+ Installiert: {{ project.created }} +
+
+ + {% if project.readme %} +
+
README:
+
+
{{ project.readme }}
+
+
+ {% endif %} +
+
+ + +
+
+
+ Umgebungskonfiguration (.env) +
+
+ + +
+
+
+
+
+ +
+ Konfigurieren Sie hier die Umgebungsvariablen für Ihr Projekt. + Diese werden in die .env Datei gespeichert. +
+
+
+ + +
+
+
+
+ + +
+
+
+ Container Logs +
+ +
+
+
+
+ Logs werden geladen... +
+
+
+
+
+ +
+ +
+
+
+ Container-Steuerung +
+
+
+
+ {% if project.status == 'running' %} + + Container stoppen + + + {% else %} + + + {% endif %} + + + Image neu bauen + + + + Projekt entfernen + +
+
+
+ + + {% if project.status == 'running' %} +
+
+
+ Aktive Verbindungen +
+
+
+
+
+ HTTP: + + :8080 + +
+
+
+ + Klicken Sie auf die Links um die Anwendung zu öffnen. + +
+
+
+ {% endif %} + + + {% if project.status == 'running' %} +
+
+
+ Monitoring +
+
+
+
+
+
+ - + CPU % +
+
+
+
+ - + RAM MB +
+
+
+
+
+
+ - + Net In +
+
+
+
+ - + Net Out +
+
+
+
+ +
+
+
+ {% endif %} + + +
+
+
+ Hilfe & Tipps +
+
+
+
+ {% if not project.has_dockerfile %} +
+ Kein Dockerfile: Erstellen Sie ein Dockerfile in Ihrem Projektverzeichnis. +
+ {% endif %} + + {% if not project.has_env_example %} +
+ Tipp: Erstellen Sie eine .env.example Datei für Umgebungsvariablen. +
+ {% endif %} + +
    +
  • Bauen Sie das Image vor dem ersten Start
  • +
  • Überprüfen Sie die .env Konfiguration
  • +
  • Beachten Sie die Container-Logs bei Problemen
  • +
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/project_details_new.html b/templates/project_details_new.html new file mode 100644 index 0000000..4ff604b --- /dev/null +++ b/templates/project_details_new.html @@ -0,0 +1,696 @@ +{% extends "base.html" %} + +{% block title %}{{ project.name }} - Details{% endblock %} + +{% block content %} +
+

+ {{ project.name }} + + + {% if project.status == 'running' %}Läuft{% elif project.status in ['exited', 'stopped'] %}Gestoppt{% else %}Unbekannt{% endif %} + +

+ + Zurück + +
+ +
+
+ +
+
+
+ Projektinformationen +
+
+
+
+
+ Name: {{ project.name }} +
+
+ Pfad: {{ project.path }} +
+
+
+
+ Docker verfügbar: + {% if project.has_dockerfile %} + Ja + {% else %} + Nein + {% endif %} +
+
+ Umgebungskonfiguration: + {% if project.has_env_example %} + .env.example vorhanden + {% else %} + Keine .env.example + {% endif %} +
+
+
+
+ Docker Compose: + {% if project.has_docker_compose %} + Verfügbar + {% else %} + Nicht verfügbar + {% endif %} +
+
+ Installiert: {{ project.created }} +
+
+ + {% if project.readme %} +
+
README:
+
+
{{ project.readme }}
+
+
+ {% endif %} +
+
+ + +
+
+
+ Umgebungskonfiguration (.env) +
+
+ + +
+
+
+
+
+ +
+ Konfigurieren Sie hier die Umgebungsvariablen für Ihr Projekt. + Diese werden in die .env Datei gespeichert. +
+
+
+ + +
+
+
+
+ + +
+
+
+ Container Logs +
+ +
+
+
+
+ {% if project.status == 'running' %} + Logs werden geladen... + {% else %} + Container ist nicht gestartet. Keine Logs verfügbar. + {% endif %} +
+
+
+
+
+ +
+ +
+
+
+ Container-Verwaltung +
+
+
+
+ {% if project.status == 'running' %} + + Container stoppen + + + + {% else %} + + + {% if not project.has_dockerfile %} +
+ + Kein Dockerfile gefunden! Build ist erforderlich. +
+ {% endif %} + {% endif %} + +
+ + + Image neu bauen + + + + + + + + Projekt entfernen + +
+
+
+ + + {% if project.status == 'running' %} +
+
+
+ Aktive Verbindungen +
+
+
+
+
+ Hauptzugriff: + + :8080 + +
+
+
+ + + Der tatsächliche Port kann variieren je nach Konfiguration. + +
+
+
+ {% endif %} + + + {% if project.status == 'running' %} +
+
+
+ Monitoring +
+
+
+
+
+
+ - + CPU % +
+
+
+
+ - + RAM MB +
+
+
+
+
+
+ - + Net In +
+
+
+
+ - + Net Out +
+
+
+
+ +
+
+
+ {% endif %} + + +
+
+
+ Hilfe & Tipps +
+
+
+
+ {% if not project.has_dockerfile %} +
+ Kein Dockerfile: Erstellen Sie ein Dockerfile in Ihrem Projektverzeichnis. +
+ {% endif %} + + {% if not project.has_env_example %} +
+ Tipp: Erstellen Sie eine .env.example Datei für Umgebungsvariablen. +
+ {% endif %} + +
    +
  • Bauen Sie das Image vor dem ersten Start
  • +
  • Überprüfen Sie die .env Konfiguration
  • +
  • Beachten Sie die Container-Logs bei Problemen
  • +
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/project_details_old.html b/templates/project_details_old.html new file mode 100644 index 0000000..0c0f1fc --- /dev/null +++ b/templates/project_details_old.html @@ -0,0 +1,709 @@ +{% extends "base.html" %} + +{% block title %}{{ project.name }} - Details{% endblock %} + +{% block content %} +
+

+ {{ project.name }} + + + {% if project.status == 'running' %}Läuft{% elif project.status in ['exited', 'stopped'] %}Gestoppt{% else %}Unbekannt{% endif %} + +

+ + Zurück + +
+ +
+
+ +
+
+
+ Projektinformationen +
+
+
+
+
+ Name: {{ project.name }} +
+
+ Pfad: {{ project.path }} +
+
+
+
+ Docker verfügbar: + {% if project.has_dockerfile %} + Ja + {% else %} + Nein + {% endif %} +
+
+ Umgebungskonfiguration: + {% if project.has_env_example %} + .env.example vorhanden + {% else %} + Keine .env.example + {% endif %} +
+
+
+
+ Docker Compose: + {% if project.has_docker_compose %} + Verfügbar + {% else %} + Nicht verfügbar + {% endif %} +
+
+ Installiert: {{ project.created }} +
+
+ + {% if project.readme %} +
+
README:
+
+
{{ project.readme }}
+
+
+ {% endif %} +
+
+ + +
+
+
+ Umgebungskonfiguration (.env) +
+
+ + +
+
+
+
+
+ +
+ Konfigurieren Sie hier die Umgebungsvariablen für Ihr Projekt. + Diese werden in die .env Datei gespeichert. +
+
+
+ + +
+
+
+
+ + +
+
+
+ Container Logs +
+ +
+
+
+
+ Logs werden geladen... +
+
+
+
+
+ +
+ +
+
+
+ Schnellaktionen +
+
+
+
+ {% if project.status == 'running' %} + + Container stoppen + + + {% else %} +
+ + +
+
+ Port wird automatisch geprüft und angepasst falls belegt +
+ {% endif %} + + + Image neu bauen + + + + + + + + Projekt entfernen + +
+
+
+ + +
+
+
+ Port Konfiguration +
+
+
+
+ +
+ + +
+
+ Port-Status wird hier angezeigt +
+
+
+ +
+ + + + +
+
+
+
+ + +
+
+ + {% if project.status == 'running' %} +
+
Aktive Ports:
+
+
+ HTTP: + + :8080 + +
+
+
+ {% endif %} +
+
+ + +
+
+
+ Backup & Wiederherstellung +
+
+
+
+ + + + +
+
+
+ + +
+
+
+ Monitoring +
+
+
+
+
+
+ - + CPU % +
+
+
+
+ - + RAM MB +
+
+
+
+
+
+ - + Net In +
+
+
+
+ - + Net Out +
+
+
+
+
+
+
+ + + +{% endblock %} + +{% block scripts %} + +{% endblock %}