modified: templates/base.html
modified: templates/index.html
This commit is contained in:
@@ -198,40 +198,272 @@
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Technologie-Statistiken */
|
||||
.tech-stat {
|
||||
border: 1px solid #e0e0e0;
|
||||
transition: background-color 0.2s ease;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.tech-stat:hover {
|
||||
background-color: rgba(0,0,0,0.05) !important;
|
||||
background-color: #f8f9fa !important;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
/* Verbesserte Header-Stats */
|
||||
.header-stats {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
padding: 30px !important;
|
||||
border-radius: 15px !important;
|
||||
margin-bottom: 30px !important;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1) !important;
|
||||
/* Erweiterte Projekt-Karten */
|
||||
.project-card {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center !important;
|
||||
color: white !important;
|
||||
padding: 15px !important;
|
||||
.project-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block !important;
|
||||
font-size: 2.5em !important;
|
||||
font-weight: bold !important;
|
||||
margin-bottom: 5px !important;
|
||||
.project-card .card-header {
|
||||
border-bottom: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-item span:last-child {
|
||||
font-size: 0.9em !important;
|
||||
opacity: 0.9 !important;
|
||||
.project-card .card-footer {
|
||||
border-top: 1px solid rgba(0,0,0,0.125);
|
||||
background-color: rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
/* Status-Badge-Verbesserungen */
|
||||
.status-badge {
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: linear-gradient(45deg, #28a745, #20c997);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-stopped {
|
||||
background: linear-gradient(45deg, #dc3545, #e74c3c);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-unknown {
|
||||
background: linear-gradient(45deg, #ffc107, #fd7e14);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Tab-Navigation */
|
||||
.nav-pills .nav-link {
|
||||
border-radius: 25px;
|
||||
margin: 0 5px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active {
|
||||
background: linear-gradient(45deg, #007bff, #0056b3);
|
||||
box-shadow: 0 4px 15px rgba(0,123,255,0.3);
|
||||
}
|
||||
|
||||
/* Aktions-Buttons */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-buttons .btn {
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8em;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-buttons .btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Schnellaktionen */
|
||||
.quick-actions .btn {
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.quick-actions .btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 15px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Progress Modal */
|
||||
.progress {
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Notification Styles */
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Responsive Verbesserungen */
|
||||
@media (max-width: 768px) {
|
||||
.action-buttons {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-buttons .btn {
|
||||
flex: 1;
|
||||
margin: 2px;
|
||||
padding: 8px 4px;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.category-stat {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.stat-number-cat {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.tech-stat {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link {
|
||||
margin: 2px 0;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.header-stats .stat-item {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.action-buttons .btn {
|
||||
padding: 6px 8px;
|
||||
font-size: 0.65em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animationen */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.project-card {
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
}
|
||||
|
||||
.project-card:nth-child(1) { animation-delay: 0.1s; }
|
||||
.project-card:nth-child(2) { animation-delay: 0.2s; }
|
||||
.project-card:nth-child(3) { animation-delay: 0.3s; }
|
||||
.project-card:nth-child(4) { animation-delay: 0.4s; }
|
||||
.project-card:nth-child(5) { animation-delay: 0.5s; }
|
||||
.project-card:nth-child(6) { animation-delay: 0.6s; }
|
||||
|
||||
/* Hover-Effekte für Kategorie-Badges */
|
||||
.badge {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.badge:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn.loading {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
margin-left: -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Dark Mode Unterstützung */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.tech-stat {
|
||||
background-color: #2d3748 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.project-card .card-footer {
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.category-stat {
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Benutzerdefinierte Scrollbar */
|
||||
.tab-content {
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -143,78 +143,300 @@
|
||||
</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 %}
|
||||
<!-- Apps nach Kategorien gruppiert -->
|
||||
{% set category_mapping = {
|
||||
'web': {'name': 'Web Applications', 'icon': 'fas fa-globe', 'color': 'primary'},
|
||||
'productivity': {'name': 'Productivity', 'icon': 'fas fa-tasks', 'color': 'success'},
|
||||
'media': {'name': 'Media & Entertainment', 'icon': 'fas fa-play', 'color': 'info'},
|
||||
'server': {'name': 'Web Server', 'icon': 'fas fa-server', 'color': 'warning'},
|
||||
'other': {'name': 'Andere', 'icon': 'fas fa-cube', 'color': 'secondary'}
|
||||
} %}
|
||||
|
||||
<!-- Kategorisierte App-Ansicht -->
|
||||
<div class="mb-4">
|
||||
<nav class="nav nav-pills nav-fill">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#all-apps">
|
||||
<i class="fas fa-th-large me-2"></i>Alle Apps ({{ projects|length }})
|
||||
</a>
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#by-category">
|
||||
<i class="fas fa-layer-group me-2"></i>Nach Kategorie
|
||||
</a>
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#by-status">
|
||||
<i class="fas fa-traffic-light me-2"></i>Nach Status
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- Alle Apps -->
|
||||
<div class="tab-pane fade show active" id="all-apps">
|
||||
<div class="row">
|
||||
{% for project in projects %}
|
||||
<div class="col-lg-6 col-xl-4 mb-4">
|
||||
{% set app_category = project.get('category', 'other')|lower|replace(' ', '')|replace('&', '')|replace('application', '') %}
|
||||
{% set cat_info = category_mapping.get(app_category, category_mapping['other']) %}
|
||||
|
||||
<div class="card project-card h-100 border-{{ cat_info.color }}">
|
||||
<div class="card-header bg-{{ cat_info.color }} text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="{{ cat_info.icon }} me-2"></i>{{ project.name }}
|
||||
</h5>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-light text-dark">{{ project.get('category', 'Unbekannt') }}</span>
|
||||
<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>
|
||||
<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
|
||||
<div class="card-body">
|
||||
<!-- Projektdetails -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-6">
|
||||
<small class="text-muted"><i class="fab fa-docker me-1"></i>Docker:</small><br>
|
||||
{% if project.has_dockerfile %}
|
||||
<span class="badge bg-success"><i class="fas fa-check me-1"></i>Verfügbar</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger"><i class="fas fa-times me-1"></i>Nicht verfügbar</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted"><i class="fas fa-cog me-1"></i>Konfiguration:</small><br>
|
||||
{% if project.has_env_example %}
|
||||
<span class="badge bg-success"><i class="fas fa-file-alt me-1"></i>.env vorhanden</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning"><i class="fas fa-minus me-1"></i>Keine .env</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if project.readme %}
|
||||
<div class="mb-3">
|
||||
<small class="text-muted"><i class="fas fa-book me-1"></i>Beschreibung:</small>
|
||||
<p class="card-text small text-muted">{{ project.readme[:120] }}{% if project.readme|length > 120 %}...{% endif %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Zusätzliche Informationen -->
|
||||
<div class="row text-center mb-3">
|
||||
<div class="col-4">
|
||||
<small class="text-muted d-block">Typ</small>
|
||||
<i class="{{ cat_info.icon }} text-{{ cat_info.color }}"></i>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<small class="text-muted d-block">Version</small>
|
||||
<span class="badge bg-light text-dark">{{ project.version or 'unbekannt' }}</span>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<small class="text-muted d-block">Compose</small>
|
||||
{% if project.has_docker_compose %}
|
||||
<i class="fas fa-check text-success"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-times text-muted"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small class="text-muted"><i class="fas fa-calendar me-1"></i>Installiert: {{ project.created }}</small>
|
||||
</div>
|
||||
<div class="card-footer bg-light">
|
||||
<div class="action-buttons d-flex flex-wrap gap-1">
|
||||
{% 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 %}
|
||||
|
||||
{% if project.has_dockerfile %}
|
||||
<a href="{{ url_for('build_project', project_name=project.name) }}" class="btn btn-info btn-sm">
|
||||
<i class="fas fa-hammer"></i> Build
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('project_details', project_name=project.name) }}" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-cog"></i> Details
|
||||
</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>
|
||||
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Nach Kategorie -->
|
||||
<div class="tab-pane fade" id="by-category">
|
||||
{% for category_key, category_info in category_mapping.items() %}
|
||||
{% set category_projects = [] %}
|
||||
{% for project in projects %}
|
||||
{% set proj_cat = project.get('category', 'other')|lower|replace(' ', '')|replace('&', '')|replace('application', '') %}
|
||||
{% if proj_cat == category_key %}
|
||||
{% set _ = category_projects.append(project) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if category_projects %}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-{{ category_info.color }}">
|
||||
<i class="{{ category_info.icon }} me-2"></i>{{ category_info.name }} ({{ category_projects|length }})
|
||||
</h4>
|
||||
<div class="row">
|
||||
{% for project in category_projects %}
|
||||
<div class="col-lg-6 col-xl-4 mb-3">
|
||||
<div class="card project-card border-{{ category_info.color }}">
|
||||
<div class="card-header bg-{{ category_info.color }} text-white">
|
||||
<h6 class="card-title mb-0">{{ project.name }}</h6>
|
||||
<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="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
{% if project.has_dockerfile %}
|
||||
<i class="fab fa-docker text-primary me-1"></i>
|
||||
{% endif %}
|
||||
{% if project.has_env_example %}
|
||||
<i class="fas fa-file-alt text-success me-1"></i>
|
||||
{% endif %}
|
||||
{% if project.has_docker_compose %}
|
||||
<i class="fas fa-layer-group text-info me-1"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<small class="text-muted">{{ project.created }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="btn-group w-100" role="group">
|
||||
{% 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>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('start_project', project_name=project.name) }}" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-play"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('project_details', project_name=project.name) }}" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Nach Status -->
|
||||
<div class="tab-pane fade" id="by-status">
|
||||
{% set running_apps = projects|selectattr('status', 'equalto', 'running')|list %}
|
||||
{% set stopped_apps = projects|selectattr('status', 'in', ['exited', 'stopped'])|list %}
|
||||
{% set unknown_apps = projects|rejectattr('status', 'in', ['running', 'exited', 'stopped'])|list %}
|
||||
|
||||
<!-- Laufende Apps -->
|
||||
{% if running_apps %}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-success">
|
||||
<i class="fas fa-play-circle me-2"></i>Laufende Apps ({{ running_apps|length }})
|
||||
</h4>
|
||||
<div class="row">
|
||||
{% for project in running_apps %}
|
||||
<div class="col-lg-4 col-md-6 mb-3">
|
||||
<div class="card border-success">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ project.name }}</h6>
|
||||
<p class="card-text small text-muted">{{ project.get('category', 'Unbekannt') }}</p>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="{{ url_for('stop_project', project_name=project.name) }}" class="btn btn-warning btn-sm">
|
||||
<i class="fas fa-stop"></i> Stoppen
|
||||
</a>
|
||||
<a href="{{ url_for('project_details', project_name=project.name) }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Gestoppte Apps -->
|
||||
{% if stopped_apps %}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-warning">
|
||||
<i class="fas fa-stop-circle me-2"></i>Gestoppte Apps ({{ stopped_apps|length }})
|
||||
</h4>
|
||||
<div class="row">
|
||||
{% for project in stopped_apps %}
|
||||
<div class="col-lg-4 col-md-6 mb-3">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ project.name }}</h6>
|
||||
<p class="card-text small text-muted">{{ project.get('category', 'Unbekannt') }}</p>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="{{ url_for('start_project', project_name=project.name) }}" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-play"></i> Starten
|
||||
</a>
|
||||
<a href="{{ url_for('project_details', project_name=project.name) }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Unbekannte Apps -->
|
||||
{% if unknown_apps %}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-danger">
|
||||
<i class="fas fa-question-circle me-2"></i>Status unbekannt ({{ unknown_apps|length }})
|
||||
</h4>
|
||||
<div class="row">
|
||||
{% for project in unknown_apps %}
|
||||
<div class="col-lg-4 col-md-6 mb-3">
|
||||
<div class="card border-danger">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ project.name }}</h6>
|
||||
<p class="card-text small text-muted">{{ project.get('category', 'Unbekannt') }}</p>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="{{ url_for('project_details', project_name=project.name) }}" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-cog"></i> Details
|
||||
</a>
|
||||
{% if project.has_dockerfile %}
|
||||
<a href="{{ url_for('build_project', project_name=project.name) }}" class="btn btn-info btn-sm">
|
||||
<i class="fas fa-hammer"></i> Build
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -229,26 +451,69 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="col-md-2 col-sm-4 mb-2">
|
||||
<a href="{{ url_for('available_projects') }}" class="btn btn-outline-primary w-100">
|
||||
<i class="fas fa-download"></i> Neue App installieren
|
||||
<i class="fas fa-download"></i>
|
||||
<span class="d-none d-md-inline"> Neue App</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="col-md-2 col-sm-4 mb-2">
|
||||
<a href="{{ url_for('refresh_projects') }}" class="btn btn-outline-info w-100">
|
||||
<i class="fas fa-sync-alt"></i> Projektliste aktualisieren
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
<span class="d-none d-md-inline"> Aktualisieren</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="col-md-2 col-sm-4 mb-2">
|
||||
<a href="{{ url_for('config') }}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-cog"></i> Einstellungen
|
||||
<i class="fas fa-cog"></i>
|
||||
<span class="d-none d-md-inline"> Einstellungen</span>
|
||||
</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
|
||||
<div class="col-md-2 col-sm-4 mb-2">
|
||||
<button class="btn btn-outline-success w-100" onclick="startAllStoppedApps()">
|
||||
<i class="fas fa-play"></i>
|
||||
<span class="d-none d-md-inline"> Alle starten</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-4 mb-2">
|
||||
<button class="btn btn-outline-warning w-100" onclick="stopAllRunningApps()">
|
||||
<i class="fas fa-stop"></i>
|
||||
<span class="d-none d-md-inline"> Alle stoppen</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-4 mb-2">
|
||||
<a href="{{ url_for('docker_status') }}" class="btn btn-outline-info w-100">
|
||||
<i class="fab fa-docker"></i>
|
||||
<span class="d-none d-md-inline"> Docker Status</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Erweiterte Aktionen -->
|
||||
<div class="mt-3">
|
||||
<h6 class="text-muted">Erweiterte Aktionen:</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-sm-6 mb-2">
|
||||
<button class="btn btn-outline-dark w-100 btn-sm" onclick="buildAllApps()">
|
||||
<i class="fas fa-hammer"></i> Alle Docker-Apps bauen
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 mb-2">
|
||||
<button class="btn btn-outline-danger w-100 btn-sm" onclick="cleanupContainers()">
|
||||
<i class="fas fa-trash-alt"></i> Container bereinigen
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 mb-2">
|
||||
<button class="btn btn-outline-warning w-100 btn-sm" onclick="showSystemInfo()">
|
||||
<i class="fas fa-info-circle"></i> System-Info
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 mb-2">
|
||||
<button class="btn btn-outline-success w-100 btn-sm" onclick="exportProjects()">
|
||||
<i class="fas fa-file-export"></i> Projekte exportieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,6 +549,19 @@
|
||||
<script>
|
||||
let currentProject = '';
|
||||
|
||||
// Confirm Action Helper
|
||||
function confirmAction(action, projectName) {
|
||||
const actionText = {
|
||||
'remove': 'entfernen',
|
||||
'stop': 'stoppen',
|
||||
'start': 'starten',
|
||||
'build': 'neu bauen'
|
||||
};
|
||||
|
||||
return confirm(`Möchten Sie wirklich "${projectName}" ${actionText[action] || action}?`);
|
||||
}
|
||||
|
||||
// Port Management
|
||||
function startWithCustomPort(projectName) {
|
||||
currentProject = projectName;
|
||||
const modal = new bootstrap.Modal(document.getElementById('portModal'));
|
||||
@@ -295,29 +573,351 @@ function startWithPort() {
|
||||
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.');
|
||||
// Bulk Actions
|
||||
function startAllStoppedApps() {
|
||||
const stoppedApps = document.querySelectorAll('.status-stopped').length;
|
||||
if (stoppedApps > 0) {
|
||||
if (confirm(`Möchten Sie alle ${stoppedApps} gestoppten Apps starten?`)) {
|
||||
showProgress('Starte alle gestoppten Apps...');
|
||||
|
||||
// Sammle alle gestoppten Apps
|
||||
const stoppedProjects = [];
|
||||
document.querySelectorAll('.status-stopped').forEach(element => {
|
||||
const projectCard = element.closest('.project-card');
|
||||
if (projectCard) {
|
||||
const projectName = projectCard.querySelector('.card-title').textContent.trim();
|
||||
stoppedProjects.push(projectName);
|
||||
}
|
||||
});
|
||||
|
||||
// Starte Apps nacheinander
|
||||
startAppsSequentially(stoppedProjects, 0);
|
||||
}
|
||||
} else {
|
||||
alert('Alle Apps laufen bereits oder es sind keine Apps installiert.');
|
||||
}
|
||||
}
|
||||
|
||||
function stopAllRunningApps() {
|
||||
const runningApps = document.querySelectorAll('.status-running').length;
|
||||
if (runningApps > 0) {
|
||||
if (confirm(`Möchten Sie alle ${runningApps} laufenden Apps stoppen?`)) {
|
||||
showProgress('Stoppe alle laufenden Apps...');
|
||||
|
||||
// Sammle alle laufenden Apps
|
||||
const runningProjects = [];
|
||||
document.querySelectorAll('.status-running').forEach(element => {
|
||||
const projectCard = element.closest('.project-card');
|
||||
if (projectCard) {
|
||||
const projectName = projectCard.querySelector('.card-title').textContent.trim();
|
||||
runningProjects.push(projectName);
|
||||
}
|
||||
});
|
||||
|
||||
// Stoppe Apps nacheinander
|
||||
stopAppsSequentially(runningProjects, 0);
|
||||
}
|
||||
} else {
|
||||
alert('Keine Apps laufen derzeit.');
|
||||
}
|
||||
}
|
||||
|
||||
function buildAllApps() {
|
||||
const dockerApps = document.querySelectorAll('.fa-docker').length;
|
||||
if (dockerApps > 0) {
|
||||
if (confirm(`Möchten Sie alle ${dockerApps} Docker-fähigen Apps neu bauen?`)) {
|
||||
showProgress('Baue alle Docker-Apps...');
|
||||
|
||||
// Sammle alle Docker-Apps
|
||||
const dockerProjects = [];
|
||||
document.querySelectorAll('.fa-docker').forEach(element => {
|
||||
const projectCard = element.closest('.project-card');
|
||||
if (projectCard) {
|
||||
const projectName = projectCard.querySelector('.card-title').textContent.trim();
|
||||
dockerProjects.push(projectName);
|
||||
}
|
||||
});
|
||||
|
||||
// Baue Apps nacheinander
|
||||
buildAppsSequentially(dockerProjects, 0);
|
||||
}
|
||||
} else {
|
||||
alert('Keine Docker-fähigen Apps gefunden.');
|
||||
}
|
||||
}
|
||||
|
||||
// Sequential Operations
|
||||
function startAppsSequentially(projects, index) {
|
||||
if (index >= projects.length) {
|
||||
hideProgress();
|
||||
showNotification('Alle Apps wurden gestartet!', 'success');
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const projectName = projects[index];
|
||||
updateProgress(`Starte ${projectName}... (${index + 1}/${projects.length})`);
|
||||
|
||||
fetch(`/start_project/${projectName}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
updateProgress(`${projectName} gestartet. Weiter mit nächster App...`);
|
||||
setTimeout(() => startAppsSequentially(projects, index + 1), 1000);
|
||||
} else {
|
||||
showNotification(`Fehler beim Starten von ${projectName}: ${data.message}`, 'error');
|
||||
setTimeout(() => startAppsSequentially(projects, index + 1), 1000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler:', error);
|
||||
setTimeout(() => startAppsSequentially(projects, index + 1), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function stopAppsSequentially(projects, index) {
|
||||
if (index >= projects.length) {
|
||||
hideProgress();
|
||||
showNotification('Alle Apps wurden gestoppt!', 'success');
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const projectName = projects[index];
|
||||
updateProgress(`Stoppe ${projectName}... (${index + 1}/${projects.length})`);
|
||||
|
||||
fetch(`/stop_project/${projectName}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
updateProgress(`${projectName} gestoppt. Weiter mit nächster App...`);
|
||||
setTimeout(() => stopAppsSequentially(projects, index + 1), 1000);
|
||||
} else {
|
||||
showNotification(`Fehler beim Stoppen von ${projectName}: ${data.message}`, 'error');
|
||||
setTimeout(() => stopAppsSequentially(projects, index + 1), 1000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler:', error);
|
||||
setTimeout(() => stopAppsSequentially(projects, index + 1), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function buildAppsSequentially(projects, index) {
|
||||
if (index >= projects.length) {
|
||||
hideProgress();
|
||||
showNotification('Alle Apps wurden gebaut!', 'success');
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const projectName = projects[index];
|
||||
updateProgress(`Baue ${projectName}... (${index + 1}/${projects.length})`);
|
||||
|
||||
fetch(`/build_project/${projectName}`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
updateProgress(`${projectName} gebaut. Weiter mit nächster App...`);
|
||||
setTimeout(() => buildAppsSequentially(projects, index + 1), 2000);
|
||||
} else {
|
||||
showNotification(`Fehler beim Bauen von ${projectName}: ${data.message}`, 'error');
|
||||
setTimeout(() => buildAppsSequentially(projects, index + 1), 1000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler:', error);
|
||||
setTimeout(() => buildAppsSequentially(projects, index + 1), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Additional Actions
|
||||
function cleanupContainers() {
|
||||
if (confirm('Möchten Sie alle gestoppten Container und nicht verwendeten Images entfernen?')) {
|
||||
showProgress('Bereinige Container und Images...');
|
||||
|
||||
fetch('/api/cleanup_docker', {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
hideProgress();
|
||||
if (data.success) {
|
||||
showNotification('Container-Bereinigung abgeschlossen!', 'success');
|
||||
} else {
|
||||
showNotification(`Fehler bei der Bereinigung: ${data.message}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideProgress();
|
||||
showNotification('Fehler bei der Bereinigung', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showSystemInfo() {
|
||||
fetch('/api/system_info')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const info = `
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>System</h6>
|
||||
<p><strong>Docker Status:</strong> ${data.docker_status ? 'Verfügbar' : 'Nicht verfügbar'}</p>
|
||||
<p><strong>Installierte Apps:</strong> ${data.installed_apps}</p>
|
||||
<p><strong>Laufende Apps:</strong> ${data.running_apps}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Ressourcen</h6>
|
||||
<p><strong>Speicher:</strong> ${data.disk_usage || 'Unbekannt'}</p>
|
||||
<p><strong>Container:</strong> ${data.container_count || 0}</p>
|
||||
<p><strong>Images:</strong> ${data.image_count || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
showModal('System-Information', info);
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('Fehler beim Laden der System-Informationen', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function exportProjects() {
|
||||
if (confirm('Möchten Sie eine Liste aller installierten Projekte exportieren?')) {
|
||||
window.location.href = '/api/export_projects';
|
||||
}
|
||||
}
|
||||
|
||||
// Progress and Notification Functions
|
||||
function showProgress(message) {
|
||||
const progressHtml = `
|
||||
<div id="progress-modal" class="modal fade" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body text-center">
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Lädt...</span>
|
||||
</div>
|
||||
<h5 id="progress-message">${message}</h5>
|
||||
<div class="progress mt-3">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', progressHtml);
|
||||
const modal = new bootstrap.Modal(document.getElementById('progress-modal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function updateProgress(message) {
|
||||
const progressMessage = document.getElementById('progress-message');
|
||||
if (progressMessage) {
|
||||
progressMessage.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
function hideProgress() {
|
||||
const progressModal = document.getElementById('progress-modal');
|
||||
if (progressModal) {
|
||||
const modal = bootstrap.Modal.getInstance(progressModal);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
setTimeout(() => progressModal.remove(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
const alertClass = type === 'success' ? 'alert-success' :
|
||||
type === 'error' ? 'alert-danger' : 'alert-info';
|
||||
|
||||
const notificationHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show position-fixed"
|
||||
style="top: 20px; right: 20px; z-index: 9999;" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', notificationHtml);
|
||||
|
||||
// Auto-remove nach 5 Sekunden
|
||||
setTimeout(() => {
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
alerts.forEach(alert => {
|
||||
if (alert.textContent.includes(message)) {
|
||||
alert.remove();
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showModal(title, content) {
|
||||
const modalHtml = `
|
||||
<div class="modal fade" id="info-modal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${title}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${content}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
const modal = new bootstrap.Modal(document.getElementById('info-modal'));
|
||||
modal.show();
|
||||
|
||||
// Modal nach dem Schließen entfernen
|
||||
document.getElementById('info-modal').addEventListener('hidden.bs.modal', function () {
|
||||
this.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-update status badges
|
||||
function updateStatus() {
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Status updates würden hier implementiert
|
||||
// Update status badges
|
||||
if (data.projects) {
|
||||
data.projects.forEach(project => {
|
||||
const statusBadges = document.querySelectorAll(`[data-project="${project.name}"]`);
|
||||
statusBadges.forEach(badge => {
|
||||
badge.className = `status-badge status-${project.status}`;
|
||||
badge.innerHTML = `<i class="fas fa-circle me-1"></i>${project.status_text}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => console.log('Status update failed:', error));
|
||||
}
|
||||
|
||||
// Update status every 30 seconds
|
||||
setInterval(updateStatus, 30000);
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update status every 30 seconds
|
||||
setInterval(updateStatus, 30000);
|
||||
|
||||
// Add project data attributes to status badges
|
||||
document.querySelectorAll('.status-badge').forEach(badge => {
|
||||
const projectCard = badge.closest('.project-card');
|
||||
if (projectCard) {
|
||||
const projectName = projectCard.querySelector('.card-title').textContent.trim();
|
||||
badge.setAttribute('data-project', projectName);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user