"""
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. """
This commit is contained in:
469
backend/app/templates/jobs.html
Normal file
469
backend/app/templates/jobs.html
Normal file
@@ -0,0 +1,469 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Druckaufträge - 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">Druckaufträge</h1>
|
||||
<p class="mt-2 text-mercedes-gray">Verwalten Sie Ihre 3D-Druckaufträge</p>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<button onclick="refreshJobs()" 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="/new-job" 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 Auftrag
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="mb-6 mercedes-card rounded-xl p-4">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<label for="status-filter" class="text-sm font-medium text-mercedes-black">Status:</label>
|
||||
<select id="status-filter" onchange="filterJobs()" class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
<option value="">Alle</option>
|
||||
<option value="pending">Wartend</option>
|
||||
<option value="printing">Druckt</option>
|
||||
<option value="completed">Abgeschlossen</option>
|
||||
<option value="failed">Fehlgeschlagen</option>
|
||||
<option value="cancelled">Abgebrochen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<label for="printer-filter" class="text-sm font-medium text-mercedes-black">Drucker:</label>
|
||||
<select id="printer-filter" onchange="filterJobs()" class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
<option value="">Alle Drucker</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<label for="search-input" class="text-sm font-medium text-mercedes-black">Suche:</label>
|
||||
<input type="text" id="search-input" placeholder="Dateiname..." onkeyup="filterJobs()"
|
||||
class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
</div>
|
||||
|
||||
<button onclick="clearFilters()" class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-3 py-1 rounded-lg text-sm mercedes-button transition-all duration-200">
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jobs Table -->
|
||||
<div class="mercedes-card rounded-xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-mercedes-silver">
|
||||
<thead class="bg-mercedes-light">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Datei
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Drucker
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Fortschritt
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Erstellt
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Aktionen
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jobs-table-body" class="bg-white divide-y divide-mercedes-silver">
|
||||
<!-- Loading state -->
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-mercedes-blue mx-auto"></div>
|
||||
<p class="mt-4 text-mercedes-gray">Lade Aufträge...</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div id="pagination" class="mt-6 flex items-center justify-between">
|
||||
<div class="text-sm text-mercedes-gray">
|
||||
<span id="pagination-info">Zeige 0 von 0 Aufträgen</span>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button id="prev-page" onclick="changePage(-1)" disabled
|
||||
class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-mercedes-light transition-all duration-200">
|
||||
Zurück
|
||||
</button>
|
||||
<span id="page-numbers" class="flex space-x-1">
|
||||
<!-- Page numbers will be inserted here -->
|
||||
</span>
|
||||
<button id="next-page" onclick="changePage(1)" disabled
|
||||
class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-mercedes-light transition-all duration-200">
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Detail Modal -->
|
||||
<div id="jobDetailModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="mercedes-card rounded-xl p-6 w-full max-w-2xl max-h-screen overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-mercedes-black">Auftrag Details</h2>
|
||||
<button onclick="hideJobDetailModal()" class="text-mercedes-gray hover:text-mercedes-black">
|
||||
<svg class="h-6 w-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>
|
||||
|
||||
<div id="job-detail-content">
|
||||
<!-- Content will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let jobs = [];
|
||||
let filteredJobs = [];
|
||||
let printers = [];
|
||||
let currentPage = 1;
|
||||
const jobsPerPage = 10;
|
||||
|
||||
// Load data
|
||||
async function loadJobs() {
|
||||
try {
|
||||
const [jobsResponse, printersResponse] = await Promise.all([
|
||||
apiCall('/api/jobs'),
|
||||
apiCall('/api/printers')
|
||||
]);
|
||||
|
||||
jobs = jobsResponse.jobs || [];
|
||||
printers = printersResponse.printers || [];
|
||||
|
||||
populatePrinterFilter();
|
||||
filterJobs();
|
||||
} catch (error) {
|
||||
console.error('Error loading jobs:', error);
|
||||
showFlashMessage('Fehler beim Laden der Aufträge', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Populate printer filter
|
||||
function populatePrinterFilter() {
|
||||
const select = document.getElementById('printer-filter');
|
||||
select.innerHTML = '<option value="">Alle Drucker</option>';
|
||||
|
||||
printers.forEach(printer => {
|
||||
const option = document.createElement('option');
|
||||
option.value = printer.id;
|
||||
option.textContent = printer.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter jobs
|
||||
function filterJobs() {
|
||||
const statusFilter = document.getElementById('status-filter').value;
|
||||
const printerFilter = document.getElementById('printer-filter').value;
|
||||
const searchTerm = document.getElementById('search-input').value.toLowerCase();
|
||||
|
||||
filteredJobs = jobs.filter(job => {
|
||||
const matchesStatus = !statusFilter || job.status === statusFilter;
|
||||
const matchesPrinter = !printerFilter || job.printer_id == printerFilter;
|
||||
const matchesSearch = !searchTerm || job.filename.toLowerCase().includes(searchTerm);
|
||||
|
||||
return matchesStatus && matchesPrinter && matchesSearch;
|
||||
});
|
||||
|
||||
currentPage = 1;
|
||||
renderJobs();
|
||||
updatePagination();
|
||||
}
|
||||
|
||||
// Clear filters
|
||||
function clearFilters() {
|
||||
document.getElementById('status-filter').value = '';
|
||||
document.getElementById('printer-filter').value = '';
|
||||
document.getElementById('search-input').value = '';
|
||||
filterJobs();
|
||||
}
|
||||
|
||||
// Render jobs table
|
||||
function renderJobs() {
|
||||
const tbody = document.getElementById('jobs-table-body');
|
||||
|
||||
if (filteredJobs.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center">
|
||||
<svg class="h-16 w-16 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 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>
|
||||
<p class="text-mercedes-gray text-lg">Keine Aufträge gefunden</p>
|
||||
<a href="/new-job" class="mt-4 inline-block bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
Ersten Auftrag erstellen
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = (currentPage - 1) * jobsPerPage;
|
||||
const endIndex = startIndex + jobsPerPage;
|
||||
const pageJobs = filteredJobs.slice(startIndex, endIndex);
|
||||
|
||||
tbody.innerHTML = pageJobs.map(job => {
|
||||
const printer = printers.find(p => p.id === job.printer_id);
|
||||
const statusColor = getJobStatusColor(job.status);
|
||||
const statusText = getJobStatusText(job.status);
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-mercedes-light transition-colors duration-200">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<svg class="h-8 w-8 text-mercedes-blue mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
|
||||
<div class="text-sm font-medium text-mercedes-black">${job.filename}</div>
|
||||
<div class="text-sm text-mercedes-gray">${formatFileSize(job.file_size)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}">
|
||||
${statusText}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-mercedes-black">
|
||||
${printer ? printer.name : 'Unbekannt'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="w-full bg-mercedes-silver rounded-full h-2">
|
||||
<div class="bg-mercedes-blue h-2 rounded-full" style="width: ${job.progress || 0}%"></div>
|
||||
</div>
|
||||
<div class="text-xs text-mercedes-gray mt-1">${job.progress || 0}%</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-mercedes-gray">
|
||||
${formatDate(job.created_at)}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="showJobDetail(${job.id})"
|
||||
class="text-mercedes-blue hover:text-blue-700 transition-colors duration-200">
|
||||
Details
|
||||
</button>
|
||||
${job.status === 'pending' ? `
|
||||
<button onclick="cancelJob(${job.id})"
|
||||
class="text-mercedes-red hover:text-red-700 transition-colors duration-200">
|
||||
Abbrechen
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function getJobStatusColor(status) {
|
||||
switch (status) {
|
||||
case 'pending': return 'bg-mercedes-yellow text-mercedes-black';
|
||||
case 'printing': return 'bg-mercedes-blue text-white';
|
||||
case 'completed': return 'bg-mercedes-green text-white';
|
||||
case 'failed': return 'bg-mercedes-red text-white';
|
||||
case 'cancelled': return 'bg-mercedes-gray text-white';
|
||||
default: return 'bg-mercedes-silver text-mercedes-black';
|
||||
}
|
||||
}
|
||||
|
||||
function getJobStatusText(status) {
|
||||
switch (status) {
|
||||
case 'pending': return 'Wartend';
|
||||
case 'printing': return 'Druckt';
|
||||
case 'completed': return 'Abgeschlossen';
|
||||
case 'failed': return 'Fehlgeschlagen';
|
||||
case 'cancelled': return 'Abgebrochen';
|
||||
default: return 'Unbekannt';
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
function updatePagination() {
|
||||
const totalPages = Math.ceil(filteredJobs.length / jobsPerPage);
|
||||
const startIndex = (currentPage - 1) * jobsPerPage + 1;
|
||||
const endIndex = Math.min(currentPage * jobsPerPage, filteredJobs.length);
|
||||
|
||||
document.getElementById('pagination-info').textContent =
|
||||
`Zeige ${startIndex}-${endIndex} von ${filteredJobs.length} Aufträgen`;
|
||||
|
||||
document.getElementById('prev-page').disabled = currentPage === 1;
|
||||
document.getElementById('next-page').disabled = currentPage === totalPages || totalPages === 0;
|
||||
|
||||
// Update page numbers
|
||||
const pageNumbers = document.getElementById('page-numbers');
|
||||
pageNumbers.innerHTML = '';
|
||||
|
||||
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = i;
|
||||
button.onclick = () => goToPage(i);
|
||||
button.className = `px-3 py-1 border rounded-lg text-sm transition-all duration-200 ${
|
||||
i === currentPage
|
||||
? 'bg-mercedes-blue text-white border-mercedes-blue'
|
||||
: 'border-mercedes-silver hover:bg-mercedes-light'
|
||||
}`;
|
||||
pageNumbers.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
function changePage(delta) {
|
||||
const totalPages = Math.ceil(filteredJobs.length / jobsPerPage);
|
||||
const newPage = currentPage + delta;
|
||||
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
currentPage = newPage;
|
||||
renderJobs();
|
||||
updatePagination();
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(page) {
|
||||
currentPage = page;
|
||||
renderJobs();
|
||||
updatePagination();
|
||||
}
|
||||
|
||||
// Job actions
|
||||
async function showJobDetail(jobId) {
|
||||
const job = jobs.find(j => j.id === jobId);
|
||||
if (!job) return;
|
||||
|
||||
const printer = printers.find(p => p.id === job.printer_id);
|
||||
const statusColor = getJobStatusColor(job.status);
|
||||
const statusText = getJobStatusText(job.status);
|
||||
|
||||
const content = document.getElementById('job-detail-content');
|
||||
content.innerHTML = `
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 class="font-medium text-mercedes-black mb-2">Dateiinformationen</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div><span class="font-medium text-mercedes-gray">Name:</span> ${job.filename}</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Größe:</span> ${formatFileSize(job.file_size)}</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Typ:</span> ${job.file_type || 'Unbekannt'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-medium text-mercedes-black mb-2">Druckstatus</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-mercedes-gray">Status:</span>
|
||||
<span class="ml-2 ${statusColor} px-2 py-1 rounded text-xs">${statusText}</span>
|
||||
</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Fortschritt:</span> ${job.progress || 0}%</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Drucker:</span> ${printer ? printer.name : 'Unbekannt'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-medium text-mercedes-black mb-2">Zeitstempel</h3>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div><span class="font-medium text-mercedes-gray">Erstellt:</span> ${formatDate(job.created_at)}</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Gestartet:</span> ${job.started_at ? formatDate(job.started_at) : 'Noch nicht gestartet'}</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Beendet:</span> ${job.completed_at ? formatDate(job.completed_at) : 'Noch nicht beendet'}</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Geschätzte Zeit:</span> ${job.estimated_time ? formatDuration(job.estimated_time) : 'Unbekannt'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${job.error_message ? `
|
||||
<div>
|
||||
<h3 class="font-medium text-mercedes-red mb-2">Fehlermeldung</h3>
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">
|
||||
${job.error_message}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="flex space-x-3 pt-4 border-t border-mercedes-silver">
|
||||
${job.status === 'pending' ? `
|
||||
<button onclick="cancelJob(${job.id}); hideJobDetailModal();"
|
||||
class="bg-mercedes-red hover:bg-red-700 text-white py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
|
||||
Auftrag abbrechen
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
<button onclick="hideJobDetailModal()"
|
||||
class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('jobDetailModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideJobDetailModal() {
|
||||
document.getElementById('jobDetailModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function cancelJob(jobId) {
|
||||
if (!confirm('Sind Sie sicher, dass Sie diesen Auftrag abbrechen möchten?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiCall(`/api/jobs/${jobId}/cancel`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
showFlashMessage('Auftrag erfolgreich abgebrochen', 'success');
|
||||
loadJobs();
|
||||
} catch (error) {
|
||||
showFlashMessage('Fehler beim Abbrechen des Auftrags', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh jobs
|
||||
function refreshJobs() {
|
||||
showFlashMessage('Aufträge werden aktualisiert...', 'info');
|
||||
loadJobs();
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadJobs();
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(loadJobs, 30000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user