Dateien
Hetzner-Backup/v2_adminpanel/templates/base.html
2025-06-08 22:37:03 +02:00

451 Zeilen
15 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Admin Panel{% endblock %} - Lizenzverwaltung</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
{% block extra_css %}{% endblock %}
<style>
/* Global Status Colors */
:root {
--status-active: #28a745;
--status-warning: #ffc107;
--status-danger: #dc3545;
--status-inactive: #6c757d;
--status-info: #17a2b8;
}
/* Status Classes - Global */
.status-aktiv { color: var(--status-active) !important; }
.status-ablaufend { color: var(--status-warning) !important; }
.status-abgelaufen { color: var(--status-danger) !important; }
.status-deaktiviert { color: var(--status-inactive) !important; }
/* Badge Variants */
.badge-aktiv { background-color: var(--status-active) !important; }
.badge-ablaufend { background-color: var(--status-warning) !important; color: #000 !important; }
.badge-abgelaufen { background-color: var(--status-danger) !important; }
.badge-deaktiviert { background-color: var(--status-inactive) !important; }
/* Session Timer Styles */
#session-timer {
font-family: monospace;
font-weight: bold;
font-size: 1.1rem;
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
transition: all 0.3s ease;
}
.timer-normal {
background-color: var(--status-active);
color: white;
}
.timer-warning {
background-color: var(--status-warning);
color: #000;
}
.timer-danger {
background-color: var(--status-danger);
color: white;
animation: pulse 1s infinite;
}
.timer-critical {
background-color: var(--status-danger);
color: white;
animation: blink 0.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.session-warning-modal {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
max-width: 400px;
}
/* Table Improvements */
.table-container {
max-height: 600px;
overflow-y: auto;
position: relative;
}
.table-sticky thead {
position: sticky;
top: 0;
background-color: #fff;
z-index: 10;
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
}
.table-sticky thead th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
}
/* Inline Actions */
.btn-copy {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-copy:hover {
background-color: #e9ecef;
}
.btn-copy.copied {
background-color: var(--status-active);
color: white;
}
/* Toggle Switch */
.form-switch-custom {
display: inline-block;
}
.form-switch-custom .form-check-input {
cursor: pointer;
width: 3em;
height: 1.5em;
}
.form-switch-custom .form-check-input:checked {
background-color: var(--status-active);
border-color: var(--status-active);
}
/* Bulk Actions Bar */
.bulk-actions {
position: sticky;
bottom: 0;
background-color: #212529;
color: white;
padding: 1rem;
display: none;
z-index: 100;
box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.1);
}
.bulk-actions.show {
display: flex;
align-items: center;
justify-content: space-between;
}
/* Checkbox Styling */
.checkbox-cell {
width: 40px;
}
.form-check-input-custom {
cursor: pointer;
width: 1.2em;
height: 1.2em;
}
/* Sortable Table Styles */
.sortable-table th.sortable {
cursor: pointer;
user-select: none;
position: relative;
padding-right: 25px;
}
.sortable-table th.sortable:hover {
background-color: #e9ecef;
}
.sortable-table th.sortable::after {
content: '↕';
position: absolute;
right: 8px;
opacity: 0.3;
}
.sortable-table th.sortable.asc::after {
content: '↑';
opacity: 1;
color: var(--status-active);
}
.sortable-table th.sortable.desc::after {
content: '↓';
opacity: 1;
color: var(--status-active);
}
/* Server-side sortable styles */
.server-sortable {
cursor: pointer;
text-decoration: none;
color: inherit;
display: flex;
align-items: center;
justify-content: space-between;
}
.server-sortable:hover {
color: var(--bs-primary);
text-decoration: none;
}
.sort-indicator {
margin-left: 5px;
font-size: 0.8em;
}
.sort-indicator.active {
color: var(--bs-primary);
}
</style>
</head>
<body class="bg-light">
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a href="/" class="navbar-brand text-decoration-none">🎛️ AccountForger - Admin Panel</a>
<div class="d-flex align-items-center">
<div id="session-timer" class="timer-normal me-3">
⏱️ <span id="timer-display">5:00</span>
</div>
<span class="text-white me-3">Angemeldet als: {{ username }}</span>
<a href="/logout" class="btn btn-outline-light btn-sm">Abmelden</a>
</div>
</div>
</nav>
{% block content %}{% endblock %}
<!-- Session Warning Modal -->
<div id="session-warning" class="session-warning-modal" style="display: none;">
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>⚠️ Session läuft ab!</strong><br>
Ihre Session läuft in weniger als 1 Minute ab.<br>
<button type="button" class="btn btn-sm btn-success mt-2" onclick="extendSession()">
Session verlängern
</button>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
// Session-Timer Konfiguration
const SESSION_TIMEOUT = 5 * 60; // 5 Minuten in Sekunden
let timeRemaining = SESSION_TIMEOUT;
let timerInterval;
let warningShown = false;
let lastActivity = Date.now();
// Timer Display Update
function updateTimerDisplay() {
const minutes = Math.floor(timeRemaining / 60);
const seconds = timeRemaining % 60;
const display = `${minutes}:${seconds.toString().padStart(2, '0')}`;
document.getElementById('timer-display').textContent = display;
// Timer-Farbe ändern
const timerElement = document.getElementById('session-timer');
timerElement.className = timerElement.className.replace(/timer-\w+/, '');
if (timeRemaining <= 30) {
timerElement.classList.add('timer-critical');
} else if (timeRemaining <= 60) {
timerElement.classList.add('timer-danger');
if (!warningShown) {
showSessionWarning();
warningShown = true;
}
} else if (timeRemaining <= 120) {
timerElement.classList.add('timer-warning');
} else {
timerElement.classList.add('timer-normal');
warningShown = false;
hideSessionWarning();
}
}
// Session Warning anzeigen
function showSessionWarning() {
document.getElementById('session-warning').style.display = 'block';
}
// Session Warning verstecken
function hideSessionWarning() {
document.getElementById('session-warning').style.display = 'none';
}
// Timer zurücksetzen
function resetTimer() {
timeRemaining = SESSION_TIMEOUT;
lastActivity = Date.now();
updateTimerDisplay();
}
// Session verlängern
function extendSession() {
fetch('/heartbeat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'ok') {
resetTimer();
}
})
.catch(error => console.error('Heartbeat error:', error));
}
// Timer Countdown
function countdown() {
timeRemaining--;
updateTimerDisplay();
if (timeRemaining <= 0) {
clearInterval(timerInterval);
window.location.href = '/logout';
}
}
// Aktivitäts-Tracking
function trackActivity() {
const now = Date.now();
// Nur wenn mehr als 5 Sekunden seit letzter Aktivität
if (now - lastActivity > 5000) {
lastActivity = now;
extendSession(); // resetTimer() wird in extendSession nach erfolgreicher Response aufgerufen
}
}
// Event Listeners für Benutzeraktivität
document.addEventListener('click', trackActivity);
document.addEventListener('keypress', trackActivity);
document.addEventListener('mousemove', () => {
const now = Date.now();
// Mausbewegung nur alle 30 Sekunden tracken
if (now - lastActivity > 30000) {
trackActivity();
}
});
// AJAX Interceptor für automatische Session-Verlängerung
const originalFetch = window.fetch;
window.fetch = function(...args) {
// Nur für non-heartbeat requests den Timer verlängern
if (!args[0].includes('/heartbeat')) {
trackActivity();
}
return originalFetch.apply(this, args);
};
// Timer starten
timerInterval = setInterval(countdown, 1000);
updateTimerDisplay();
// Initial Heartbeat
extendSession();
// Client-side table sorting
document.addEventListener('DOMContentLoaded', function() {
// Initialize all sortable tables
const sortableTables = document.querySelectorAll('.sortable-table');
sortableTables.forEach(table => {
const headers = table.querySelectorAll('th.sortable');
headers.forEach((header, index) => {
header.addEventListener('click', function() {
sortTable(table, index, header);
});
});
});
});
function sortTable(table, columnIndex, header) {
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isNumeric = header.dataset.type === 'numeric';
const isDate = header.dataset.type === 'date';
// Determine sort direction
let direction = 'asc';
if (header.classList.contains('asc')) {
direction = 'desc';
}
// Remove all sort classes from headers
table.querySelectorAll('th.sortable').forEach(th => {
th.classList.remove('asc', 'desc');
});
// Add appropriate class to clicked header
header.classList.add(direction);
// Sort rows
rows.sort((a, b) => {
let aValue = a.cells[columnIndex].textContent.trim();
let bValue = b.cells[columnIndex].textContent.trim();
// Handle different data types
if (isNumeric) {
aValue = parseFloat(aValue.replace(/[^0-9.-]/g, '')) || 0;
bValue = parseFloat(bValue.replace(/[^0-9.-]/g, '')) || 0;
} else if (isDate) {
// Parse German date format (DD.MM.YYYY HH:MM)
aValue = parseGermanDate(aValue);
bValue = parseGermanDate(bValue);
} else {
// Text comparison with locale support for umlauts
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
}
if (aValue < bValue) return direction === 'asc' ? -1 : 1;
if (aValue > bValue) return direction === 'asc' ? 1 : -1;
return 0;
});
// Reorder rows in DOM
rows.forEach(row => tbody.appendChild(row));
}
function parseGermanDate(dateStr) {
// Handle DD.MM.YYYY HH:MM format
const parts = dateStr.match(/(\d{2})\.(\d{2})\.(\d{4})\s*(\d{2}:\d{2})?/);
if (parts) {
const [_, day, month, year, time] = parts;
const timeStr = time || '00:00';
return new Date(`${year}-${month}-${day}T${timeStr}`);
}
return new Date(0);
}
</script>
{% block extra_js %}{% endblock %}
</body>
</html>