Files
Projektarbeit-MYP/backend/app/templates/jobs.html
Till Tomczak 2d33753b94 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.
2025-05-25 20:33:38 +02:00

914 lines
42 KiB
HTML

{% extends "base.html" %}
{% block title %}Jobs - MYP Platform{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Header mit Bereichstitel -->
<div class="relative overflow-hidden rounded-2xl p-6 bg-white/70 dark:bg-slate-800/70 backdrop-blur-lg border border-gray-200/80 dark:border-slate-700/30 shadow-xl transition-all duration-300">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Job-Reservierungen</h1>
<p class="mt-2 text-slate-500 dark:text-slate-400">Verwalten Sie 3D-Druckjobs und Druckerreservierungen</p>
</div>
<div class="flex mt-4 lg:mt-0">
<button onclick="refreshJobs()" class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-6 py-2.5 rounded-xl transition-colors duration-300">
<svg class="h-5 w-5 mr-2 inline" 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>
</div>
</div>
</div>
<!-- Neue Reservierung anlegen -->
<div class="relative overflow-hidden rounded-2xl p-6 bg-white/70 dark:bg-slate-800/70 backdrop-blur-lg border border-gray-200/80 dark:border-slate-700/30 shadow-xl transition-all duration-300">
<h2 class="text-xl font-semibold mb-6 text-slate-900 dark:text-white">Neue Reservierung anlegen</h2>
<form id="newJobForm" class="space-y-6">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Drucker auswählen -->
<div>
<label for="printer_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Drucker auswählen*</label>
<select id="printer_id" name="printer_id" required class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700/50 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm transition-colors duration-200">
<option value="">Drucker auswählen...</option>
<!-- Wird durch JavaScript gefüllt -->
</select>
</div>
<!-- Gewünschte Startzeit -->
<div>
<label for="start_time" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Gewünschte Startzeit*</label>
<input type="datetime-local" id="start_time" name="start_time" required
class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700/50 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm transition-colors duration-200">
</div>
<!-- Geschätzte Druckdauer -->
<div>
<label for="duration" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Geschätzte Druckdauer (Minuten)*</label>
<input type="number" id="duration" name="duration" required min="1" max="7200"
class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700/50 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm transition-colors duration-200">
</div>
</div>
<div>
<label for="job_title" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Job-Titel*</label>
<input type="text" id="job_title" name="job_title" required placeholder="Geben Sie einen beschreibenden Titel ein"
class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700/50 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm transition-colors duration-200">
</div>
<div>
<label for="stl_file" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">STL-Datei hochladen (optional)</label>
<div class="flex items-center justify-center w-full">
<label for="stl_file" class="flex flex-col items-center justify-center w-full h-32 border-2 border-slate-300 dark:border-slate-600 border-dashed rounded-xl cursor-pointer bg-slate-50 dark:bg-slate-700/30 hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors duration-200">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<svg class="w-10 h-10 mb-3 text-slate-500 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p class="mb-2 text-sm text-slate-600 dark:text-slate-400"><span class="font-semibold">Klicken Sie zum Hochladen</span> oder ziehen Sie die Datei hierher</p>
<p class="text-xs text-slate-500 dark:text-slate-500">STL bis zu 50MB</p>
</div>
<input id="stl_file" name="stl_file" type="file" accept=".stl" class="hidden" />
</label>
</div>
<div id="file-name" class="mt-2 text-sm text-slate-500 dark:text-slate-400 hidden"></div>
</div>
<div class="flex justify-end pt-4">
<button type="submit" class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-8 py-3 rounded-xl font-medium transition-colors duration-300">
Reservierung erstellen
</button>
</div>
</form>
</div>
<!-- Aktive und geplante Jobs -->
<div class="relative overflow-hidden rounded-2xl p-6 bg-white/70 dark:bg-slate-800/70 backdrop-blur-lg border border-gray-200/80 dark:border-slate-700/30 shadow-xl transition-all duration-300">
<h2 class="text-xl font-semibold mb-6 text-slate-900 dark:text-white">Aktive und geplante Jobs</h2>
<div id="active-jobs-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Jobs werden dynamisch mit JavaScript geladen -->
<div class="col-span-full py-12 text-center bg-slate-50 dark:bg-slate-700/30 rounded-xl" id="no-jobs-message">
<svg class="w-16 h-16 mx-auto mb-4 text-slate-400 dark:text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-1">Keine aktiven Jobs</h3>
<p class="text-slate-500 dark:text-slate-400">Sie haben derzeit keine aktiven oder geplanten Druckjobs.</p>
</div>
</div>
</div>
</div>
<!-- Job Details Modal -->
<div id="jobDetailsModal" class="fixed inset-0 z-50 flex items-center justify-center hidden">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeJobModal()"></div>
<div class="relative bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[80vh] overflow-auto">
<button class="absolute top-4 right-4 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200" onclick="closeJobModal()">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div class="p-6" id="jobDetailsContent">
<!-- Inhalt wird dynamisch geladen -->
<div class="animate-pulse">
<div class="h-6 bg-slate-200 dark:bg-slate-700 rounded w-3/4 mb-4"></div>
<div class="h-4 bg-slate-200 dark:bg-slate-700 rounded w-1/2 mb-8"></div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded"></div>
</div>
<div class="h-32 bg-slate-200 dark:bg-slate-700 rounded mb-6"></div>
<div class="h-10 bg-slate-200 dark:bg-slate-700 rounded w-1/3"></div>
</div>
</div>
</div>
</div>
<!-- Job verlängern Modal -->
<div id="extendJobModal" class="fixed inset-0 z-50 flex items-center justify-center hidden">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeExtendModal()"></div>
<div class="relative bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-md">
<button class="absolute top-4 right-4 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200" onclick="closeExtendModal()">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div class="p-6">
<h3 class="text-xl font-bold mb-4 text-slate-900 dark:text-white">Druckzeit verlängern</h3>
<p class="mb-6 text-slate-600 dark:text-slate-400">Geben Sie an, um wie viele Minuten die Druckzeit verlängert werden soll.</p>
<form id="extendJobForm">
<input type="hidden" id="extend_job_id" name="job_id">
<div class="mb-6">
<label for="extra_minutes" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Zusätzliche Minuten</label>
<input type="number" id="extra_minutes" name="extra_minutes" required min="1" max="720" value="30"
class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700/50 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm transition-colors duration-200">
</div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="closeExtendModal()"
class="px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
Abbrechen
</button>
<button type="submit"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white rounded-lg transition-colors duration-200">
Verlängern
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// File upload preview
const fileInput = document.getElementById('stl_file');
const fileNameDisplay = document.getElementById('file-name');
if (fileInput) {
fileInput.addEventListener('change', function() {
if (this.files && this.files.length > 0) {
fileNameDisplay.textContent = 'Ausgewählte Datei: ' + this.files[0].name;
fileNameDisplay.classList.remove('hidden');
} else {
fileNameDisplay.classList.add('hidden');
}
});
}
// Zeige die aktuelle Zeit als Standardwert für Startzeit
const startTimeInput = document.getElementById('start_time');
if (startTimeInput) {
const now = new Date();
now.setMinutes(now.getMinutes() + 30); // Default: 30 Minuten in der Zukunft
startTimeInput.value = formatDateTimeForInput(now);
}
// Drucker-Liste laden und Formular initialisieren
loadPrinters();
loadActiveJobs();
// Formulare initialisieren
initNewJobForm();
initExtendJobForm();
// Timer für automatische Aktualisierung der Jobs (alle 60 Sekunden)
setInterval(loadActiveJobs, 60000);
});
// Hilfsfunktion zum Formatieren des Datums für Datetime-Input
function formatDateTimeForInput(date) {
const pad = (num) => num.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
// Aktualisiere die Job-Liste
function refreshJobs() {
const refreshButton = document.querySelector('button[onclick="refreshJobs()"]');
if (refreshButton) {
refreshButton.disabled = true;
refreshButton.innerHTML = `
<svg class="h-5 w-5 mr-2 inline animate-spin" 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...
`;
}
loadActiveJobs().then(() => {
if (refreshButton) {
refreshButton.disabled = false;
refreshButton.innerHTML = `
<svg class="h-5 w-5 mr-2 inline" 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
`;
}
}).catch(error => {
console.error('Fehler beim Aktualisieren der Jobs:', error);
showNotification('Fehler beim Aktualisieren der Jobs', 'error');
if (refreshButton) {
refreshButton.disabled = false;
refreshButton.innerHTML = `
<svg class="h-5 w-5 mr-2 inline" 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
`;
}
});
}
// Laden der Drucker für das Dropdown
function loadPrinters() {
fetch('/api/printers')
.then(response => response.json())
.then(data => {
const printerSelect = document.getElementById('printer_id');
printerSelect.innerHTML = '<option value="">Drucker auswählen...</option>';
// Nur aktive Drucker hinzufügen
data.printers.filter(printer => printer.active).forEach(printer => {
const option = document.createElement('option');
option.value = printer.id;
option.textContent = `${printer.name} (${printer.model || 'Unbekanntes Modell'})`;
printerSelect.appendChild(option);
});
})
.catch(error => {
console.error('Fehler beim Laden der Drucker:', error);
showNotification('Fehler beim Laden der Drucker', 'error');
});
}
// Laden der aktiven Jobs
function loadActiveJobs() {
fetch('/api/jobs/active')
.then(response => response.json())
.then(data => {
renderActiveJobs(data.jobs);
})
.catch(error => {
console.error('Fehler beim Laden der aktiven Jobs:', error);
showNotification('Fehler beim Laden der aktiven Jobs', 'error');
});
}
// Initialisierung des Formulars für neue Jobs
function initNewJobForm() {
const form = document.getElementById('newJobForm');
form.addEventListener('submit', function(e) {
e.preventDefault();
// Daten sammeln
const printer_id = document.getElementById('printer_id').value;
const start_time = document.getElementById('start_time').value;
const duration = document.getElementById('duration').value;
const job_title = document.getElementById('job_title').value;
const fileInput = document.getElementById('stl_file');
if (!printer_id || !start_time || !duration || !job_title) {
showNotification('Bitte füllen Sie alle Pflichtfelder aus', 'warning');
return;
}
// Startzeit in ISO-Format konvertieren
const start_date = new Date(start_time);
// Dateiupload, falls vorhanden
let file_path = null;
let formData = null;
if (fileInput.files && fileInput.files.length > 0) {
formData = new FormData();
formData.append('file', fileInput.files[0]);
// Zuerst Datei hochladen, dann Job erstellen
fetch('/api/files/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(fileData => {
file_path = fileData.file_path;
createJob(printer_id, start_date.toISOString(), parseInt(duration), job_title, file_path);
})
.catch(error => {
console.error('Fehler beim Hochladen der Datei:', error);
showNotification('Fehler beim Hochladen der Datei', 'error');
});
} else {
// Direktes Erstellen des Jobs ohne Dateiupload
createJob(printer_id, start_date.toISOString(), parseInt(duration), job_title, null);
}
});
}
// Job erstellen
function createJob(printer_id, start_iso, duration_minutes, name, file_path) {
const jobData = {
printer_id: printer_id,
start_iso: start_iso,
duration_minutes: duration_minutes,
name: name
};
if (file_path) {
jobData.file_path = file_path;
}
fetch('/api/jobs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(jobData)
})
.then(response => response.json())
.then(data => {
if (data.job) {
showNotification('Reservierung erfolgreich erstellt', 'success');
resetNewJobForm();
loadActiveJobs();
} else {
showNotification(data.error || 'Fehler beim Erstellen der Reservierung', 'error');
}
})
.catch(error => {
console.error('Fehler beim Erstellen des Jobs:', error);
showNotification('Fehler beim Erstellen der Reservierung', 'error');
});
}
// Formular zurücksetzen
function resetNewJobForm() {
document.getElementById('newJobForm').reset();
document.getElementById('file-name').classList.add('hidden');
// Startzeit zurücksetzen (30 Minuten in der Zukunft)
const startTimeInput = document.getElementById('start_time');
const now = new Date();
now.setMinutes(now.getMinutes() + 30);
startTimeInput.value = formatDateTimeForInput(now);
}
// Initialisierung des Formulars zum Verlängern von Jobs
function initExtendJobForm() {
const form = document.getElementById('extendJobForm');
form.addEventListener('submit', function(e) {
e.preventDefault();
const job_id = document.getElementById('extend_job_id').value;
const extra_minutes = document.getElementById('extra_minutes').value;
if (!job_id || !extra_minutes || parseInt(extra_minutes) <= 0) {
showNotification('Bitte geben Sie eine gültige Anzahl an Minuten ein', 'warning');
return;
}
extendJob(job_id, parseInt(extra_minutes));
});
}
// Notification/Toast anzeigen
function showNotification(message, type = 'info') {
// Prüfen, ob der Flash-Container existiert
let container = document.getElementById('flash-messages');
if (!container) {
// Flash-Container erstellen, falls nicht vorhanden
container = document.createElement('div');
container.id = 'flash-messages';
document.querySelector('main').prepend(container);
}
// Notification erstellen
const notification = document.createElement('div');
notification.className = `flash-message ${type} mb-4`;
// Icon basierend auf Typ
let icon = '';
if (type === 'success') {
icon = '<svg class="w-5 h-5 mr-3 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>';
} else if (type === 'error') {
icon = '<svg class="w-5 h-5 mr-3 text-red-500" 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"></path></svg>';
} else if (type === 'warning') {
icon = '<svg class="w-5 h-5 mr-3 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>';
} else {
icon = '<svg class="w-5 h-5 mr-3 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg>';
}
// HTML setzen
notification.innerHTML = `
<div class="flex items-center">
${icon}
<p class="text-sm">${message}</p>
</div>
<button class="absolute top-2 right-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300">
<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"></path>
</svg>
</button>
`;
// Close-Button
notification.querySelector('button').addEventListener('click', function() {
notification.remove();
});
// Zum Container hinzufügen
container.appendChild(notification);
// Nach 5 Sekunden automatisch entfernen
setTimeout(() => {
if (notification.parentNode) {
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 300);
}
}, 5000);
}
// Job verlängern
function extendJob(job_id, extra_minutes) {
fetch(`/api/jobs/${job_id}/extend`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ extra_minutes: extra_minutes })
})
.then(response => response.json())
.then(data => {
if (data.job) {
showNotification('Job erfolgreich verlängert', 'success');
closeExtendModal();
loadActiveJobs();
} else {
showNotification(data.error || 'Fehler beim Verlängern des Jobs', 'error');
}
})
.catch(error => {
console.error('Fehler beim Verlängern des Jobs:', error);
showNotification('Fehler beim Verlängern des Jobs', 'error');
});
}
// Job manuell beenden (nur für Admins)
function finishJob(job_id) {
if (!confirm('Sind Sie sicher, dass Sie diesen Job beenden möchten? Die Steckdose wird sofort ausgeschaltet.')) {
return;
}
fetch(`/api/jobs/${job_id}/finish`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.job) {
showNotification('Job erfolgreich beendet', 'success');
loadActiveJobs();
} else {
showNotification(data.error || 'Fehler beim Beenden des Jobs', 'error');
}
})
.catch(error => {
console.error('Fehler beim Beenden des Jobs:', error);
showNotification('Fehler beim Beenden des Jobs', 'error');
});
}
// Job-Details anzeigen
function viewJobDetails(job_id) {
fetch(`/api/jobs/${job_id}`)
.then(response => response.json())
.then(job => {
renderJobDetails(job);
openJobModal();
})
.catch(error => {
console.error('Fehler beim Laden der Job-Details:', error);
showNotification('Fehler beim Laden der Job-Details', 'error');
});
}
// Job-Modal öffnen
function openJobModal() {
document.getElementById('jobDetailsModal').classList.remove('hidden');
document.body.classList.add('overflow-hidden');
}
// Job-Modal schließen
function closeJobModal() {
document.getElementById('jobDetailsModal').classList.add('hidden');
document.body.classList.remove('overflow-hidden');
}
// Modal zum Verlängern eines Jobs öffnen
function openExtendModal(job_id) {
document.getElementById('extend_job_id').value = job_id;
document.getElementById('extendJobModal').classList.remove('hidden');
document.body.classList.add('overflow-hidden');
document.getElementById('extra_minutes').focus();
}
// Modal zum Verlängern schließen
function closeExtendModal() {
document.getElementById('extendJobModal').classList.add('hidden');
document.body.classList.remove('overflow-hidden');
}
// Job-Karte rendern (für die Anzeige aktiver Jobs)
function renderJobCard(job) {
const now = new Date();
const startAt = new Date(job.start_at);
const endAt = new Date(job.end_at);
// Status-Badge bestimmen
let statusBadge = '';
if (job.status === 'running') {
statusBadge = `
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400">
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full animate-pulse"></span>
Aktiv
</span>
`;
} else if (job.status === 'scheduled') {
statusBadge = `
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-400">
<span class="w-2 h-2 mr-1 bg-blue-500 rounded-full"></span>
Geplant
</span>
`;
}
// Restzeit oder Zeit bis Start berechnen
let timeInfo = '';
let progressBar = '';
if (job.status === 'running') {
// Restzeit für laufende Jobs
const remainingMinutes = job.remaining_minutes || Math.max(0, Math.floor((endAt - now) / 60000));
const hours = Math.floor(remainingMinutes / 60);
const minutes = remainingMinutes % 60;
const timeDisplay = hours > 0
? `${hours} Std. ${minutes} Min.`
: `${minutes} Minuten`;
timeInfo = `
<div class="mb-4">
<div class="flex justify-between text-xs text-slate-500 dark:text-slate-400 mb-1">
<span>Restzeit:</span>
<span>${timeDisplay}</span>
</div>
</div>
`;
// Fortschrittsbalken
const totalDuration = job.duration_minutes;
const elapsed = totalDuration - remainingMinutes;
const progress = Math.min(100, Math.max(0, Math.round((elapsed / totalDuration) * 100)));
progressBar = `
<div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2.5 mb-4">
<div class="bg-blue-600 h-2.5 rounded-full" style="width: ${progress}%"></div>
</div>
`;
} else if (job.status === 'scheduled') {
// Zeit bis zum Start für geplante Jobs
const minutesToStart = Math.max(0, Math.floor((startAt - now) / 60000));
const hours = Math.floor(minutesToStart / 60);
const minutes = minutesToStart % 60;
const timeDisplay = hours > 0
? `${hours} Std. ${minutes} Min.`
: `${minutes} Minuten`;
timeInfo = `
<div class="mb-4">
<div class="flex justify-between text-xs text-slate-500 dark:text-slate-400 mb-1">
<span>Startet in:</span>
<span>${timeDisplay}</span>
</div>
</div>
`;
}
// Aktionsbuttons
let actionButtons = `
<button type="button" onclick="viewJobDetails(${job.id})"
class="text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white text-sm font-medium transition-colors duration-200">
Details
</button>
`;
// Verlängern-Button für laufende und geplante Jobs
if (job.status === 'running' || job.status === 'scheduled') {
actionButtons += `
<button type="button" onclick="openExtendModal(${job.id})"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 text-sm font-medium transition-colors duration-200">
Verlängern
</button>
`;
}
// Admin: Beenden-Button für laufende Jobs
if (job.status === 'running' && isAdmin) {
actionButtons += `
<button type="button" onclick="finishJob(${job.id})"
class="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium transition-colors duration-200">
Beenden
</button>
`;
}
// Drucker-Icon basierend auf Status
const printerStatus = job.status === 'running' ? 'online' : 'idle';
const statusDot = job.status === 'running'
? '<span class="absolute bottom-0 right-0 block h-2.5 w-2.5 rounded-full bg-green-500 dark:bg-green-400 ring-2 ring-white dark:ring-slate-700"></span>'
: '<span class="absolute bottom-0 right-0 block h-2.5 w-2.5 rounded-full bg-blue-500 dark:bg-blue-400 ring-2 ring-white dark:ring-slate-700"></span>';
// Vollständige Job-Karte
const card = document.createElement('div');
card.className = 'relative overflow-hidden rounded-xl bg-slate-50 dark:bg-slate-700/30 border border-slate-200 dark:border-slate-600/30 shadow-md hover:shadow-lg transition-all duration-300';
card.innerHTML = `
<div class="absolute top-4 right-4">
${statusBadge}
</div>
<div class="p-5">
<h3 class="text-lg font-bold mb-2 pr-24 text-slate-900 dark:text-white">${job.name}</h3>
<div class="flex items-center text-sm text-slate-500 dark:text-slate-400 mb-4">
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>${formatDateTime(job.start_at)}</span>
<svg class="h-4 w-4 ml-3 mr-1" 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>
<span>${job.duration_minutes} Min.</span>
</div>
<div class="flex items-center mb-4">
<div class="mr-3 flex-shrink-0">
<div class="relative">
<svg class="h-10 w-10 text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-600 p-1.5 rounded-lg shadow-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>
${statusDot}
</div>
</div>
<div>
<p class="text-sm font-medium text-slate-900 dark:text-white">${job.printer?.name || 'Unbekannter Drucker'}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">${job.printer?.model || ''}</p>
</div>
</div>
${progressBar}
${timeInfo}
<div class="flex justify-between pt-3 border-t border-slate-200 dark:border-slate-600/30 mt-4">
${actionButtons}
</div>
</div>
`;
return card;
}
// Job-Details rendern
function renderJobDetails(job) {
const detailsContainer = document.getElementById('jobDetailsContent');
const startAt = new Date(job.start_at);
const endAt = new Date(job.end_at);
const duration = job.duration_minutes;
// Status-Label
let statusLabel = '';
if (job.status === 'running') {
statusLabel = '<span class="bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400 px-2.5 py-1 rounded-full text-xs font-medium">Aktiv</span>';
} else if (job.status === 'scheduled') {
statusLabel = '<span class="bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-400 px-2.5 py-1 rounded-full text-xs font-medium">Geplant</span>';
} else if (job.status === 'finished') {
statusLabel = '<span class="bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-400 px-2.5 py-1 rounded-full text-xs font-medium">Abgeschlossen</span>';
} else if (job.status === 'aborted') {
statusLabel = '<span class="bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400 px-2.5 py-1 rounded-full text-xs font-medium">Abgebrochen</span>';
}
// Restzeit oder Zeit bis Start berechnen
const now = new Date();
let timeInfo = '';
if (job.status === 'running') {
const remainingMinutes = Math.max(0, Math.floor((endAt - now) / 60000));
const hours = Math.floor(remainingMinutes / 60);
const minutes = remainingMinutes % 60;
const timeDisplay = hours > 0
? `${hours} Stunden, ${minutes} Minuten`
: `${minutes} Minuten`;
timeInfo = `<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">Verbleibende Zeit: <span class="font-semibold">${timeDisplay}</span></p>`;
} else if (job.status === 'scheduled') {
const minutesToStart = Math.max(0, Math.floor((startAt - now) / 60000));
const hours = Math.floor(minutesToStart / 60);
const minutes = minutesToStart % 60;
const timeDisplay = hours > 0
? `${hours} Stunden, ${minutes} Minuten`
: `${minutes} Minuten`;
timeInfo = `<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">Startet in: <span class="font-semibold">${timeDisplay}</span></p>`;
}
// Aktionsbuttons
let actionButtons = '';
if (job.status === 'running' || job.status === 'scheduled') {
actionButtons += `
<button type="button" onclick="openExtendModal(${job.id}); closeJobModal();"
class="ml-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-4 py-2 rounded-lg transition-colors duration-200">
Verlängern
</button>
`;
}
if (job.status === 'running' && isAdmin) {
actionButtons += `
<button type="button" onclick="finishJob(${job.id}); closeJobModal();"
class="ml-3 bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-500 text-white px-4 py-2 rounded-lg transition-colors duration-200">
Beenden
</button>
`;
}
// Datei-Informationen
let fileInfo = '';
if (job.file_path) {
const fileName = job.file_path.split('/').pop();
fileInfo = `
<div class="mb-6 p-4 bg-slate-100 dark:bg-slate-700 rounded-lg">
<div class="flex items-center">
<svg class="w-6 h-6 text-slate-500 dark:text-slate-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 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>
<p class="text-sm font-medium text-slate-900 dark:text-white">${fileName}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">STL-Datei</p>
</div>
<a href="/api/files/download?path=${encodeURIComponent(job.file_path)}"
class="ml-auto bg-slate-200 hover:bg-slate-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-slate-800 dark:text-slate-200 px-3 py-1 text-sm rounded-lg transition-colors duration-200">
Herunterladen
</a>
</div>
</div>
`;
}
// HTML für die Job-Details zusammenbauen
detailsContainer.innerHTML = `
<div class="flex justify-between items-start mb-4">
<h2 class="text-xl font-bold text-slate-900 dark:text-white">${job.name}</h2>
${statusLabel}
</div>
${timeInfo}
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Drucker</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${job.printer?.name || 'Unbekannt'}</p>
</div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Modell</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${job.printer?.model || 'Unbekannt'}</p>
</div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Startzeit</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${formatDateTime(job.start_at)}</p>
</div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Geplante Endzeit</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${formatDateTime(job.end_at)}</p>
</div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Dauer</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${duration} Minuten</p>
</div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Erstellt von</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${job.user?.name || job.user?.email || 'Unbekannt'}</p>
</div>
</div>
${fileInfo}
<div class="flex justify-end mt-6 pt-4 border-t border-slate-200 dark:border-slate-700">
<button type="button" onclick="closeJobModal()"
class="bg-slate-200 hover:bg-slate-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-slate-200 px-4 py-2 rounded-lg transition-colors duration-200">
Schließen
</button>
${actionButtons}
</div>
`;
}
// Aktive Jobs anzeigen
function renderActiveJobs(jobs) {
const container = document.getElementById('active-jobs-container');
const noJobsMessage = document.getElementById('no-jobs-message');
// Container leeren
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// "Keine Jobs" Nachricht anzeigen, wenn keine Jobs vorhanden sind
if (!jobs || jobs.length === 0) {
container.appendChild(noJobsMessage);
return;
} else {
// Nachricht entfernen, falls Jobs vorhanden sind
if (noJobsMessage.parentNode === container) {
container.removeChild(noJobsMessage);
}
}
// Jobs sortieren: Laufende zuerst, dann geplante, dann nach Startzeit
jobs.sort((a, b) => {
// Laufende Jobs haben Priorität
if (a.status === 'running' && b.status !== 'running') return -1;
if (a.status !== 'running' && b.status === 'running') return 1;
// Dann nach Startzeit sortieren
return new Date(a.start_at) - new Date(b.start_at);
});
// Jobs anzeigen
jobs.forEach(job => {
const jobCard = renderJobCard(job);
container.appendChild(jobCard);
});
}
// Formatiert einen ISO-Datums-String in lesbares Datum/Uhrzeit
function formatDateTime(isoString) {
if (!isoString) return 'Unbekannt';
const date = new Date(isoString);
return date.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// Globale Variable für Admin-Status
const isAdmin = {% if current_user.is_admin %}true{% else %}false{% endif %};
</script>
{% endblock %}