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:
2025-05-24 18:58:17 +02:00
parent ead75ae451
commit 62e131c02f
19 changed files with 3433 additions and 105 deletions

View 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 %}