feat: Update frontend and backend configurations for development environment - Downgrade PyP100 version in requirements.txt for compatibility. - Add new frontend routes for index, login, dashboard, printers, jobs, and profile pages. - Modify docker-compose files for development setup, including environment variables and service names. - Update Caddyfile for local development with Raspberry Pi backend. - Adjust health check route to use updated backend URL. - Enhance setup-backend-url.sh for development environment configuration. """
461 lines
22 KiB
HTML
461 lines
22 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Dashboard - MYP Platform{% 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>
|
|
<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">
|
|
<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>
|
|
<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">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Neuer Job
|
|
</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>
|
|
</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>
|
|
</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>
|
|
</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>
|
|
</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>
|
|
</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>
|
|
</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>
|
|
</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>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
</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" />
|
|
</svg>
|
|
Admin-Panel
|
|
</a>
|
|
</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 %}
|
|
<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);
|
|
});
|
|
</script>
|
|
{% endblock %} |