"Refactor database configuration and templates for improved maintainability (feat)"

This commit is contained in:
2025-05-29 16:46:01 +02:00
parent 0eca19e3ce
commit 259bf3f19d
5 changed files with 1247 additions and 183 deletions

View File

@@ -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 %}