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.
This commit is contained in:
@@ -1,461 +1,488 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - MYP Platform{% endblock %}
|
||||
{% block title %}Dashboard - Mercedes-Benz MYP Platform{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Professional Mercedes-Benz Dashboard Styles */
|
||||
.mb-dashboard-card {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .mb-dashboard-card {
|
||||
background: var(--mb-black, #000000); /* Schwarzer Hintergrund im Dark Mode */
|
||||
border-color: var(--border-primary, #334155);
|
||||
}
|
||||
|
||||
.mb-dashboard-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.dark .mb-dashboard-card:hover {
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.mb-stat-card {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e6f2ff 100%);
|
||||
color: #0f172a;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dark .mb-stat-card {
|
||||
background: linear-gradient(135deg, #000000 0%, #111827 100%); /* Mercedes Schwarz */
|
||||
color: var(--text-primary, #f8fafc);
|
||||
}
|
||||
|
||||
.mb-stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(0, 114, 206, 0.1) 0%, transparent 70%);
|
||||
animation: pulse-glow 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dark .mb-stat-card::before {
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.05) 0%, transparent 70%); /* Subtle glow for dark */
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { opacity: 0.5; transform: scale(1); }
|
||||
50% { opacity: 0.8; transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.mb-stat-icon {
|
||||
background: rgba(0, 0, 0, 0.1); /* Schwarz statt Blau */
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .mb-stat-icon {
|
||||
background: rgba(255, 255, 255, 0.1); /* Lighter icon background for contrast */
|
||||
}
|
||||
|
||||
.mb-stat-card:hover .mb-stat-icon {
|
||||
transform: scale(1.1);
|
||||
background: rgba(0, 0, 0, 0.2); /* Schwarz statt Blau */
|
||||
}
|
||||
|
||||
.dark .mb-stat-card:hover .mb-stat-icon {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.mb-status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mb-status-indicator::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
animation: status-pulse 2s infinite;
|
||||
}
|
||||
|
||||
.mb-status-online {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.mb-status-online::after {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.mb-status-busy {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.mb-status-busy::after {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.mb-status-offline {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.mb-status-offline::after {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.mb-status-idle {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.mb-status-idle::after {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
@keyframes status-pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.5); opacity: 0.5; }
|
||||
100% { transform: scale(2); opacity: 0; }
|
||||
}
|
||||
|
||||
.mb-progress-container {
|
||||
background: #f3f4f6;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dark .mb-progress-container {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.mb-progress-bar {
|
||||
background: linear-gradient(90deg, #000000 0%, #333333 100%); /* Schwarz statt Blau */
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .mb-progress-bar {
|
||||
background: linear-gradient(90deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||
}
|
||||
|
||||
.mb-progress-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
animation: progress-shine 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes progress-shine {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.mb-activity-item {
|
||||
border-left: 4px solid #000000; /* Schwarz statt Blau */
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.05) 0%, transparent 100%); /* Schwarz statt Blau */
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.dark .mb-activity-item {
|
||||
border-left-color: #f3f4f6;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.02) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
/* Light/Dark Mode compatible cards */
|
||||
.glass-card-light {
|
||||
@apply bg-white/70 dark:bg-black/80 backdrop-blur-lg border border-gray-200/80 dark:border-slate-700/30 rounded-xl shadow-xl transition-all duration-300;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark .glass-card-light {
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-slate-800 dark:text-white text-xl font-semibold mb-4;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@apply text-slate-900 dark:text-white font-bold text-2xl;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@apply text-slate-500 dark:text-slate-400 text-sm;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-mercedes-black">Dashboard</h1>
|
||||
<p class="mt-2 text-mercedes-gray">Willkommen zurück! Hier ist Ihre Übersicht.</p>
|
||||
<div class="space-y-8">
|
||||
<!-- Dashboard Header Card -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="w-16 h-16 flex-shrink-0">
|
||||
<svg class="w-full h-full text-slate-900 dark:text-white" fill="currentColor" viewBox="0 0 80 80" aria-hidden="true">
|
||||
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
|
||||
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
|
||||
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
|
||||
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
|
||||
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
|
||||
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Dashboard</h1>
|
||||
<p class="text-slate-500 dark:text-slate-400 mt-1">Übersicht über Ihre 3D-Druck Aktivitäten</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<button onclick="refreshDashboard()" class="bg-mercedes-blue hover:bg-blue-700 text-white px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button id="refreshDashboard"
|
||||
class="btn-secondary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" 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
|
||||
<span>Aktualisieren</span>
|
||||
</button>
|
||||
<a href="/jobs/new" class="bg-mercedes-green hover:bg-green-700 text-white px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<a href="{{ url_for('jobs_page') }}"
|
||||
class="btn-primary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Neuer Job
|
||||
<span>Neuer Auftrag</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Active Jobs -->
|
||||
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-mercedes-green p-3 rounded-full">
|
||||
<svg class="h-6 w-6 text-white" 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>
|
||||
</div>
|
||||
<!-- Stats Overview Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Active Jobs Card -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Aktive Aufträge</h3>
|
||||
<div class="stat-value">{{ active_jobs_count }}</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-mercedes-gray">Aktive Jobs</p>
|
||||
<p class="text-2xl font-bold text-mercedes-black" id="active-jobs-count">-</p>
|
||||
<div class="mb-stat-icon text-slate-900 dark:text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 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 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduled Jobs -->
|
||||
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-mercedes-blue p-3 rounded-full">
|
||||
<svg class="h-6 w-6 text-white" 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>
|
||||
<!-- Available Printers Card -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Verfügbare Drucker</h3>
|
||||
<div class="stat-value">{{ available_printers_count }}</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-mercedes-gray">Geplante Jobs</p>
|
||||
<p class="text-2xl font-bold text-mercedes-black" id="scheduled-jobs-count">-</p>
|
||||
<div class="mb-stat-icon text-green-600 dark:text-green-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Printers -->
|
||||
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-mercedes-yellow p-3 rounded-full">
|
||||
<svg class="h-6 w-6 text-mercedes-black" 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>
|
||||
<!-- Total Jobs Card -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Aufträge (gesamt)</h3>
|
||||
<div class="stat-value">{{ total_jobs_count }}</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-mercedes-gray">Verfügbare Drucker</p>
|
||||
<p class="text-2xl font-bold text-mercedes-black" id="available-printers-count">-</p>
|
||||
<div class="mb-stat-icon text-purple-600 dark:text-purple-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Print Time -->
|
||||
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-mercedes-silver p-3 rounded-full">
|
||||
<svg class="h-6 w-6 text-mercedes-black" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Success Rate Card -->
|
||||
<div class="dashboard-card p-6">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h3 class="stat-label">Erfolgsrate</h3>
|
||||
<div class="stat-value">{{ success_rate }}%</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-mercedes-gray">Gesamte Druckzeit</p>
|
||||
<p class="text-2xl font-bold text-mercedes-black" id="total-print-time">-</p>
|
||||
<div class="mb-stat-icon text-amber-600 dark:text-amber-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Recent Jobs -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-mercedes-black">Aktuelle Jobs</h2>
|
||||
<a href="/jobs" class="text-mercedes-blue hover:text-blue-700 text-sm font-medium transition-colors duration-200">
|
||||
Alle anzeigen →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="recent-jobs" class="space-y-4">
|
||||
<!-- Jobs will be loaded here -->
|
||||
<div class="text-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-mercedes-blue mx-auto"></div>
|
||||
<p class="mt-2 text-mercedes-gray">Lade Jobs...</p>
|
||||
<!-- Active Jobs Section -->
|
||||
<div class="dashboard-card p-6">
|
||||
<h2 class="section-title">Aktuelle Druckaufträge</h2>
|
||||
<div class="overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
|
||||
<thead>
|
||||
<tr class="text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
|
||||
<th class="px-6 py-3">Status</th>
|
||||
<th class="px-6 py-3">Auftrag</th>
|
||||
<th class="px-6 py-3">Drucker</th>
|
||||
<th class="px-6 py-3">Startzeit</th>
|
||||
<th class="px-6 py-3">Fortschritt</th>
|
||||
<th class="px-6 py-3">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
|
||||
{% if active_jobs and active_jobs|length > 0 %}
|
||||
{% for job in active_jobs %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="mb-status-indicator {{ job.status_class }}"></div>
|
||||
<span class="ml-2 text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{{ job.status_text }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ job.name }}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">{{ job.file_name }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-slate-700 dark:text-slate-300">{{ job.printer }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-slate-700 dark:text-slate-300">{{ job.start_time }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="mb-progress-container">
|
||||
<div class="mb-progress-bar" style="width: {{ job.progress }}%"></div>
|
||||
</div>
|
||||
<div class="text-xs text-right mt-1 text-slate-500 dark:text-slate-400">{{ job.progress }}%</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<a href="{{ url_for('job_detail', job_id=job.id) }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 font-medium">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-8 text-center">
|
||||
<div class="text-slate-500 dark:text-slate-400">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 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 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine aktiven Druckaufträge</p>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm">Starten Sie einen neuen Druckauftrag, um ihn hier zu sehen.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 text-right">
|
||||
<a href="{{ url_for('jobs_page') }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 text-sm font-medium">
|
||||
Alle Druckaufträge anzeigen →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Row: Printer Status and Recent Activity -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Available Printers -->
|
||||
<div class="dashboard-card p-6">
|
||||
<h2 class="section-title">Druckerstatus</h2>
|
||||
<div class="space-y-4">
|
||||
{% if printers and printers|length > 0 %}
|
||||
{% for printer in printers %}
|
||||
<div class="flex items-center justify-between p-4 rounded-xl bg-gray-50 dark:bg-slate-700/30">
|
||||
<div class="flex items-center">
|
||||
<div class="mb-status-indicator {{ printer.status_class }} mr-3"></div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ printer.name }}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">{{ printer.model }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ printer.status_text }}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">{{ printer.location }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
</svg>
|
||||
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine Drucker gefunden</p>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm">Prüfen Sie die Verbindung oder fügen Sie Drucker hinzu.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-4 text-right">
|
||||
<a href="{{ url_for('printers_page') }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 text-sm font-medium">
|
||||
Alle Drucker anzeigen →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions & System Status -->
|
||||
<div class="space-y-6">
|
||||
<!-- Quick Actions -->
|
||||
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
|
||||
<h2 class="text-xl font-bold text-mercedes-black mb-6">Schnellaktionen</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<a href="/jobs/new" class="block w-full bg-mercedes-green hover:bg-green-700 text-white text-center py-3 px-4 rounded-lg mercedes-button transition-all duration-200">
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Neuen Job erstellen
|
||||
</a>
|
||||
|
||||
<a href="/printers" class="block w-full bg-mercedes-blue hover:bg-blue-700 text-white text-center py-3 px-4 rounded-lg mercedes-button transition-all duration-200">
|
||||
<svg class="h-5 w-5 inline mr-2" 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>
|
||||
Drucker verwalten
|
||||
</a>
|
||||
|
||||
<a href="/stats" class="block w-full bg-mercedes-silver hover:bg-gray-400 text-mercedes-black text-center py-3 px-4 rounded-lg mercedes-button transition-all duration-200">
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
Statistiken anzeigen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Status -->
|
||||
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
|
||||
<h2 class="text-xl font-bold text-mercedes-black mb-6">Systemstatus</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Scheduler Status -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-mercedes-gray">Job-Scheduler</span>
|
||||
<div class="flex items-center">
|
||||
<div id="scheduler-status" class="h-3 w-3 rounded-full bg-mercedes-gray mr-2"></div>
|
||||
<span id="scheduler-text" class="text-sm text-mercedes-gray">Prüfe...</span>
|
||||
<!-- Recent Activity -->
|
||||
<div class="dashboard-card p-6">
|
||||
<h2 class="section-title">Letzte Aktivitäten</h2>
|
||||
<div class="space-y-3">
|
||||
{% if activities and activities|length > 0 %}
|
||||
{% for activity in activities %}
|
||||
<div class="mb-activity-item pl-4 py-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-slate-700 dark:text-slate-300">{{ activity.description }}</p>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">{{ activity.time }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database Status -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-mercedes-gray">Datenbank</span>
|
||||
<div class="flex items-center">
|
||||
<div class="h-3 w-3 rounded-full bg-mercedes-green mr-2"></div>
|
||||
<span class="text-sm text-mercedes-green">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Status -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-mercedes-gray">API</span>
|
||||
<div class="flex items-center">
|
||||
<div class="h-3 w-3 rounded-full bg-mercedes-green mr-2"></div>
|
||||
<span class="text-sm text-mercedes-green">Verfügbar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
<div class="mt-6 pt-4 border-t border-mercedes-silver">
|
||||
<a href="/admin" class="block w-full bg-mercedes-yellow hover:bg-yellow-500 text-mercedes-black text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200 text-sm font-medium">
|
||||
<svg class="h-4 w-4 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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>
|
||||
Admin-Panel
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-slate-700 dark:text-slate-300 font-medium mb-1">Keine Aktivitäten</p>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm">Ihre Aktivitäten werden hier angezeigt.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
|
||||
<h2 class="text-xl font-bold text-mercedes-black mb-6">Letzte Aktivitäten</h2>
|
||||
|
||||
<div id="recent-activity" class="space-y-3">
|
||||
<!-- Activity will be loaded here -->
|
||||
<div class="text-center py-4">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-mercedes-blue mx-auto"></div>
|
||||
<p class="mt-2 text-sm text-mercedes-gray">Lade Aktivitäten...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Dashboard data
|
||||
let dashboardData = {
|
||||
stats: {},
|
||||
jobs: [],
|
||||
printers: [],
|
||||
schedulerStatus: false
|
||||
};
|
||||
|
||||
// Load dashboard data
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
// Load stats
|
||||
const statsResponse = await apiCall('/api/stats');
|
||||
dashboardData.stats = statsResponse;
|
||||
|
||||
// Load jobs
|
||||
const jobsResponse = await apiCall('/api/jobs');
|
||||
dashboardData.jobs = jobsResponse.jobs || [];
|
||||
|
||||
// Load printers
|
||||
const printersResponse = await apiCall('/api/printers');
|
||||
dashboardData.printers = printersResponse.printers || [];
|
||||
|
||||
// Load scheduler status (try to load, will fail if not admin)
|
||||
try {
|
||||
const schedulerResponse = await apiCall('/api/scheduler/status');
|
||||
dashboardData.schedulerStatus = schedulerResponse.running;
|
||||
} catch (error) {
|
||||
console.log('Scheduler status not available (not admin or error)');
|
||||
dashboardData.schedulerStatus = false;
|
||||
}
|
||||
|
||||
updateDashboard();
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
showFlashMessage('Fehler beim Laden der Dashboard-Daten', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Update dashboard display
|
||||
function updateDashboard() {
|
||||
updateStats();
|
||||
updateRecentJobs();
|
||||
updateSystemStatus();
|
||||
updateRecentActivity();
|
||||
}
|
||||
|
||||
// Update stats cards
|
||||
function updateStats() {
|
||||
const stats = dashboardData.stats;
|
||||
const jobs = dashboardData.jobs;
|
||||
const printers = dashboardData.printers;
|
||||
|
||||
// Active jobs
|
||||
const activeJobs = jobs.filter(job => job.status === 'active').length;
|
||||
document.getElementById('active-jobs-count').textContent = activeJobs;
|
||||
|
||||
// Scheduled jobs
|
||||
const scheduledJobs = jobs.filter(job => job.status === 'scheduled').length;
|
||||
document.getElementById('scheduled-jobs-count').textContent = scheduledJobs;
|
||||
|
||||
// Available printers
|
||||
const availablePrinters = printers.filter(printer => printer.status === 'available').length;
|
||||
document.getElementById('available-printers-count').textContent = availablePrinters;
|
||||
|
||||
// Total print time
|
||||
const totalTime = stats.total_print_time || 0;
|
||||
document.getElementById('total-print-time').textContent = formatDuration(totalTime);
|
||||
}
|
||||
|
||||
// Update recent jobs
|
||||
function updateRecentJobs() {
|
||||
const container = document.getElementById('recent-jobs');
|
||||
const recentJobs = dashboardData.jobs
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
||||
.slice(0, 5);
|
||||
|
||||
if (recentJobs.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<svg class="h-12 w-12 text-mercedes-silver mx-auto mb-4" 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>
|
||||
<p class="text-mercedes-gray">Noch keine Jobs vorhanden</p>
|
||||
<a href="/jobs/new" class="mt-2 inline-block text-mercedes-blue hover:text-blue-700 font-medium">Ersten Job erstellen</a>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = recentJobs.map(job => {
|
||||
const statusColor = getJobStatusColor(job.status);
|
||||
const statusText = getJobStatusText(job.status);
|
||||
|
||||
return `
|
||||
<div class="border border-mercedes-silver rounded-lg p-4 hover:shadow-md transition-shadow duration-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="font-medium text-mercedes-black">${job.title}</h3>
|
||||
<p class="text-sm text-mercedes-gray mt-1">
|
||||
Drucker: ${job.printer_name || 'Unbekannt'} •
|
||||
Erstellt: ${formatDate(job.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}">
|
||||
${statusText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between">
|
||||
<div class="text-sm text-mercedes-gray">
|
||||
${job.start_time ? `Start: ${formatDate(job.start_time)}` : 'Kein Startzeit'}
|
||||
</div>
|
||||
<a href="/jobs/${job.id}" class="text-mercedes-blue hover:text-blue-700 text-sm font-medium">
|
||||
Details →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Update system status
|
||||
function updateSystemStatus() {
|
||||
const schedulerStatus = document.getElementById('scheduler-status');
|
||||
const schedulerText = document.getElementById('scheduler-text');
|
||||
|
||||
if (dashboardData.schedulerStatus) {
|
||||
schedulerStatus.className = 'h-3 w-3 rounded-full bg-mercedes-green mr-2';
|
||||
schedulerText.textContent = 'Aktiv';
|
||||
schedulerText.className = 'text-sm text-mercedes-green';
|
||||
} else {
|
||||
schedulerStatus.className = 'h-3 w-3 rounded-full bg-mercedes-red mr-2';
|
||||
schedulerText.textContent = 'Inaktiv';
|
||||
schedulerText.className = 'text-sm text-mercedes-red';
|
||||
}
|
||||
}
|
||||
|
||||
// Update recent activity
|
||||
function updateRecentActivity() {
|
||||
const container = document.getElementById('recent-activity');
|
||||
const activities = [];
|
||||
|
||||
// Generate activity from recent jobs
|
||||
dashboardData.jobs
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
||||
.slice(0, 5)
|
||||
.forEach(job => {
|
||||
activities.push({
|
||||
type: 'job_created',
|
||||
message: `Job "${job.title}" erstellt`,
|
||||
time: job.created_at,
|
||||
icon: 'plus'
|
||||
});
|
||||
|
||||
if (job.status === 'active') {
|
||||
activities.push({
|
||||
type: 'job_started',
|
||||
message: `Job "${job.title}" gestartet`,
|
||||
time: job.start_time,
|
||||
icon: 'play'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by time
|
||||
activities.sort((a, b) => new Date(b.time) - new Date(a.time));
|
||||
|
||||
if (activities.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-4">
|
||||
<p class="text-sm text-mercedes-gray">Keine Aktivitäten</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = activities.slice(0, 5).map(activity => {
|
||||
const icon = getActivityIcon(activity.icon);
|
||||
return `
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-mercedes-blue p-1 rounded-full">
|
||||
${icon}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm text-mercedes-black">${activity.message}</p>
|
||||
<p class="text-xs text-mercedes-gray">${formatDate(activity.time)}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function getJobStatusColor(status) {
|
||||
switch (status) {
|
||||
case 'active': return 'bg-mercedes-green text-white';
|
||||
case 'scheduled': return 'bg-mercedes-blue text-white';
|
||||
case 'completed': return 'bg-mercedes-silver text-mercedes-black';
|
||||
case 'aborted': return 'bg-mercedes-red text-white';
|
||||
default: return 'bg-mercedes-gray text-white';
|
||||
}
|
||||
}
|
||||
|
||||
function getJobStatusText(status) {
|
||||
switch (status) {
|
||||
case 'active': return 'Aktiv';
|
||||
case 'scheduled': return 'Geplant';
|
||||
case 'completed': return 'Abgeschlossen';
|
||||
case 'aborted': return 'Abgebrochen';
|
||||
default: return 'Unbekannt';
|
||||
}
|
||||
}
|
||||
|
||||
function getActivityIcon(type) {
|
||||
switch (type) {
|
||||
case 'plus':
|
||||
return '<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" /></svg>';
|
||||
case 'play':
|
||||
return '<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1.586a1 1 0 01.707.293l2.414 2.414a1 1 0 00.707.293H15" /></svg>';
|
||||
default:
|
||||
return '<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>';
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh dashboard
|
||||
function refreshDashboard() {
|
||||
showFlashMessage('Dashboard wird aktualisiert...', 'info');
|
||||
loadDashboardData();
|
||||
}
|
||||
|
||||
// Initialize dashboard
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadDashboardData();
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(loadDashboardData, 30000);
|
||||
// Refresh Button
|
||||
const refreshBtn = document.getElementById('refreshDashboard');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
// Dashboard Theme Switch Updates
|
||||
window.addEventListener('darkModeChanged', function(e) {
|
||||
// Update any theme-specific elements if needed
|
||||
console.log('Theme changed to:', e.detail.isDark ? 'dark' : 'light');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user