Dateien
Hetzner-Backup/v2_adminpanel/templates/resources.html
2025-06-09 04:09:59 +02:00

600 Zeilen
24 KiB
HTML
Originalformat Blame Verlauf

Diese Datei enthält unsichtbare Unicode-Zeichen
Diese Datei enthält unsichtbare Unicode-Zeichen, die für Menschen nicht unterscheidbar sind, aber von einem Computer unterschiedlich verarbeitet werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.
Diese Datei enthält Unicode-Zeichen, die mit anderen Zeichen verwechselt werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.
{% extends "base.html" %}
{% block title %}Resource Pool{% endblock %}
{% block extra_css %}
<style>
/* Statistik-Karten Design wie Dashboard */
.stat-card {
transition: all 0.3s ease;
cursor: pointer;
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
height: 100%;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
}
.stat-card .card-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
opacity: 0.8;
}
.stat-card .card-value {
font-size: 2.5rem;
font-weight: bold;
margin: 0.5rem 0;
}
.stat-card .card-label {
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.7;
}
/* Resource Type Icons */
.resource-icon {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 1.2rem;
}
.resource-icon.domain {
background-color: #e3f2fd;
color: #1976d2;
}
.resource-icon.ipv4 {
background-color: #f3e5f5;
color: #7b1fa2;
}
.resource-icon.phone {
background-color: #e8f5e9;
color: #388e3c;
}
/* Status Badges */
.status-badge {
padding: 0.35rem 0.65rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 500;
}
.status-available {
background-color: #d4edda;
color: #155724;
}
.status-allocated {
background-color: #d1ecf1;
color: #0c5460;
}
.status-quarantine {
background-color: #fff3cd;
color: #856404;
}
/* Progress Bar Custom */
.progress-custom {
height: 25px;
background-color: #f0f0f0;
border-radius: 10px;
}
.progress-bar-custom {
font-size: 0.75rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
/* Table Styling */
.table-custom {
border: none;
}
.table-custom thead th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
text-transform: uppercase;
font-size: 0.875rem;
color: #495057;
padding: 1rem;
}
.table-custom tbody tr {
transition: all 0.2s ease;
}
.table-custom tbody tr:hover {
background-color: #f8f9fa;
}
.table-custom td {
padding: 1rem;
vertical-align: middle;
}
/* Action Buttons */
.btn-action {
width: 35px;
height: 35px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
margin: 0 2px;
transition: all 0.2s ease;
}
.btn-action:hover {
transform: scale(1.1);
}
/* Copy Button */
.copy-btn {
background: none;
border: none;
color: #6c757d;
padding: 0.25rem 0.5rem;
transition: color 0.2s ease;
}
.copy-btn:hover {
color: #28a745;
}
.copy-btn.copied {
color: #28a745;
}
/* Filter Card */
.filter-card {
background-color: #f8f9fa;
border: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem;
color: #6c757d;
}
.empty-state i {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="mb-0">Resource Pool</h1>
<p class="text-muted mb-0">Verwalten Sie Domains, IPs und Telefonnummern</p>
</div>
<div>
<a href="{{ url_for('add_resources') }}" class="btn btn-success">
Ressourcen hinzufügen
</a>
<a href="{{ url_for('resources_metrics') }}" class="btn btn-info">
📊 Metriken
</a>
<a href="{{ url_for('resources_report') }}" class="btn btn-secondary">
📄 Report
</a>
</div>
</div>
<!-- Statistik-Karten -->
<div class="row g-4 mb-4">
{% for type, data in stats.items() %}
<div class="col-md-4">
<div class="card stat-card">
<div class="card-body text-center">
<div class="card-icon">
{% if type == 'domain' %}
🌐
{% elif type == 'ipv4' %}
🖥️
{% else %}
📱
{% endif %}
</div>
<h5 class="text-muted mb-2">{{ type|upper }}</h5>
<div class="card-value text-primary">{{ data.available }}</div>
<div class="card-label text-muted mb-3">von {{ data.total }} verfügbar</div>
<div class="progress progress-custom">
<div class="progress-bar bg-success progress-bar-custom"
style="width: {{ data.available_percent }}%"
data-bs-toggle="tooltip"
title="{{ data.available }} verfügbar">
{{ data.available_percent }}%
</div>
<div class="progress-bar bg-info progress-bar-custom"
style="width: {{ (data.allocated / data.total * 100) if data.total > 0 else 0 }}%"
data-bs-toggle="tooltip"
title="{{ data.allocated }} zugeteilt">
{% if data.allocated > 0 %}{{ data.allocated }}{% endif %}
</div>
<div class="progress-bar bg-warning progress-bar-custom"
style="width: {{ (data.quarantine / data.total * 100) if data.total > 0 else 0 }}%"
data-bs-toggle="tooltip"
title="{{ data.quarantine }} in Quarantäne">
{% if data.quarantine > 0 %}{{ data.quarantine }}{% endif %}
</div>
</div>
<div class="mt-3">
{% if data.available_percent < 20 %}
<span class="badge bg-danger">⚠️ Niedriger Bestand</span>
{% elif data.available_percent < 50 %}
<span class="badge bg-warning text-dark">⚡ Bestand prüfen</span>
{% else %}
<span class="badge bg-success">✅ Gut gefüllt</span>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Filter -->
<div class="card filter-card mb-4">
<div class="card-body">
<form method="get" action="{{ url_for('resources') }}" id="filterForm">
<div class="row g-3">
<div class="col-md-3">
<label for="type" class="form-label">🏷️ Typ</label>
<select name="type" id="type" class="form-select">
<option value="">Alle Typen</option>
<option value="domain" {% if resource_type == 'domain' %}selected{% endif %}>🌐 Domain</option>
<option value="ipv4" {% if resource_type == 'ipv4' %}selected{% endif %}>🖥️ IPv4</option>
<option value="phone" {% if resource_type == 'phone' %}selected{% endif %}>📱 Telefon</option>
</select>
</div>
<div class="col-md-3">
<label for="status" class="form-label">📊 Status</label>
<select name="status" id="status" class="form-select">
<option value="">Alle Status</option>
<option value="available" {% if status_filter == 'available' %}selected{% endif %}>✅ Verfügbar</option>
<option value="allocated" {% if status_filter == 'allocated' %}selected{% endif %}>🔗 Zugeteilt</option>
<option value="quarantine" {% if status_filter == 'quarantine' %}selected{% endif %}>⚠️ Quarantäne</option>
</select>
</div>
<div class="col-md-4">
<label for="search" class="form-label">🔍 Suche</label>
<input type="text" name="search" id="search" class="form-control"
placeholder="Ressource suchen..." value="{{ search }}">
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<a href="{{ url_for('resources') }}" class="btn btn-secondary w-100">
🔄 Zurücksetzen
</a>
</div>
</div>
</form>
</div>
</div>
<!-- Ressourcen-Tabelle -->
<div class="card">
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">📋 Ressourcen-Liste</h5>
<span class="badge bg-secondary">{{ total }} Einträge</span>
</div>
</div>
<div class="card-body p-0">
{% if resources %}
<div class="table-responsive">
<table class="table table-custom mb-0">
<thead>
<tr>
<th width="80">ID</th>
<th width="120">Typ</th>
<th>Ressource</th>
<th width="140">Status</th>
<th>Zugewiesen an</th>
<th width="180">Letzte Änderung</th>
<th width="140" class="text-center">Aktionen</th>
</tr>
</thead>
<tbody>
{% for resource in resources %}
<tr>
<td>
<span class="text-muted">#{{ resource[0] }}</span>
</td>
<td>
<div class="resource-icon {{ resource[1] }}">
{% if resource[1] == 'domain' %}
🌐
{% elif resource[1] == 'ipv4' %}
🖥️
{% else %}
📱
{% endif %}
</div>
</td>
<td>
<div class="d-flex align-items-center">
<code class="me-2">{{ resource[2] }}</code>
<button class="copy-btn" onclick="copyToClipboard('{{ resource[2] }}', this)"
title="Kopieren">
<i class="fas fa-copy"></i>
</button>
</div>
</td>
<td>
{% if resource[3] == 'available' %}
<span class="status-badge status-available">
✅ Verfügbar
</span>
{% elif resource[3] == 'allocated' %}
<span class="status-badge status-allocated">
🔗 Zugeteilt
</span>
{% else %}
<span class="status-badge status-quarantine">
⚠️ Quarantäne
</span>
{% if resource[8] %}
<div class="small text-muted mt-1">{{ resource[8] }}</div>
{% endif %}
{% endif %}
</td>
<td>
{% if resource[5] %}
<div>
<a href="{{ url_for('edit_license', license_id=resource[4]) }}"
class="text-decoration-none">
<strong>{{ resource[5] }}</strong>
</a>
</div>
<div class="small text-muted">{{ resource[6] }}</div>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if resource[7] %}
<div class="small">
<div>{{ resource[7].strftime('%d.%m.%Y') }}</div>
<div class="text-muted">{{ resource[7].strftime('%H:%M Uhr') }}</div>
</div>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="text-center">
<a href="{{ url_for('resource_history', resource_id=resource[0]) }}"
class="btn btn-action btn-sm btn-outline-info"
title="Historie anzeigen">
<i class="fas fa-history"></i>
</a>
{% if resource[3] == 'available' %}
<button class="btn btn-action btn-sm btn-outline-warning"
onclick="showQuarantineModal({{ resource[0] }})"
title="In Quarantäne setzen">
<i class="fas fa-ban"></i>
</button>
{% elif resource[3] == 'quarantine' %}
<form method="post" action="{{ url_for('release_resources') }}"
style="display: inline;">
<input type="hidden" name="resource_ids[]" value="{{ resource[0] }}">
<button type="submit"
class="btn btn-action btn-sm btn-outline-success"
title="Ressource freigeben">
<i class="fas fa-check"></i>
</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<i class="fas fa-inbox"></i>
<h4>Keine Ressourcen gefunden</h4>
<p>Ändern Sie Ihre Filterkriterien oder fügen Sie neue Ressourcen hinzu.</p>
</div>
{% endif %}
</div>
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<nav class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link"
href="{{ url_for('resources', page=1, type=resource_type, status=status_filter, search=search) }}">
<i class="fas fa-angle-double-left"></i> Erste
</a>
</li>
<li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link"
href="{{ url_for('resources', page=page-1, type=resource_type, status=status_filter, search=search) }}">
<i class="fas fa-angle-left"></i> Zurück
</a>
</li>
{% for p in range(1, total_pages + 1) %}
{% if p == page or (p >= page - 2 and p <= page + 2) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link"
href="{{ url_for('resources', page=p, type=resource_type, status=status_filter, search=search) }}">
{{ p }}
</a>
</li>
{% endif %}
{% endfor %}
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link"
href="{{ url_for('resources', page=page+1, type=resource_type, status=status_filter, search=search) }}">
Weiter <i class="fas fa-angle-right"></i>
</a>
</li>
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link"
href="{{ url_for('resources', page=total_pages, type=resource_type, status=status_filter, search=search) }}">
Letzte <i class="fas fa-angle-double-right"></i>
</a>
</li>
</ul>
</nav>
{% endif %}
<!-- Kürzliche Aktivitäten -->
{% if recent_activities %}
<div class="card mt-4">
<div class="card-header bg-white">
<h5 class="mb-0">⏰ Kürzliche Aktivitäten</h5>
</div>
<div class="card-body">
<div class="timeline">
{% for activity in recent_activities %}
<div class="d-flex mb-3">
<div class="me-3">
{% if activity[0] == 'created' %}
<span class="badge bg-success rounded-pill"></span>
{% elif activity[0] == 'allocated' %}
<span class="badge bg-info rounded-pill">🔗</span>
{% elif activity[0] == 'deallocated' %}
<span class="badge bg-secondary rounded-pill">🔓</span>
{% elif activity[0] == 'quarantined' %}
<span class="badge bg-warning rounded-pill">⚠️</span>
{% else %}
<span class="badge bg-primary rounded-pill"></span>
{% endif %}
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between">
<div>
<strong>{{ activity[4] }}</strong> ({{ activity[3] }}) - {{ activity[0] }}
{% if activity[1] %}
<span class="text-muted">von {{ activity[1] }}</span>
{% endif %}
</div>
<small class="text-muted">
{{ activity[2].strftime('%d.%m.%Y %H:%M') if activity[2] else '' }}
</small>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
<!-- Quarantäne Modal -->
<div class="modal fade" id="quarantineModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" id="quarantineForm">
<div class="modal-header">
<h5 class="modal-title">⚠️ Ressource in Quarantäne setzen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="reason" class="form-label">Grund</label>
<select name="reason" id="reason" class="form-select" required>
<option value="">Bitte wählen...</option>
<option value="review">🔍 Überprüfung</option>
<option value="abuse">⚠️ Missbrauch</option>
<option value="defect">❌ Defekt</option>
<option value="maintenance">🔧 Wartung</option>
<option value="blacklisted">🚫 Blacklisted</option>
<option value="expired">⏰ Abgelaufen</option>
</select>
</div>
<div class="mb-3">
<label for="until_date" class="form-label">Bis wann? (optional)</label>
<input type="date" name="until_date" id="until_date" class="form-control"
min="{{ datetime.now().strftime('%Y-%m-%d') }}">
</div>
<div class="mb-3">
<label for="notes" class="form-label">Notizen</label>
<textarea name="notes" id="notes" class="form-control" rows="3"
placeholder="Zusätzliche Informationen..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Abbrechen
</button>
<button type="submit" class="btn btn-warning">
⚠️ In Quarantäne setzen
</button>
</div>
</form>
</div>
</div>
</div>
<script>
// Live-Filtering
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('filterForm');
const inputs = form.querySelectorAll('select, input[type="text"]');
inputs.forEach(input => {
if (input.type === 'text') {
let timeout;
input.addEventListener('input', function() {
clearTimeout(timeout);
timeout = setTimeout(() => form.submit(), 300);
});
} else {
input.addEventListener('change', () => form.submit());
}
});
// Bootstrap Tooltips initialisieren
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
});
// Copy to Clipboard mit besserem Feedback
function copyToClipboard(text, button) {
navigator.clipboard.writeText(text).then(() => {
// Button Icon ändern
const icon = button.querySelector('i');
icon.classList.remove('fa-copy');
icon.classList.add('fa-check');
button.classList.add('copied');
// Nach 2 Sekunden zurücksetzen
setTimeout(() => {
icon.classList.remove('fa-check');
icon.classList.add('fa-copy');
button.classList.remove('copied');
}, 2000);
});
}
// Quarantäne Modal
function showQuarantineModal(resourceId) {
const modalElement = document.getElementById('quarantineModal');
const modal = new bootstrap.Modal(modalElement);
const form = modalElement.querySelector('form');
form.setAttribute('action', `/resources/quarantine/${resourceId}`);
modal.show();
}
</script>
{% endblock %}