"Refactor database configuration and templates for improved maintainability (feat)"
This commit is contained in:
@@ -776,47 +776,527 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let statisticsData = {
|
||||
total_jobs: 0,
|
||||
active_printers: 0,
|
||||
success_rate: 98.5,
|
||||
uptime: 99.9
|
||||
};
|
||||
let animationInProgress = false;
|
||||
let lastUpdateTime = 0;
|
||||
let retryCount = 0;
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAY = 5000;
|
||||
const UPDATE_INTERVAL = 30000;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Lade Live-Statistiken
|
||||
loadStatistics();
|
||||
|
||||
// Alle 30 Sekunden aktualisieren
|
||||
setInterval(loadStatistics, 30000);
|
||||
initializeIndexPage();
|
||||
});
|
||||
|
||||
async function loadStatistics() {
|
||||
function initializeIndexPage() {
|
||||
// Initialize components
|
||||
loadStatistics();
|
||||
setupIntersectionObservers();
|
||||
setupProcessStepAnimations();
|
||||
setupErrorHandling();
|
||||
setupPerformanceOptimizations();
|
||||
setupAccessibilityFeatures();
|
||||
|
||||
// Set up update interval
|
||||
setInterval(loadStatisticsWithRetry, UPDATE_INTERVAL);
|
||||
|
||||
// Handle visibility change for performance
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// Setup smooth scrolling for anchor links
|
||||
setupSmoothScrolling();
|
||||
}
|
||||
|
||||
async function loadStatisticsWithRetry() {
|
||||
try {
|
||||
const response = await fetch('/api/statistics/public');
|
||||
await loadStatistics();
|
||||
retryCount = 0; // Reset retry count on success
|
||||
} catch (error) {
|
||||
console.warn('Statistics load failed, attempt', retryCount + 1);
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
retryCount++;
|
||||
setTimeout(loadStatisticsWithRetry, RETRY_DELAY);
|
||||
} else {
|
||||
console.error('Max retries reached for statistics loading');
|
||||
showStatisticsError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStatistics() {
|
||||
const now = Date.now();
|
||||
|
||||
// Prevent too frequent updates
|
||||
if (now - lastUpdateTime < 5000) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastUpdateTime = now;
|
||||
|
||||
try {
|
||||
// Show loading state for first load
|
||||
if (statisticsData.total_jobs === 0) {
|
||||
showLoadingSkeletons();
|
||||
}
|
||||
|
||||
// Try multiple endpoints for reliability
|
||||
let response;
|
||||
try {
|
||||
response = await fetchWithTimeout('/api/statistics/public', 5000);
|
||||
} catch (primaryError) {
|
||||
console.log('Primary statistics API failed, trying fallback');
|
||||
response = await fetchWithTimeout('/api/stats/public', 5000);
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const stats = await response.json();
|
||||
|
||||
// Animierte Zähler
|
||||
animateCounter('total-jobs', stats.total_jobs || 0);
|
||||
animateCounter('active-printers', stats.active_printers || 0);
|
||||
// Validate data
|
||||
const newData = {
|
||||
total_jobs: Math.max(0, parseInt(stats.total_jobs) || 0),
|
||||
active_printers: Math.max(0, parseInt(stats.active_printers) || 0),
|
||||
success_rate: Math.min(100, Math.max(0, parseFloat(stats.success_rate) || 98.5)),
|
||||
uptime: Math.min(100, Math.max(0, parseFloat(stats.uptime) || 99.9))
|
||||
};
|
||||
|
||||
// Statische Werte mit Animation
|
||||
animatePercentage('success-rate', stats.success_rate || 98.5);
|
||||
animatePercentage('uptime', stats.uptime || 99.9);
|
||||
// Update statistics with animation
|
||||
await updateStatisticsAnimated(newData);
|
||||
|
||||
// Update progress bars
|
||||
updateProgressBars(newData);
|
||||
|
||||
// Hide loading skeletons
|
||||
hideLoadingSkeletons();
|
||||
|
||||
// Update last successful fetch time
|
||||
updateLiveIndicator(true);
|
||||
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('Statistiken konnten nicht geladen werden');
|
||||
console.error('Error loading statistics:', error);
|
||||
updateLiveIndicator(false);
|
||||
|
||||
// Use fallback values for first load
|
||||
if (statisticsData.total_jobs === 0) {
|
||||
const fallbackData = {
|
||||
total_jobs: 1250,
|
||||
active_printers: 12,
|
||||
success_rate: 98.5,
|
||||
uptime: 99.9
|
||||
};
|
||||
await updateStatisticsAnimated(fallbackData);
|
||||
updateProgressBars(fallbackData);
|
||||
hideLoadingSkeletons();
|
||||
|
||||
showStatisticsMessage('Demo-Daten werden angezeigt', 'warning');
|
||||
}
|
||||
|
||||
throw error; // Re-throw for retry logic
|
||||
}
|
||||
}
|
||||
|
||||
function animateCounter(elementId, targetValue) {
|
||||
const element = document.getElementById(elementId);
|
||||
const currentValue = parseInt(element.textContent) || 0;
|
||||
const increment = Math.ceil((targetValue - currentValue) / 20);
|
||||
async function updateStatisticsAnimated(newData) {
|
||||
if (animationInProgress) return;
|
||||
|
||||
if (currentValue < targetValue) {
|
||||
element.textContent = Math.min(currentValue + increment, targetValue);
|
||||
setTimeout(() => animateCounter(elementId, targetValue), 50);
|
||||
animationInProgress = true;
|
||||
|
||||
try {
|
||||
// Animate counters
|
||||
await Promise.all([
|
||||
animateCounter('total-jobs', statisticsData.total_jobs, newData.total_jobs, 2000),
|
||||
animateCounter('active-printers', statisticsData.active_printers, newData.active_printers, 1500),
|
||||
animatePercentage('success-rate', statisticsData.success_rate, newData.success_rate, 1800),
|
||||
animatePercentage('uptime', statisticsData.uptime, newData.uptime, 1600)
|
||||
]);
|
||||
|
||||
// Update stored data
|
||||
statisticsData = { ...newData };
|
||||
|
||||
} finally {
|
||||
animationInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
function animatePercentage(elementId, targetValue) {
|
||||
const element = document.getElementById(elementId);
|
||||
element.textContent = targetValue.toFixed(1) + '%';
|
||||
function animateCounter(elementId, fromValue, toValue, duration = 2000) {
|
||||
return new Promise((resolve) => {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
const difference = toValue - fromValue;
|
||||
|
||||
function updateCounter(currentTime) {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// Use easing function for smooth animation
|
||||
const easedProgress = easeOutQuart(progress);
|
||||
const currentValue = Math.round(fromValue + (difference * easedProgress));
|
||||
|
||||
element.textContent = formatNumber(currentValue);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateCounter);
|
||||
});
|
||||
}
|
||||
|
||||
function animatePercentage(elementId, fromValue, toValue, duration = 1500) {
|
||||
return new Promise((resolve) => {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
const difference = toValue - fromValue;
|
||||
|
||||
function updatePercentage(currentTime) {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
const easedProgress = easeOutQuart(progress);
|
||||
const currentValue = fromValue + (difference * easedProgress);
|
||||
|
||||
element.textContent = currentValue.toFixed(1) + '%';
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updatePercentage);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updatePercentage);
|
||||
});
|
||||
}
|
||||
|
||||
function updateProgressBars(data) {
|
||||
const progressBars = {
|
||||
'jobs-progress': Math.min(100, (data.total_jobs / 1500) * 100),
|
||||
'printers-progress': Math.min(100, (data.active_printers / 15) * 100),
|
||||
'success-progress': data.success_rate,
|
||||
'uptime-progress': data.uptime
|
||||
};
|
||||
|
||||
Object.entries(progressBars).forEach(([id, width]) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.style.width = `${width}%`;
|
||||
element.style.transition = 'width 1s ease-out';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupIntersectionObservers() {
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animate-in');
|
||||
|
||||
// Trigger specific animations based on element
|
||||
if (entry.target.classList.contains('mercedes-feature-card')) {
|
||||
animateFeatureCard(entry.target);
|
||||
} else if (entry.target.classList.contains('stat-card')) {
|
||||
animateStatCard(entry.target);
|
||||
} else if (entry.target.classList.contains('process-step')) {
|
||||
animateProcessStep(entry.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe elements
|
||||
document.querySelectorAll('.mercedes-feature-card, .stat-card, .process-step').forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
function animateFeatureCard(element) {
|
||||
element.style.transform = 'translateY(20px)';
|
||||
element.style.opacity = '0';
|
||||
element.style.transition = 'all 0.6s ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'translateY(0)';
|
||||
element.style.opacity = '1';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function animateStatCard(element) {
|
||||
const delay = Array.from(element.parentNode.children).indexOf(element) * 150;
|
||||
|
||||
element.style.transform = 'scale(0.8) translateY(30px)';
|
||||
element.style.opacity = '0';
|
||||
element.style.transition = 'all 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
|
||||
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'scale(1) translateY(0)';
|
||||
element.style.opacity = '1';
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function animateProcessStep(element) {
|
||||
const delay = Array.from(element.parentNode.children).indexOf(element) * 200;
|
||||
|
||||
element.style.transform = 'translateY(40px)';
|
||||
element.style.opacity = '0';
|
||||
element.style.transition = 'all 0.7s ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'translateY(0)';
|
||||
element.style.opacity = '1';
|
||||
|
||||
// Animate step number
|
||||
const stepNumber = element.querySelector('.process-step-number');
|
||||
if (stepNumber) {
|
||||
stepNumber.style.animation = 'bounce 0.6s ease-out';
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function setupProcessStepAnimations() {
|
||||
const processSteps = document.querySelectorAll('.process-step');
|
||||
|
||||
processSteps.forEach((step, index) => {
|
||||
step.addEventListener('mouseenter', () => {
|
||||
const stepNumber = step.querySelector('.process-step-number');
|
||||
if (stepNumber) {
|
||||
stepNumber.style.transform = 'scale(1.1) rotate(5deg)';
|
||||
}
|
||||
});
|
||||
|
||||
step.addEventListener('mouseleave', () => {
|
||||
const stepNumber = step.querySelector('.process-step-number');
|
||||
if (stepNumber) {
|
||||
stepNumber.style.transform = 'scale(1) rotate(0deg)';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupSmoothScrolling() {
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
// Pause animations and updates when tab is not visible
|
||||
console.log('Page hidden, pausing updates');
|
||||
} else {
|
||||
// Resume updates when tab becomes visible
|
||||
console.log('Page visible, resuming updates');
|
||||
loadStatistics();
|
||||
}
|
||||
}
|
||||
|
||||
function setupErrorHandling() {
|
||||
window.addEventListener('error', (event) => {
|
||||
console.error('JavaScript error:', event.error);
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('Unhandled promise rejection:', event.reason);
|
||||
});
|
||||
}
|
||||
|
||||
function setupPerformanceOptimizations() {
|
||||
// Preload critical resources
|
||||
const preloadLinks = [
|
||||
'/api/statistics/public',
|
||||
'/static/icons/iso-27001.svg',
|
||||
'/static/icons/mercedes-star.svg',
|
||||
'/static/icons/gdpr.svg'
|
||||
];
|
||||
|
||||
preloadLinks.forEach(href => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'prefetch';
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
function setupAccessibilityFeatures() {
|
||||
// Add ARIA labels for statistics
|
||||
const statElements = document.querySelectorAll('.stat-number');
|
||||
statElements.forEach(element => {
|
||||
element.setAttribute('aria-live', 'polite');
|
||||
element.setAttribute('aria-atomic', 'true');
|
||||
});
|
||||
|
||||
// Add keyboard navigation for interactive elements
|
||||
document.querySelectorAll('.mercedes-feature-card').forEach(card => {
|
||||
card.setAttribute('tabindex', '0');
|
||||
card.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
this.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function fetchWithTimeout(url, timeout = 5000) {
|
||||
return Promise.race([
|
||||
fetch(url),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Request timeout')), timeout)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - (--t) * t * t * t;
|
||||
}
|
||||
|
||||
function showLoadingSkeletons() {
|
||||
document.querySelectorAll('.stat-number').forEach(element => {
|
||||
element.classList.add('loading-skeleton');
|
||||
element.textContent = '---';
|
||||
});
|
||||
}
|
||||
|
||||
function hideLoadingSkeletons() {
|
||||
document.querySelectorAll('.stat-number').forEach(element => {
|
||||
element.classList.remove('loading-skeleton');
|
||||
});
|
||||
}
|
||||
|
||||
function updateLiveIndicator(isConnected) {
|
||||
const indicator = document.querySelector('.animate-pulse');
|
||||
if (indicator) {
|
||||
if (isConnected) {
|
||||
indicator.classList.add('animate-pulse');
|
||||
indicator.classList.remove('opacity-50');
|
||||
} else {
|
||||
indicator.classList.remove('animate-pulse');
|
||||
indicator.classList.add('opacity-50');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showStatisticsError() {
|
||||
const errorMessage = document.createElement('div');
|
||||
errorMessage.className = 'fixed top-4 right-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded z-50';
|
||||
errorMessage.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Statistiken konnten nicht geladen werden</span>
|
||||
<button onclick="this.parentElement.parentElement.remove()" class="ml-4">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(errorMessage);
|
||||
|
||||
setTimeout(() => {
|
||||
errorMessage.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showStatisticsMessage(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
const bgColor = type === 'warning' ? 'bg-yellow-100 border-yellow-400 text-yellow-700' : 'bg-blue-100 border-blue-400 text-blue-700';
|
||||
|
||||
toast.className = `fixed top-4 right-4 ${bgColor} px-4 py-3 rounded border z-50`;
|
||||
toast.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<span>${message}</span>
|
||||
<button onclick="this.parentElement.parentElement.remove()" class="ml-4">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Add bounce animation keyframe
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes bounce {
|
||||
0%, 20%, 53%, 80%, 100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
40%, 43% {
|
||||
transform: translate3d(0, -10px, 0);
|
||||
}
|
||||
70% {
|
||||
transform: translate3d(0, -5px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate3d(0, -2px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user