Till Tomczak 2d33753b94 feat: Major updates to backend structure and security enhancements
- Removed `COMMON_ERRORS.md` file to streamline documentation.
- Added `Flask-Limiter` for rate limiting and `redis` for session management in `requirements.txt`.
- Expanded `ROADMAP.md` to include completed security features and planned enhancements for version 2.2.
- Enhanced `setup_myp.sh` for ultra-secure kiosk installation, including system hardening and security configurations.
- Updated `app.py` to integrate CSRF protection and improved logging setup.
- Refactored user model to include username and active status for better user management.
- Improved job scheduler with uptime tracking and task management features.
- Updated various templates for a more cohesive user interface and experience.
2025-05-25 20:33:38 +02:00

464 lines
22 KiB
HTML

{% extends "base.html" %}
{% block title %}Statistiken - MYP Platform{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Header -->
<div class="relative overflow-hidden rounded-2xl p-6 stats-card">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Statistiken</h1>
<p class="mt-2 text-slate-500 dark:text-slate-400">Übersicht über Systemleistung und Nutzungsstatistiken</p>
</div>
<div class="flex gap-4 mt-4 lg:mt-0">
<button onclick="refreshStats()" class="bg-black hover:bg-gray-800 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-6 py-2.5 rounded-xl transition-colors duration-300">
<svg class="h-5 w-5 mr-2 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Aktualisieren
</button>
<button onclick="exportStats()" class="bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-700 dark:text-white px-6 py-2.5 rounded-xl transition-colors duration-300 border border-slate-200 dark:border-slate-600">
<svg class="h-5 w-5 mr-2 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Exportieren
</button>
</div>
</div>
</div>
<!-- Overview Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<!-- Total Jobs -->
<div class="stats-card p-6" data-api-endpoint="/api/stats/total-jobs" data-counter>
<div class="absolute top-5 right-5 text-slate-900 dark:text-blue-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Gesamte Jobs</p>
{% if total_jobs is defined and total_jobs %}
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ total_jobs }}</p>
{% endif %}
</div>
<!-- Completed Jobs -->
<div class="stats-card p-6" data-api-endpoint="/api/stats/completed-jobs" data-counter>
<div class="absolute top-5 right-5 text-green-600 dark:text-green-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Abgeschlossene Jobs</p>
{% if completed_jobs is defined and completed_jobs %}
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ completed_jobs }}</p>
{% endif %}
</div>
<!-- Active Printers -->
<div class="stats-card p-6" data-api-endpoint="/api/stats/active-printers" data-counter>
<div class="absolute top-5 right-5 text-purple-600 dark:text-purple-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Aktive Drucker</p>
{% if active_printers is defined and active_printers %}
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ active_printers }}</p>
{% endif %}
</div>
<!-- Total Print Time -->
<div class="stats-card p-6" data-api-endpoint="/api/stats/print-time" data-counter>
<div class="absolute top-5 right-5 text-amber-600 dark:text-amber-400 text-3xl">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mt-2">Gesamte Druckzeit</p>
{% if total_print_time is defined and total_print_time %}
<p class="text-3xl md:text-4xl font-semibold text-slate-900 dark:text-white">{{ total_print_time }}</p>
{% endif %}
</div>
</div>
<!-- Charts and Detailed Stats -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Job Status Distribution -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Job-Status Verteilung</h2>
<div id="job-status-chart" class="h-64" data-api-endpoint="/api/stats/job-status" data-chart>
{% if job_status_data is defined and job_status_data and job_status_data|length > 0 %}
<!-- Recharts wird hier dynamisch gerendert -->
{% endif %}
</div>
</div>
<!-- Printer Usage -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Drucker-Nutzung</h2>
<div id="printer-usage-chart" class="h-64" data-api-endpoint="/api/stats/printer-usage" data-chart>
{% if printer_usage_data is defined and printer_usage_data and printer_usage_data|length > 0 %}
<!-- Recharts wird hier dynamisch gerendert -->
{% endif %}
</div>
</div>
</div>
<!-- Detailed Statistics Tables -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Recent Activity -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Letzte Aktivitäten</h2>
<div id="recent-activity" class="space-y-4" data-api-endpoint="/api/stats/activity" data-list>
{% if recent_activity is defined and recent_activity and recent_activity|length > 0 %}
{% for activity in recent_activity %}
<div class="border-l-4 border-black dark:border-blue-500 pl-4 py-3 bg-slate-50 dark:bg-slate-700/30 rounded-r-xl">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-slate-900 dark:text-white">{{ activity.description }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">{{ activity.timestamp|format_datetime }}</p>
</div>
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
<!-- Top Users -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Top Benutzer</h2>
<div id="top-users" class="space-y-4" data-api-endpoint="/api/stats/users" data-list>
{% if top_users is defined and top_users and top_users|length > 0 %}
{% for user in top_users %}
<div class="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
<div class="flex items-center">
<div class="mr-3 text-lg font-bold text-slate-900 dark:text-blue-400">#{{ loop.index }}</div>
<div>
<p class="text-sm font-medium text-slate-900 dark:text-white">{{ user.name }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">{{ user.email }}</p>
</div>
</div>
<div class="text-right">
<p class="text-lg font-bold text-slate-900 dark:text-white">{{ user.job_count }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">Jobs</p>
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
<!-- System Performance Metrics -->
<div class="stats-card p-6">
<h2 class="text-xl font-semibold text-slate-900 dark:text-white mb-6">Systemleistung</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Average Job Duration -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl" data-api-endpoint="/api/stats/job-duration" data-counter>
<svg class="h-8 w-8 mx-auto mb-3 text-slate-900 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Durchschnittliche Job-Dauer</p>
{% if avg_job_duration is defined and avg_job_duration %}
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ avg_job_duration }}</p>
{% endif %}
</div>
<!-- Success Rate -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl" data-api-endpoint="/api/stats/success-rate" data-counter>
<svg class="h-8 w-8 mx-auto mb-3 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">Erfolgsrate</p>
{% if success_rate is defined and success_rate %}
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ success_rate }}%</p>
{% endif %}
</div>
<!-- System Uptime -->
<div class="text-center p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl" data-api-endpoint="/api/stats/uptime" data-counter>
<svg class="h-8 w-8 mx-auto mb-3 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">System-Verfügbarkeit</p>
{% if system_uptime is defined and system_uptime %}
<p class="text-xl font-bold text-slate-900 dark:text-white">{{ system_uptime }}%</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- ApexCharts Integration für unsere Diagramme -->
<script>
document.addEventListener('DOMContentLoaded', function() {
loadStats();
// Theme wechsel Event-Listener
window.addEventListener('darkModeChanged', function(e) {
// Charts neu rendern mit passendem Farbschema
if (window.jobStatusChart) {
updateChartTheme(window.jobStatusChart, e.detail.isDark);
}
if (window.printerUsageChart) {
updateChartTheme(window.printerUsageChart, e.detail.isDark);
}
});
function loadStats() {
const counterElements = document.querySelectorAll('[data-counter]');
const chartElements = document.querySelectorAll('[data-chart]');
const listElements = document.querySelectorAll('[data-list]');
// Zähler laden
counterElements.forEach(el => {
const endpoint = el.getAttribute('data-api-endpoint');
if (endpoint) {
fetch(endpoint)
.then(response => response.json())
.then(data => {
el.querySelector('p:last-child').textContent = data.value || '0';
})
.catch(error => console.error('Error loading counter:', error));
}
});
// Diagramme laden (vereinfachte Implementierung)
chartElements.forEach(el => {
const endpoint = el.getAttribute('data-api-endpoint');
if (endpoint) {
fetch(endpoint)
.then(response => response.json())
.then(data => {
const chartId = el.id;
if (chartId === 'job-status-chart') {
renderJobStatusChart(el, data);
} else if (chartId === 'printer-usage-chart') {
renderPrinterUsageChart(el, data);
}
})
.catch(error => {
console.error(`Fehler beim Laden der Diagrammdaten von ${endpoint}:`, error);
el.innerHTML = '<div class="flex items-center justify-center h-full"><p class="text-red-500">Fehler beim Laden der Daten</p></div>';
});
}
});
// Listen laden
listElements.forEach(el => {
const endpoint = el.getAttribute('data-api-endpoint');
if (endpoint) {
fetch(endpoint)
.then(response => response.json())
.then(data => {
const listId = el.id;
if (listId === 'recent-activity') {
renderActivityList(el, data.activities || []);
} else if (listId === 'top-users') {
renderUsersList(el, data.users || []);
}
})
.catch(error => {
console.error(`Fehler beim Laden der Listendaten von ${endpoint}:`, error);
el.innerHTML = '<div class="p-4"><p class="text-red-500">Fehler beim Laden der Daten</p></div>';
});
}
});
}
function renderJobStatusChart(element, data) {
const chartData = data.data || [];
const labels = data.labels || ['Abgeschlossen', 'In Bearbeitung', 'Fehler', 'Wartend', 'Abgebrochen'];
const series = chartData.length > 0 ? chartData : [44, 55, 13, 33, 22]; // Fallback-Daten
const isDark = document.documentElement.classList.contains('dark');
const options = {
series: series,
chart: {
type: 'donut',
height: 240,
fontFamily: 'Inter, sans-serif',
foreColor: isDark ? '#94a3b8' : '#64748b',
},
labels: labels,
colors: isDark ? ['#10b981', '#3b82f6', '#ef4444', '#f59e0b', '#6b7280'] : ['#10b981', '#000000', '#ef4444', '#f59e0b', '#6b7280'],
plotOptions: {
pie: {
donut: {
size: '60%'
},
expandOnClick: true
}
},
legend: {
position: 'bottom'
},
tooltip: {
theme: isDark ? 'dark' : 'light'
}
};
// Element leeren und Chart erstellen
element.innerHTML = '';
window.jobStatusChart = new ApexCharts(element, options);
window.jobStatusChart.render();
}
function renderPrinterUsageChart(element, data) {
const printerNames = data.categories || ['Drucker 1', 'Drucker 2', 'Drucker 3', 'Drucker 4', 'Drucker 5'];
const usageData = data.series || [30, 40, 25, 50, 49]; // Fallback-Daten
const isDark = document.documentElement.classList.contains('dark');
const options = {
series: [{
name: 'Druckzeit (Stunden)',
data: usageData
}],
chart: {
type: 'bar',
height: 240,
fontFamily: 'Inter, sans-serif',
toolbar: {
show: false
},
foreColor: isDark ? '#94a3b8' : '#64748b',
},
plotOptions: {
bar: {
borderRadius: 4,
horizontal: false,
columnWidth: '60%',
}
},
dataLabels: {
enabled: false
},
colors: [isDark ? '#3b82f6' : '#000000'],
xaxis: {
categories: printerNames,
labels: {
style: {
fontSize: '12px'
}
}
},
yaxis: {
title: {
text: 'Stunden'
}
},
tooltip: {
theme: isDark ? 'dark' : 'light'
}
};
// Element leeren und Chart erstellen
element.innerHTML = '';
window.printerUsageChart = new ApexCharts(element, options);
window.printerUsageChart.render();
}
function renderActivityList(element, activities) {
if (!activities || activities.length === 0) {
element.innerHTML = '<div class="p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl"><p class="text-center text-slate-500 dark:text-slate-400">Keine Aktivitäten gefunden</p></div>';
return;
}
let html = '';
activities.forEach(activity => {
const timestamp = activity.timestamp ? new Date(activity.timestamp).toLocaleString('de-DE') : '';
html += `
<div class="border-l-4 border-black dark:border-blue-500 pl-4 py-3 bg-slate-50 dark:bg-slate-700/30 rounded-r-xl">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-slate-900 dark:text-white">${activity.description}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">${timestamp}</p>
</div>
</div>
</div>
`;
});
element.innerHTML = html;
}
function renderUsersList(element, users) {
if (!users || users.length === 0) {
element.innerHTML = '<div class="p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl"><p class="text-center text-slate-500 dark:text-slate-400">Keine Benutzer gefunden</p></div>';
return;
}
let html = '';
users.forEach((user, index) => {
html += `
<div class="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-700/30 rounded-xl">
<div class="flex items-center">
<div class="mr-3 text-lg font-bold text-slate-900 dark:text-blue-400">#${index + 1}</div>
<div>
<p class="text-sm font-medium text-slate-900 dark:text-white">${user.name}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">${user.email}</p>
</div>
</div>
<div class="text-right">
<p class="text-lg font-bold text-slate-900 dark:text-white">${user.job_count || 0}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">Jobs</p>
</div>
</div>
`;
});
element.innerHTML = html;
}
function updateChartTheme(chart, isDark) {
chart.updateOptions({
chart: {
foreColor: isDark ? '#94a3b8' : '#64748b'
},
tooltip: {
theme: isDark ? 'dark' : 'light'
},
colors: isDark ? ['#10b981', '#3b82f6', '#ef4444', '#f59e0b', '#6b7280'] : ['#10b981', '#000000', '#ef4444', '#f59e0b', '#6b7280']
});
}
// Statistiken neu laden
function refreshStats() {
// Feedback für den Benutzer
showToast('Statistiken werden aktualisiert...', 'info');
// Daten neu laden
loadStats();
// Erfolgsmeldung
setTimeout(() => {
showToast('Statistiken erfolgreich aktualisiert', 'success');
}, 1000);
}
// Statistiken exportieren
function exportStats() {
// Direkter Download vom API-Endpunkt
window.location.href = '/api/stats/export';
}
// Helper-Funktion für Toast-Benachrichtigungen
function showToast(message, type) {
if (window.showToast) {
window.showToast(message, type);
} else {
alert(message);
}
}
});
</script>
{% endblock %}