386 Zeilen
13 KiB
HTML
386 Zeilen
13 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Live Dashboard{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.stats-card {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
text-align: center;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.stats-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.stats-number {
|
|
font-size: 2.5rem;
|
|
font-weight: bold;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.stats-label {
|
|
color: #6c757d;
|
|
font-size: 0.9rem;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.session-card {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-bottom: 10px;
|
|
border-left: 4px solid #28a745;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.session-card:hover {
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.activity-indicator {
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
margin-right: 5px;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.activity-active {
|
|
background-color: #28a745;
|
|
}
|
|
|
|
.activity-recent {
|
|
background-color: #ffc107;
|
|
}
|
|
|
|
.activity-inactive {
|
|
background-color: #6c757d;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
.geo-info {
|
|
font-size: 0.85rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.device-info {
|
|
background: #f8f9fa;
|
|
padding: 5px 10px;
|
|
border-radius: 15px;
|
|
font-size: 0.85rem;
|
|
display: inline-block;
|
|
margin: 2px;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<h2><i class="bi bi-activity"></i> Live Dashboard</h2>
|
|
<p class="text-muted">Echtzeit-Übersicht der aktiven Kunden-Sessions</p>
|
|
</div>
|
|
<div class="col-auto">
|
|
<span class="text-muted">Auto-Refresh: <span id="refresh-countdown">30</span>s</span>
|
|
<button class="btn btn-sm btn-outline-primary ms-2" onclick="refreshData()">
|
|
<i class="bi bi-arrow-clockwise"></i> Jetzt aktualisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-4">
|
|
<div class="stats-card">
|
|
<i class="bi bi-people-fill text-primary" style="font-size: 2rem;"></i>
|
|
<div class="stats-number text-primary">{{ stats.active_licenses|default(0) }}</div>
|
|
<div class="stats-label">Aktive Kunden</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stats-card">
|
|
<i class="bi bi-laptop text-success" style="font-size: 2rem;"></i>
|
|
<div class="stats-number text-success">{{ stats.active_devices|default(0) }}</div>
|
|
<div class="stats-label">Aktive Geräte</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stats-card">
|
|
<i class="bi bi-speedometer2 text-info" style="font-size: 2rem;"></i>
|
|
<div class="stats-number text-info" id="validations-per-minute">0</div>
|
|
<div class="stats-label">Validierungen/Min</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity Timeline Chart -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Aktivität (letzte 60 Minuten)</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<canvas id="activityChart" height="80"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Sessions -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0"><i class="bi bi-people"></i> Aktive Kunden-Sessions</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="active-sessions-container">
|
|
{% for session in active_sessions %}
|
|
<div class="session-card">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-4">
|
|
<h6 class="mb-1">
|
|
<span class="activity-indicator activity-active"></span>
|
|
{{ session.company_name }}
|
|
</h6>
|
|
<small class="text-muted">{{ session.contact_person }}</small>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="mb-1">
|
|
<i class="bi bi-key"></i> {{ session.license_key[:8] }}...
|
|
</div>
|
|
<div class="device-info">
|
|
<i class="bi bi-laptop"></i> {{ session.active_devices }} Gerät(e)
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="geo-info">
|
|
<i class="bi bi-geo-alt"></i> {{ session.ip_address }}
|
|
<div><small>Hardware: {{ session.hardware_id[:12] }}...</small></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2 text-end">
|
|
<div class="text-muted">
|
|
<i class="bi bi-clock"></i>
|
|
<span class="last-activity" data-timestamp="{{ session.last_activity }}">
|
|
vor wenigen Sekunden
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center text-muted py-5">
|
|
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
|
<p>Keine aktiven Sessions in den letzten 5 Minuten</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden validation timeline data for chart -->
|
|
<script id="validation-timeline-data" type="application/json">
|
|
{{ validation_timeline|tojson }}
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js"></script>
|
|
<script>
|
|
let activityChart;
|
|
let refreshInterval;
|
|
let refreshCountdown = 30;
|
|
|
|
// Initialize activity chart
|
|
function initActivityChart() {
|
|
const ctx = document.getElementById('activityChart').getContext('2d');
|
|
const timelineData = JSON.parse(document.getElementById('validation-timeline-data').textContent);
|
|
|
|
// Prepare data for chart
|
|
const labels = timelineData.map(item => {
|
|
const date = new Date(item.minute);
|
|
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
|
}).reverse();
|
|
|
|
const data = timelineData.map(item => item.validations).reverse();
|
|
|
|
activityChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Validierungen',
|
|
data: data,
|
|
borderColor: 'rgb(75, 192, 192)',
|
|
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
|
tension: 0.1,
|
|
fill: true
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update last activity times
|
|
function updateActivityTimes() {
|
|
document.querySelectorAll('.last-activity').forEach(el => {
|
|
const timestamp = new Date(el.dataset.timestamp);
|
|
const now = new Date();
|
|
const seconds = Math.floor((now - timestamp) / 1000);
|
|
|
|
if (seconds < 60) {
|
|
el.textContent = 'vor wenigen Sekunden';
|
|
} else if (seconds < 3600) {
|
|
const minutes = Math.floor(seconds / 60);
|
|
el.textContent = `vor ${minutes} Minute${minutes > 1 ? 'n' : ''}`;
|
|
} else {
|
|
const hours = Math.floor(seconds / 3600);
|
|
el.textContent = `vor ${hours} Stunde${hours > 1 ? 'n' : ''}`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Refresh data via AJAX
|
|
async function refreshData() {
|
|
try {
|
|
// Get live stats
|
|
const statsResponse = await fetch('/monitoring/api/live-stats');
|
|
const stats = await statsResponse.json();
|
|
|
|
// Update stats cards
|
|
document.querySelector('.stats-number.text-primary').textContent = stats.active_licenses || 0;
|
|
document.querySelector('.stats-number.text-success').textContent = stats.active_devices || 0;
|
|
document.getElementById('validations-per-minute').textContent = stats.validations_last_minute || 0;
|
|
|
|
// Get active sessions
|
|
const sessionsResponse = await fetch('/monitoring/api/active-sessions');
|
|
const sessions = await sessionsResponse.json();
|
|
|
|
// Update sessions display
|
|
updateSessionsDisplay(sessions);
|
|
|
|
// Reset countdown
|
|
refreshCountdown = 30;
|
|
|
|
} catch (error) {
|
|
console.error('Error refreshing data:', error);
|
|
}
|
|
}
|
|
|
|
// Update sessions display
|
|
function updateSessionsDisplay(sessions) {
|
|
const container = document.getElementById('active-sessions-container');
|
|
|
|
if (sessions.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="text-center text-muted py-5">
|
|
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
|
<p>Keine aktiven Sessions in den letzten 5 Minuten</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const sessionsHtml = sessions.map(session => {
|
|
const secondsAgo = Math.floor(session.seconds_ago);
|
|
let activityClass = 'activity-active';
|
|
if (secondsAgo > 120) activityClass = 'activity-recent';
|
|
if (secondsAgo > 240) activityClass = 'activity-inactive';
|
|
|
|
return `
|
|
<div class="session-card">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-4">
|
|
<h6 class="mb-1">
|
|
<span class="activity-indicator ${activityClass}"></span>
|
|
${session.company_name}
|
|
</h6>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="mb-1">
|
|
<i class="bi bi-key"></i> ${session.license_key.substring(0, 8)}...
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="geo-info">
|
|
<i class="bi bi-geo-alt"></i> ${session.ip_address}
|
|
<div><small>Hardware: ${session.hardware_id.substring(0, 12)}...</small></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2 text-end">
|
|
<div class="text-muted">
|
|
<i class="bi bi-clock"></i>
|
|
vor ${formatSecondsAgo(secondsAgo)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
container.innerHTML = sessionsHtml;
|
|
}
|
|
|
|
// Format seconds ago
|
|
function formatSecondsAgo(seconds) {
|
|
if (seconds < 60) return 'wenigen Sekunden';
|
|
if (seconds < 3600) {
|
|
const minutes = Math.floor(seconds / 60);
|
|
return `${minutes} Minute${minutes > 1 ? 'n' : ''}`;
|
|
}
|
|
const hours = Math.floor(seconds / 3600);
|
|
return `${hours} Stunde${hours > 1 ? 'n' : ''}`;
|
|
}
|
|
|
|
// Countdown timer
|
|
function updateCountdown() {
|
|
refreshCountdown--;
|
|
document.getElementById('refresh-countdown').textContent = refreshCountdown;
|
|
|
|
if (refreshCountdown <= 0) {
|
|
refreshData();
|
|
}
|
|
}
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initActivityChart();
|
|
updateActivityTimes();
|
|
|
|
// Set up auto-refresh
|
|
refreshInterval = setInterval(() => {
|
|
updateCountdown();
|
|
updateActivityTimes();
|
|
}, 1000);
|
|
});
|
|
|
|
// Clean up on page leave
|
|
window.addEventListener('beforeunload', function() {
|
|
if (refreshInterval) {
|
|
clearInterval(refreshInterval);
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |