- 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.
914 lines
42 KiB
HTML
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 %} |