447 lines
15 KiB
JavaScript
447 lines
15 KiB
JavaScript
// Dashboard JavaScript - Externe Datei für CSP-Konformität
|
|
|
|
// Globale Variablen
|
|
let dashboardData = {};
|
|
let updateInterval;
|
|
|
|
// API Base URL Detection
|
|
function detectApiBaseUrl() {
|
|
const currentPort = window.location.port;
|
|
const currentProtocol = window.location.protocol;
|
|
const currentHost = window.location.hostname;
|
|
|
|
// Development-Umgebung (Port 5000)
|
|
if (currentPort === '5000') {
|
|
return `${currentProtocol}//${currentHost}:${currentPort}`;
|
|
}
|
|
|
|
// Production-Umgebung (Port 443 oder kein Port)
|
|
if (currentPort === '443' || currentPort === '') {
|
|
return `${currentProtocol}//${currentHost}`;
|
|
}
|
|
|
|
// Fallback für andere Ports
|
|
return window.location.origin;
|
|
}
|
|
|
|
const API_BASE_URL = detectApiBaseUrl();
|
|
|
|
/**
|
|
* Zentrale API-Response-Validierung mit umfassendem Error-Handling
|
|
* @param {Response} response - Fetch Response-Objekt
|
|
* @param {string} context - Kontext der API-Anfrage für bessere Fehlermeldungen
|
|
* @returns {Promise<Object>} - Validierte JSON-Daten
|
|
* @throws {Error} - Bei Validierungsfehlern
|
|
*/
|
|
async function validateApiResponse(response, context = 'API-Anfrage') {
|
|
try {
|
|
// 1. HTTP Status Code prüfen
|
|
if (!response.ok) {
|
|
// Spezielle Behandlung für bekannte Fehler-Codes
|
|
switch (response.status) {
|
|
case 401:
|
|
throw new Error(`Authentifizierung fehlgeschlagen (${context})`);
|
|
case 403:
|
|
throw new Error(`Zugriff verweigert (${context})`);
|
|
case 404:
|
|
throw new Error(`Ressource nicht gefunden (${context})`);
|
|
case 429:
|
|
throw new Error(`Zu viele Anfragen (${context})`);
|
|
case 500:
|
|
throw new Error(`Serverfehler (${context})`);
|
|
case 503:
|
|
throw new Error(`Service nicht verfügbar (${context})`);
|
|
default:
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText} (${context})`);
|
|
}
|
|
}
|
|
|
|
// 2. Content-Type prüfen (muss application/json enthalten)
|
|
const contentType = response.headers.get('content-type');
|
|
if (!contentType || !contentType.includes('application/json')) {
|
|
// Versuche Response-Text zu lesen für bessere Fehlermeldung
|
|
const responseText = await response.text();
|
|
|
|
// Prüfe auf HTML-Fehlerseiten (typisch für 404/500 Seiten)
|
|
if (responseText.includes('<!DOCTYPE html>') || responseText.includes('<html')) {
|
|
console.warn(`❌ HTML-Fehlerseite erhalten statt JSON (${context}):`, responseText.substring(0, 200));
|
|
throw new Error(`Server-Fehlerseite erhalten statt JSON-Response (${context})`);
|
|
}
|
|
|
|
console.warn(`❌ Ungültiger Content-Type (${context}):`, contentType);
|
|
console.warn(`❌ Response-Text (${context}):`, responseText.substring(0, 500));
|
|
throw new Error(`Ungültiger Content-Type: ${contentType || 'fehlt'} (${context})`);
|
|
}
|
|
|
|
// 3. JSON parsing mit detailliertem Error-Handling
|
|
let data;
|
|
try {
|
|
data = await response.json();
|
|
} catch (jsonError) {
|
|
// Versuche rohen Text zu lesen für Debugging
|
|
const rawText = await response.text();
|
|
console.error(`❌ JSON-Parsing-Fehler (${context}):`, jsonError);
|
|
console.error(`❌ Raw Response (${context}):`, rawText.substring(0, 1000));
|
|
throw new Error(`Ungültige JSON-Response: ${jsonError.message} (${context})`);
|
|
}
|
|
|
|
// 4. Prüfe auf null/undefined Response
|
|
if (data === null || data === undefined) {
|
|
throw new Error(`Leere Response erhalten (${context})`);
|
|
}
|
|
|
|
// 5. Validiere Response-Struktur (wenn success-Feld erwartet wird)
|
|
if (typeof data === 'object' && data.hasOwnProperty('success')) {
|
|
if (!data.success && data.error) {
|
|
console.warn(`❌ API-Fehler (${context}):`, data.error);
|
|
throw new Error(`API-Fehler: ${data.error} (${context})`);
|
|
}
|
|
}
|
|
|
|
// Erfolgreiche Validierung
|
|
console.log(`✅ API-Response validiert (${context}):`, data);
|
|
return data;
|
|
|
|
} catch (error) {
|
|
// Error-Logging mit Kontext
|
|
console.error(`❌ validateApiResponse fehlgeschlagen (${context}):`, error);
|
|
console.error(`❌ Response-Details (${context}):`, {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
url: response.url,
|
|
headers: Object.fromEntries(response.headers.entries())
|
|
});
|
|
|
|
// Re-throw mit erweiterten Informationen
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// DOM-Elemente
|
|
const elements = {
|
|
activeJobs: null,
|
|
scheduledJobs: null,
|
|
availablePrinters: null,
|
|
totalPrintTime: null,
|
|
schedulerStatus: null,
|
|
recentJobsList: null,
|
|
recentActivitiesList: null,
|
|
refreshBtn: null,
|
|
schedulerToggleBtn: null
|
|
};
|
|
|
|
// Initialisierung beim Laden der Seite
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeDashboard();
|
|
});
|
|
|
|
function initializeDashboard() {
|
|
// DOM-Elemente referenzieren
|
|
elements.activeJobs = document.getElementById('active-jobs');
|
|
elements.scheduledJobs = document.getElementById('scheduled-jobs');
|
|
elements.availablePrinters = document.getElementById('available-printers');
|
|
elements.totalPrintTime = document.getElementById('total-print-time');
|
|
elements.schedulerStatus = document.getElementById('scheduler-status');
|
|
elements.recentJobsList = document.getElementById('recent-jobs-list');
|
|
elements.recentActivitiesList = document.getElementById('recent-activities-list');
|
|
elements.refreshBtn = document.getElementById('refresh-btn');
|
|
elements.schedulerToggleBtn = document.getElementById('scheduler-toggle-btn');
|
|
|
|
// Event-Listener hinzufügen
|
|
if (elements.refreshBtn) {
|
|
elements.refreshBtn.addEventListener('click', refreshDashboard);
|
|
}
|
|
|
|
if (elements.schedulerToggleBtn) {
|
|
elements.schedulerToggleBtn.addEventListener('click', toggleScheduler);
|
|
}
|
|
|
|
// Initiales Laden der Daten
|
|
loadDashboardData();
|
|
loadRecentJobs();
|
|
loadRecentActivities();
|
|
loadSchedulerStatus();
|
|
|
|
// Auto-Update alle 30 Sekunden
|
|
updateInterval = setInterval(function() {
|
|
loadDashboardData();
|
|
loadRecentJobs();
|
|
loadRecentActivities();
|
|
loadSchedulerStatus();
|
|
}, 30000);
|
|
}
|
|
|
|
// Dashboard-Hauptdaten laden
|
|
async function loadDashboardData() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/dashboard`);
|
|
dashboardData = await validateApiResponse(response, 'Dashboard-Daten');
|
|
updateDashboardUI();
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Dashboard-Daten:', error);
|
|
showError(`Fehler beim Laden der Dashboard-Daten: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Dashboard-UI aktualisieren
|
|
function updateDashboardUI() {
|
|
if (elements.activeJobs) {
|
|
elements.activeJobs.textContent = dashboardData.active_jobs || 0;
|
|
}
|
|
|
|
if (elements.scheduledJobs) {
|
|
elements.scheduledJobs.textContent = dashboardData.scheduled_jobs || 0;
|
|
}
|
|
|
|
if (elements.availablePrinters) {
|
|
elements.availablePrinters.textContent = dashboardData.available_printers || 0;
|
|
}
|
|
|
|
if (elements.totalPrintTime) {
|
|
const hours = Math.floor((dashboardData.total_print_time || 0) / 3600);
|
|
const minutes = Math.floor(((dashboardData.total_print_time || 0) % 3600) / 60);
|
|
elements.totalPrintTime.textContent = `${hours}h ${minutes}m`;
|
|
}
|
|
}
|
|
|
|
// Aktuelle Jobs laden
|
|
async function loadRecentJobs() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/jobs/recent`);
|
|
const data = await validateApiResponse(response, 'Aktuelle Jobs');
|
|
updateRecentJobsList(data.jobs);
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der aktuellen Jobs:', error);
|
|
if (elements.recentJobsList) {
|
|
elements.recentJobsList.innerHTML = `<li class="list-group-item text-danger">Fehler beim Laden: ${error.message}</li>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Jobs-Liste aktualisieren
|
|
function updateRecentJobsList(jobs) {
|
|
if (!elements.recentJobsList) return;
|
|
|
|
if (!jobs || jobs.length === 0) {
|
|
elements.recentJobsList.innerHTML = '<li class="list-group-item text-muted">Keine aktuellen Jobs</li>';
|
|
return;
|
|
}
|
|
|
|
const jobsHtml = jobs.map(job => {
|
|
const statusClass = getStatusClass(job.status);
|
|
const timeAgo = formatTimeAgo(job.created_at);
|
|
|
|
return `
|
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>${escapeHtml(job.name)}</strong><br>
|
|
<small class="text-muted">${escapeHtml(job.printer_name)} • ${timeAgo}</small>
|
|
</div>
|
|
<span class="badge ${statusClass}">${getStatusText(job.status)}</span>
|
|
</li>
|
|
`;
|
|
}).join('');
|
|
|
|
elements.recentJobsList.innerHTML = jobsHtml;
|
|
}
|
|
|
|
// Aktuelle Aktivitäten laden
|
|
async function loadRecentActivities() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/activity/recent`);
|
|
const data = await validateApiResponse(response, 'Aktivitäten');
|
|
updateRecentActivitiesList(data.activities);
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Aktivitäten:', error);
|
|
if (elements.recentActivitiesList) {
|
|
elements.recentActivitiesList.innerHTML = `<li class="list-group-item text-danger">Fehler beim Laden: ${error.message}</li>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Aktivitäten-Liste aktualisieren
|
|
function updateRecentActivitiesList(activities) {
|
|
if (!elements.recentActivitiesList) return;
|
|
|
|
if (!activities || activities.length === 0) {
|
|
elements.recentActivitiesList.innerHTML = '<li class="list-group-item text-muted">Keine aktuellen Aktivitäten</li>';
|
|
return;
|
|
}
|
|
|
|
const activitiesHtml = activities.map(activity => {
|
|
const timeAgo = formatTimeAgo(activity.timestamp);
|
|
|
|
return `
|
|
<li class="list-group-item">
|
|
<div>${escapeHtml(activity.description)}</div>
|
|
<small class="text-muted">${timeAgo}</small>
|
|
</li>
|
|
`;
|
|
}).join('');
|
|
|
|
elements.recentActivitiesList.innerHTML = activitiesHtml;
|
|
}
|
|
|
|
// Scheduler-Status laden
|
|
async function loadSchedulerStatus() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/scheduler/status`);
|
|
const data = await validateApiResponse(response, 'Scheduler-Status');
|
|
updateSchedulerStatus(data.running);
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden des Scheduler-Status:', error);
|
|
if (elements.schedulerStatus) {
|
|
elements.schedulerStatus.innerHTML = '<span class="badge bg-secondary">Unbekannt</span>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scheduler-Status aktualisieren
|
|
function updateSchedulerStatus(isRunning) {
|
|
if (!elements.schedulerStatus) return;
|
|
|
|
const statusClass = isRunning ? 'bg-success' : 'bg-danger';
|
|
const statusText = isRunning ? 'Aktiv' : 'Gestoppt';
|
|
|
|
elements.schedulerStatus.innerHTML = `<span class="badge ${statusClass}">${statusText}</span>`;
|
|
|
|
if (elements.schedulerToggleBtn) {
|
|
elements.schedulerToggleBtn.textContent = isRunning ? 'Scheduler stoppen' : 'Scheduler starten';
|
|
elements.schedulerToggleBtn.className = isRunning ? 'btn btn-danger btn-sm' : 'btn btn-success btn-sm';
|
|
}
|
|
}
|
|
|
|
// Scheduler umschalten
|
|
async function toggleScheduler() {
|
|
try {
|
|
const isRunning = dashboardData.scheduler_running;
|
|
const endpoint = isRunning ? '/api/scheduler/stop' : '/api/scheduler/start';
|
|
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
const result = await validateApiResponse(response, 'Scheduler umschalten');
|
|
|
|
if (result.success) {
|
|
showSuccess(result.message);
|
|
// Status sofort neu laden
|
|
setTimeout(loadSchedulerStatus, 1000);
|
|
} else {
|
|
showError(result.error || 'Unbekannter Fehler');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Umschalten des Schedulers:', error);
|
|
showError('Fehler beim Umschalten des Schedulers');
|
|
}
|
|
}
|
|
|
|
// Dashboard manuell aktualisieren
|
|
function refreshDashboard() {
|
|
if (elements.refreshBtn) {
|
|
elements.refreshBtn.disabled = true;
|
|
elements.refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Aktualisiere...';
|
|
}
|
|
|
|
Promise.all([
|
|
loadDashboardData(),
|
|
loadRecentJobs(),
|
|
loadRecentActivities(),
|
|
loadSchedulerStatus()
|
|
]).finally(() => {
|
|
if (elements.refreshBtn) {
|
|
elements.refreshBtn.disabled = false;
|
|
elements.refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> Aktualisieren';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Hilfsfunktionen
|
|
function getStatusClass(status) {
|
|
const statusClasses = {
|
|
'pending': 'bg-warning',
|
|
'printing': 'bg-primary',
|
|
'completed': 'bg-success',
|
|
'failed': 'bg-danger',
|
|
'cancelled': 'bg-secondary',
|
|
'scheduled': 'bg-info'
|
|
};
|
|
return statusClasses[status] || 'bg-secondary';
|
|
}
|
|
|
|
function getStatusText(status) {
|
|
const statusTexts = {
|
|
'pending': 'Wartend',
|
|
'printing': 'Druckt',
|
|
'completed': 'Abgeschlossen',
|
|
'failed': 'Fehlgeschlagen',
|
|
'cancelled': 'Abgebrochen',
|
|
'scheduled': 'Geplant'
|
|
};
|
|
return statusTexts[status] || status;
|
|
}
|
|
|
|
function formatTimeAgo(timestamp) {
|
|
const now = new Date();
|
|
const time = new Date(timestamp);
|
|
const diffMs = now - time;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffMins < 1) return 'Gerade eben';
|
|
if (diffMins < 60) return `vor ${diffMins} Min`;
|
|
if (diffHours < 24) return `vor ${diffHours} Std`;
|
|
return `vor ${diffDays} Tag${diffDays > 1 ? 'en' : ''}`;
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
showNotification(message, 'success');
|
|
}
|
|
|
|
function showError(message) {
|
|
showNotification(message, 'danger');
|
|
}
|
|
|
|
function showNotification(message, type) {
|
|
// Einfache Notification - kann später durch Toast-System ersetzt werden
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
|
alertDiv.style.top = '20px';
|
|
alertDiv.style.right = '20px';
|
|
alertDiv.style.zIndex = '9999';
|
|
alertDiv.innerHTML = `
|
|
${escapeHtml(message)}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.appendChild(alertDiv);
|
|
|
|
// Automatisch nach 5 Sekunden entfernen
|
|
setTimeout(() => {
|
|
if (alertDiv.parentNode) {
|
|
alertDiv.parentNode.removeChild(alertDiv);
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
// Cleanup beim Verlassen der Seite
|
|
window.addEventListener('beforeunload', function() {
|
|
if (updateInterval) {
|
|
clearInterval(updateInterval);
|
|
}
|
|
});
|