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

View File

@ -7,7 +7,7 @@ from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Any, Union from typing import Dict, List, Optional, Tuple, Any, Union
from functools import wraps from functools import wraps
from flask import Flask, request, jsonify, session from flask import Flask, request, jsonify, session, render_template, redirect, url_for
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
import sqlalchemy.exc import sqlalchemy.exc
from PyP100 import PyP110 from PyP100 import PyP110
@ -972,6 +972,81 @@ def stop_scheduler_api():
"running": scheduler.is_running() "running": scheduler.is_running()
}) })
# Frontend-Routen
@app.route("/")
def index():
"""Hauptseite - Weiterleitung zum Dashboard oder Login."""
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
return redirect(url_for('login_page'))
@app.route("/login")
def login_page():
"""Login-Seite."""
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
return render_template('login.html')
@app.route("/dashboard")
@login_required
def dashboard():
"""Dashboard-Seite."""
return render_template('dashboard.html')
@app.route("/printers")
@login_required
def printers_page():
"""Drucker-Übersichtsseite."""
return render_template('printers.html')
@app.route("/jobs")
@login_required
def jobs_page():
"""Jobs-Übersichtsseite."""
return render_template('jobs.html')
@app.route("/jobs/new")
@login_required
def new_job_page():
"""Neuen Job erstellen."""
return render_template('job_create.html')
@app.route("/jobs/<int:job_id>")
@login_required
@job_owner_required
def job_detail_page(job_id):
"""Job-Detailseite."""
return render_template('job_detail.html', job_id=job_id)
@app.route("/stats")
@login_required
def stats_page():
"""Statistiken-Seite."""
return render_template('stats.html')
@app.route("/admin")
@login_required
@admin_required
def admin_page():
"""Admin-Panel."""
return render_template('admin.html')
@app.route("/profile")
@login_required
def profile_page():
"""Benutzerprofil."""
return render_template('profile.html')
# Scheduler starten # Scheduler starten
def start_scheduler(): def start_scheduler():
"""Initialisiert und startet den Scheduler mit den erforderlichen Tasks.""" """Initialisiert und startet den Scheduler mit den erforderlichen Tasks."""

65
backend/app/static/css/tailwind.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" version="1.1" id="svg3544" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 80 80"
xml:space="preserve" width="800px" height="800px">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 976 B

View File

@ -0,0 +1,504 @@
<!DOCTYPE html>
<html lang="de" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}MYP - Mercedes 3D Printing Platform{% endblock %}</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Custom CSS für Mercedes Farben -->
<style>
:root {
--mercedes-silver: #C0C0C0;
--mercedes-dark-gray: #2D2D2D;
--mercedes-light-gray: #F5F5F5;
--mercedes-blue: #0066CC;
--mercedes-green: #00B04F;
--mercedes-red: #E60012;
--mercedes-yellow: #FFD700;
--mercedes-black: #000000;
--mercedes-white: #FFFFFF;
}
/* Mercedes Color Classes */
.bg-mercedes-silver { background-color: var(--mercedes-silver); }
.bg-mercedes-dark-gray { background-color: var(--mercedes-dark-gray); }
.bg-mercedes-light-gray { background-color: var(--mercedes-light-gray); }
.bg-mercedes-blue { background-color: var(--mercedes-blue); }
.bg-mercedes-green { background-color: var(--mercedes-green); }
.bg-mercedes-red { background-color: var(--mercedes-red); }
.bg-mercedes-yellow { background-color: var(--mercedes-yellow); }
.bg-mercedes-black { background-color: var(--mercedes-black); }
.bg-mercedes-white { background-color: var(--mercedes-white); }
.text-mercedes-silver { color: var(--mercedes-silver); }
.text-mercedes-dark-gray { color: var(--mercedes-dark-gray); }
.text-mercedes-light-gray { color: var(--mercedes-light-gray); }
.text-mercedes-blue { color: var(--mercedes-blue); }
.text-mercedes-green { color: var(--mercedes-green); }
.text-mercedes-red { color: var(--mercedes-red); }
.text-mercedes-yellow { color: var(--mercedes-yellow); }
.text-mercedes-black { color: var(--mercedes-black); }
.text-mercedes-white { color: var(--mercedes-white); }
.border-mercedes-silver { border-color: var(--mercedes-silver); }
.border-mercedes-dark-gray { border-color: var(--mercedes-dark-gray); }
.border-mercedes-light-gray { border-color: var(--mercedes-light-gray); }
.border-mercedes-blue { border-color: var(--mercedes-blue); }
.border-mercedes-green { border-color: var(--mercedes-green); }
.border-mercedes-red { border-color: var(--mercedes-red); }
.border-mercedes-yellow { border-color: var(--mercedes-yellow); }
.border-mercedes-black { border-color: var(--mercedes-black); }
.border-mercedes-white { border-color: var(--mercedes-white); }
/* Mercedes Gradient */
.mercedes-gradient {
background: linear-gradient(135deg, var(--mercedes-black) 0%, var(--mercedes-dark-gray) 100%);
}
/* Mercedes Shadow */
.mercedes-shadow {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.mercedes-shadow-lg {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Mercedes Button */
.mercedes-button {
transition: all 0.2s ease-in-out;
border-radius: 0.5rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.mercedes-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.mercedes-button:active {
transform: translateY(0);
}
/* Mercedes Card */
.mercedes-card {
background: var(--mercedes-white);
border: 1px solid var(--mercedes-light-gray);
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.mercedes-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
/* Mercedes Input */
.mercedes-input {
border: 2px solid var(--mercedes-light-gray);
border-radius: 0.5rem;
padding: 0.75rem 1rem;
transition: all 0.2s ease-in-out;
background: var(--mercedes-white);
}
.mercedes-input:focus {
outline: none;
border-color: var(--mercedes-blue);
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
/* Mercedes Table */
.mercedes-table {
background: var(--mercedes-white);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.mercedes-table th {
background: var(--mercedes-dark-gray);
color: var(--mercedes-white);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 1rem;
}
.mercedes-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--mercedes-light-gray);
}
.mercedes-table tr:hover {
background: var(--mercedes-light-gray);
}
/* Mercedes Modal */
.mercedes-modal {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.mercedes-modal-content {
background: var(--mercedes-white);
border-radius: 1rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
max-height: 90vh;
overflow-y: auto;
}
/* Mercedes Progress Bar */
.mercedes-progress {
background: var(--mercedes-light-gray);
border-radius: 9999px;
overflow: hidden;
}
.mercedes-progress-bar {
background: linear-gradient(90deg, var(--mercedes-blue), var(--mercedes-green));
height: 100%;
transition: width 0.3s ease-in-out;
}
/* Mercedes Status Badges */
.status-online { background: var(--mercedes-green); color: white; }
.status-offline { background: var(--mercedes-red); color: white; }
.status-busy { background: var(--mercedes-yellow); color: var(--mercedes-black); }
.status-maintenance { background: var(--mercedes-silver); color: var(--mercedes-black); }
.status-pending { background: var(--mercedes-yellow); color: var(--mercedes-black); }
.status-printing { background: var(--mercedes-blue); color: white; }
.status-completed { background: var(--mercedes-green); color: white; }
.status-failed { background: var(--mercedes-red); color: white; }
.status-cancelled { background: var(--mercedes-silver); color: var(--mercedes-black); }
/* Mercedes Navigation */
.mercedes-nav-item {
position: relative;
transition: all 0.2s ease-in-out;
}
.mercedes-nav-item:hover::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: var(--mercedes-silver);
}
/* Mercedes Animations */
@keyframes mercedes-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.mercedes-pulse {
animation: mercedes-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes mercedes-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.mercedes-spin {
animation: mercedes-spin 1s linear infinite;
}
/* Mercedes Responsive */
@media (max-width: 768px) {
.mercedes-card {
margin: 0.5rem;
}
.mercedes-table {
font-size: 0.875rem;
}
.mercedes-button {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
}
/* Mercedes Dark Mode Support */
@media (prefers-color-scheme: dark) {
.mercedes-card {
background: var(--mercedes-dark-gray);
border-color: var(--mercedes-silver);
color: var(--mercedes-white);
}
.mercedes-input {
background: var(--mercedes-dark-gray);
color: var(--mercedes-white);
border-color: var(--mercedes-silver);
}
}
</style>
{% block head %}{% endblock %}
</head>
<body class="h-full bg-gradient-to-br from-mercedes-light-gray to-white font-sans">
<!-- Navigation -->
<nav class="mercedes-gradient mercedes-shadow sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Logo und Marke -->
<div class="flex items-center space-x-4">
<div class="flex-shrink-0">
<svg class="h-10 w-10 text-mercedes-silver" fill="currentColor" viewBox="0 0 80 80">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
</div>
<div class="text-white">
<h1 class="text-xl font-bold tracking-wide">MYP</h1>
<p class="text-xs text-mercedes-silver">3D Printing Platform</p>
</div>
</div>
<!-- Navigation Links -->
<div class="hidden md:block">
<div class="ml-10 flex items-baseline space-x-8">
<a href="/dashboard" class="mercedes-nav-item text-white hover:text-mercedes-silver px-3 py-2 text-sm font-medium transition-colors duration-200">
Dashboard
</a>
<a href="/printers" class="mercedes-nav-item text-white hover:text-mercedes-silver px-3 py-2 text-sm font-medium transition-colors duration-200">
Drucker
</a>
<a href="/jobs" class="mercedes-nav-item text-white hover:text-mercedes-silver px-3 py-2 text-sm font-medium transition-colors duration-200">
Jobs
</a>
<a href="/stats" class="mercedes-nav-item text-white hover:text-mercedes-silver px-3 py-2 text-sm font-medium transition-colors duration-200">
Statistiken
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="/admin" class="mercedes-nav-item text-mercedes-yellow hover:text-white px-3 py-2 text-sm font-medium transition-colors duration-200">
Admin
</a>
{% endif %}
</div>
</div>
<!-- User Menu -->
<div class="flex items-center space-x-4">
{% if current_user.is_authenticated %}
<div class="text-white text-sm">
<span class="text-mercedes-silver">Willkommen,</span>
<span class="font-medium">{{ current_user.email }}</span>
</div>
<button onclick="logout()" class="bg-mercedes-red hover:bg-red-700 text-white px-4 py-2 rounded-lg text-sm font-medium mercedes-button transition-all duration-200">
Abmelden
</button>
{% else %}
<a href="/login" class="bg-mercedes-blue hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium mercedes-button transition-all duration-200">
Anmelden
</a>
{% endif %}
</div>
<!-- Mobile menu button -->
<div class="md:hidden">
<button type="button" class="text-white hover:text-mercedes-silver focus:outline-none focus:text-mercedes-silver" onclick="toggleMobileMenu()">
<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="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile menu -->
<div id="mobile-menu" class="md:hidden hidden">
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-mercedes-gray">
<a href="/dashboard" class="text-white hover:text-mercedes-silver block px-3 py-2 text-base font-medium">Dashboard</a>
<a href="/printers" class="text-white hover:text-mercedes-silver block px-3 py-2 text-base font-medium">Drucker</a>
<a href="/jobs" class="text-white hover:text-mercedes-silver block px-3 py-2 text-base font-medium">Jobs</a>
<a href="/stats" class="text-white hover:text-mercedes-silver block px-3 py-2 text-base font-medium">Statistiken</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="/admin" class="text-mercedes-yellow hover:text-white block px-3 py-2 text-base font-medium">Admin</a>
{% endif %}
</div>
</div>
</nav>
<!-- Flash Messages -->
<div id="flash-messages" class="fixed top-20 right-4 z-40 space-y-2">
<!-- Flash messages will be inserted here by JavaScript -->
</div>
<!-- Main Content -->
<main class="min-h-screen">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="mercedes-gradient text-white py-8 mt-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="flex items-center space-x-4 mb-4 md:mb-0">
<svg class="h-8 w-8 text-mercedes-silver" fill="currentColor" viewBox="0 0 80 80">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
<div>
<p class="text-sm font-medium">MYP - 3D Printing Platform</p>
<p class="text-xs text-mercedes-silver">Powered by Mercedes Excellence</p>
</div>
</div>
<div class="text-center md:text-right">
<p class="text-sm text-mercedes-silver">© 2024 MYP Platform. Alle Rechte vorbehalten.</p>
<p class="text-xs text-mercedes-silver mt-1">Version 1.0</p>
</div>
</div>
</div>
</footer>
<!-- JavaScript -->
<script>
// Mobile menu toggle
function toggleMobileMenu() {
const menu = document.getElementById('mobile-menu');
menu.classList.toggle('hidden');
}
// Logout function
async function logout() {
try {
const response = await fetch('/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
window.location.href = '/login';
} else {
showFlashMessage('Fehler beim Abmelden', 'error');
}
} catch (error) {
showFlashMessage('Netzwerkfehler beim Abmelden', 'error');
}
}
// Flash message system
function showFlashMessage(message, type = 'info') {
const container = document.getElementById('flash-messages');
const messageDiv = document.createElement('div');
let bgColor = 'bg-mercedes-blue';
let textColor = 'text-white';
switch(type) {
case 'success':
bgColor = 'bg-mercedes-green';
break;
case 'error':
bgColor = 'bg-mercedes-red';
break;
case 'warning':
bgColor = 'bg-mercedes-yellow';
textColor = 'text-mercedes-black';
break;
}
messageDiv.className = `${bgColor} ${textColor} px-6 py-3 rounded-lg shadow-lg mercedes-shadow transform transition-all duration-300 translate-x-full`;
messageDiv.innerHTML = `
<div class="flex items-center justify-between">
<span class="font-medium">${message}</span>
<button onclick="this.parentElement.parentElement.remove()" class="ml-4 text-lg font-bold hover:opacity-75">×</button>
</div>
`;
container.appendChild(messageDiv);
// Animate in
setTimeout(() => {
messageDiv.classList.remove('translate-x-full');
}, 100);
// Auto remove after 5 seconds
setTimeout(() => {
messageDiv.classList.add('translate-x-full');
setTimeout(() => {
if (messageDiv.parentElement) {
messageDiv.remove();
}
}, 300);
}, 5000);
}
// API helper function
async function apiCall(url, options = {}) {
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'API-Fehler');
}
return data;
} catch (error) {
showFlashMessage(error.message, 'error');
throw error;
}
}
// Format date helper
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// Format duration helper
function formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
} else {
return `${secs}s`;
}
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,461 @@
{% extends "base.html" %}
{% block title %}Dashboard - 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">Dashboard</h1>
<p class="mt-2 text-mercedes-gray">Willkommen zurück! Hier ist Ihre Übersicht.</p>
</div>
<div class="flex space-x-4">
<button onclick="refreshDashboard()" 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="/jobs/new" 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 Job
</a>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Active Jobs -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="bg-mercedes-green p-3 rounded-full">
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-mercedes-gray">Aktive Jobs</p>
<p class="text-2xl font-bold text-mercedes-black" id="active-jobs-count">-</p>
</div>
</div>
</div>
<!-- Scheduled Jobs -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="bg-mercedes-blue p-3 rounded-full">
<svg class="h-6 w-6 text-white" 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>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-mercedes-gray">Geplante Jobs</p>
<p class="text-2xl font-bold text-mercedes-black" id="scheduled-jobs-count">-</p>
</div>
</div>
</div>
<!-- Available Printers -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="bg-mercedes-yellow p-3 rounded-full">
<svg class="h-6 w-6 text-mercedes-black" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-mercedes-gray">Verfügbare Drucker</p>
<p class="text-2xl font-bold text-mercedes-black" id="available-printers-count">-</p>
</div>
</div>
</div>
<!-- Total Print Time -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="bg-mercedes-silver p-3 rounded-full">
<svg class="h-6 w-6 text-mercedes-black" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-mercedes-gray">Gesamte Druckzeit</p>
<p class="text-2xl font-bold text-mercedes-black" id="total-print-time">-</p>
</div>
</div>
</div>
</div>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Recent Jobs -->
<div class="lg:col-span-2">
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-mercedes-black">Aktuelle Jobs</h2>
<a href="/jobs" class="text-mercedes-blue hover:text-blue-700 text-sm font-medium transition-colors duration-200">
Alle anzeigen →
</a>
</div>
<div id="recent-jobs" class="space-y-4">
<!-- Jobs will be loaded here -->
<div class="text-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-mercedes-blue mx-auto"></div>
<p class="mt-2 text-mercedes-gray">Lade Jobs...</p>
</div>
</div>
</div>
</div>
<!-- Quick Actions & System Status -->
<div class="space-y-6">
<!-- Quick Actions -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<h2 class="text-xl font-bold text-mercedes-black mb-6">Schnellaktionen</h2>
<div class="space-y-4">
<a href="/jobs/new" class="block w-full bg-mercedes-green hover:bg-green-700 text-white text-center py-3 px-4 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>
Neuen Job erstellen
</a>
<a href="/printers" class="block w-full bg-mercedes-blue hover:bg-blue-700 text-white text-center py-3 px-4 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="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
Drucker verwalten
</a>
<a href="/stats" class="block w-full bg-mercedes-silver hover:bg-gray-400 text-mercedes-black text-center py-3 px-4 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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Statistiken anzeigen
</a>
</div>
</div>
<!-- System Status -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<h2 class="text-xl font-bold text-mercedes-black mb-6">Systemstatus</h2>
<div class="space-y-4">
<!-- Scheduler Status -->
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-mercedes-gray">Job-Scheduler</span>
<div class="flex items-center">
<div id="scheduler-status" class="h-3 w-3 rounded-full bg-mercedes-gray mr-2"></div>
<span id="scheduler-text" class="text-sm text-mercedes-gray">Prüfe...</span>
</div>
</div>
<!-- Database Status -->
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-mercedes-gray">Datenbank</span>
<div class="flex items-center">
<div class="h-3 w-3 rounded-full bg-mercedes-green mr-2"></div>
<span class="text-sm text-mercedes-green">Online</span>
</div>
</div>
<!-- API Status -->
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-mercedes-gray">API</span>
<div class="flex items-center">
<div class="h-3 w-3 rounded-full bg-mercedes-green mr-2"></div>
<span class="text-sm text-mercedes-green">Verfügbar</span>
</div>
</div>
</div>
{% if current_user.is_admin %}
<div class="mt-6 pt-4 border-t border-mercedes-silver">
<a href="/admin" class="block w-full bg-mercedes-yellow hover:bg-yellow-500 text-mercedes-black text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200 text-sm font-medium">
<svg class="h-4 w-4 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Admin-Panel
</a>
</div>
{% endif %}
</div>
<!-- Recent Activity -->
<div class="mercedes-card rounded-xl p-6 mercedes-shadow">
<h2 class="text-xl font-bold text-mercedes-black mb-6">Letzte Aktivitäten</h2>
<div id="recent-activity" class="space-y-3">
<!-- Activity will be loaded here -->
<div class="text-center py-4">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-mercedes-blue mx-auto"></div>
<p class="mt-2 text-sm text-mercedes-gray">Lade Aktivitäten...</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Dashboard data
let dashboardData = {
stats: {},
jobs: [],
printers: [],
schedulerStatus: false
};
// Load dashboard data
async function loadDashboardData() {
try {
// Load stats
const statsResponse = await apiCall('/api/stats');
dashboardData.stats = statsResponse;
// Load jobs
const jobsResponse = await apiCall('/api/jobs');
dashboardData.jobs = jobsResponse.jobs || [];
// Load printers
const printersResponse = await apiCall('/api/printers');
dashboardData.printers = printersResponse.printers || [];
// Load scheduler status (try to load, will fail if not admin)
try {
const schedulerResponse = await apiCall('/api/scheduler/status');
dashboardData.schedulerStatus = schedulerResponse.running;
} catch (error) {
console.log('Scheduler status not available (not admin or error)');
dashboardData.schedulerStatus = false;
}
updateDashboard();
} catch (error) {
console.error('Error loading dashboard data:', error);
showFlashMessage('Fehler beim Laden der Dashboard-Daten', 'error');
}
}
// Update dashboard display
function updateDashboard() {
updateStats();
updateRecentJobs();
updateSystemStatus();
updateRecentActivity();
}
// Update stats cards
function updateStats() {
const stats = dashboardData.stats;
const jobs = dashboardData.jobs;
const printers = dashboardData.printers;
// Active jobs
const activeJobs = jobs.filter(job => job.status === 'active').length;
document.getElementById('active-jobs-count').textContent = activeJobs;
// Scheduled jobs
const scheduledJobs = jobs.filter(job => job.status === 'scheduled').length;
document.getElementById('scheduled-jobs-count').textContent = scheduledJobs;
// Available printers
const availablePrinters = printers.filter(printer => printer.status === 'available').length;
document.getElementById('available-printers-count').textContent = availablePrinters;
// Total print time
const totalTime = stats.total_print_time || 0;
document.getElementById('total-print-time').textContent = formatDuration(totalTime);
}
// Update recent jobs
function updateRecentJobs() {
const container = document.getElementById('recent-jobs');
const recentJobs = dashboardData.jobs
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
.slice(0, 5);
if (recentJobs.length === 0) {
container.innerHTML = `
<div class="text-center py-8">
<svg class="h-12 w-12 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 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p class="text-mercedes-gray">Noch keine Jobs vorhanden</p>
<a href="/jobs/new" class="mt-2 inline-block text-mercedes-blue hover:text-blue-700 font-medium">Ersten Job erstellen</a>
</div>
`;
return;
}
container.innerHTML = recentJobs.map(job => {
const statusColor = getJobStatusColor(job.status);
const statusText = getJobStatusText(job.status);
return `
<div class="border border-mercedes-silver rounded-lg p-4 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div class="flex-1">
<h3 class="font-medium text-mercedes-black">${job.title}</h3>
<p class="text-sm text-mercedes-gray mt-1">
Drucker: ${job.printer_name || 'Unbekannt'} •
Erstellt: ${formatDate(job.created_at)}
</p>
</div>
<div class="ml-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}">
${statusText}
</span>
</div>
</div>
<div class="mt-3 flex items-center justify-between">
<div class="text-sm text-mercedes-gray">
${job.start_time ? `Start: ${formatDate(job.start_time)}` : 'Kein Startzeit'}
</div>
<a href="/jobs/${job.id}" class="text-mercedes-blue hover:text-blue-700 text-sm font-medium">
Details →
</a>
</div>
</div>
`;
}).join('');
}
// Update system status
function updateSystemStatus() {
const schedulerStatus = document.getElementById('scheduler-status');
const schedulerText = document.getElementById('scheduler-text');
if (dashboardData.schedulerStatus) {
schedulerStatus.className = 'h-3 w-3 rounded-full bg-mercedes-green mr-2';
schedulerText.textContent = 'Aktiv';
schedulerText.className = 'text-sm text-mercedes-green';
} else {
schedulerStatus.className = 'h-3 w-3 rounded-full bg-mercedes-red mr-2';
schedulerText.textContent = 'Inaktiv';
schedulerText.className = 'text-sm text-mercedes-red';
}
}
// Update recent activity
function updateRecentActivity() {
const container = document.getElementById('recent-activity');
const activities = [];
// Generate activity from recent jobs
dashboardData.jobs
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
.slice(0, 5)
.forEach(job => {
activities.push({
type: 'job_created',
message: `Job "${job.title}" erstellt`,
time: job.created_at,
icon: 'plus'
});
if (job.status === 'active') {
activities.push({
type: 'job_started',
message: `Job "${job.title}" gestartet`,
time: job.start_time,
icon: 'play'
});
}
});
// Sort by time
activities.sort((a, b) => new Date(b.time) - new Date(a.time));
if (activities.length === 0) {
container.innerHTML = `
<div class="text-center py-4">
<p class="text-sm text-mercedes-gray">Keine Aktivitäten</p>
</div>
`;
return;
}
container.innerHTML = activities.slice(0, 5).map(activity => {
const icon = getActivityIcon(activity.icon);
return `
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<div class="bg-mercedes-blue p-1 rounded-full">
${icon}
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm text-mercedes-black">${activity.message}</p>
<p class="text-xs text-mercedes-gray">${formatDate(activity.time)}</p>
</div>
</div>
`;
}).join('');
}
// Helper functions
function getJobStatusColor(status) {
switch (status) {
case 'active': return 'bg-mercedes-green text-white';
case 'scheduled': return 'bg-mercedes-blue text-white';
case 'completed': return 'bg-mercedes-silver text-mercedes-black';
case 'aborted': return 'bg-mercedes-red text-white';
default: return 'bg-mercedes-gray text-white';
}
}
function getJobStatusText(status) {
switch (status) {
case 'active': return 'Aktiv';
case 'scheduled': return 'Geplant';
case 'completed': return 'Abgeschlossen';
case 'aborted': return 'Abgebrochen';
default: return 'Unbekannt';
}
}
function getActivityIcon(type) {
switch (type) {
case 'plus':
return '<svg class="h-4 w-4 text-white" 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>';
case 'play':
return '<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1.586a1 1 0 01.707.293l2.414 2.414a1 1 0 00.707.293H15" /></svg>';
default:
return '<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><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" /></svg>';
}
}
// Refresh dashboard
function refreshDashboard() {
showFlashMessage('Dashboard wird aktualisiert...', 'info');
loadDashboardData();
}
// Initialize dashboard
document.addEventListener('DOMContentLoaded', function() {
loadDashboardData();
// Auto-refresh every 30 seconds
setInterval(loadDashboardData, 30000);
});
</script>
{% endblock %}

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

View File

@ -0,0 +1,384 @@
{% extends "base.html" %}
{% block title %}Anmelden - MYP Platform{% endblock %}
{% block content %}
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<!-- Logo und Header -->
<div class="text-center">
<div class="flex justify-center">
<svg class="h-20 w-20 text-mercedes-black" fill="currentColor" viewBox="0 0 80 80">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
</svg>
</div>
<h2 class="mt-6 text-3xl font-bold text-mercedes-black">
Willkommen bei MYP
</h2>
<p class="mt-2 text-sm text-mercedes-gray">
3D Printing Platform - Powered by Mercedes Excellence
</p>
</div>
<!-- Login Form -->
<div class="mercedes-card rounded-xl shadow-2xl p-8">
<form id="loginForm" class="space-y-6" onsubmit="handleLogin(event)">
<div>
<label for="email" class="block text-sm font-medium text-mercedes-black mb-2">
E-Mail-Adresse
</label>
<input
id="email"
name="email"
type="email"
required
class="w-full px-4 py-3 border-2 border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue transition-all duration-200 text-mercedes-black placeholder-mercedes-gray"
placeholder="ihre.email@beispiel.de"
>
</div>
<div>
<label for="password" class="block text-sm font-medium text-mercedes-black mb-2">
Passwort
</label>
<div class="relative">
<input
id="password"
name="password"
type="password"
required
class="w-full px-4 py-3 border-2 border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue transition-all duration-200 text-mercedes-black placeholder-mercedes-gray pr-12"
placeholder="••••••••"
>
<button
type="button"
onclick="togglePassword()"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-mercedes-gray hover:text-mercedes-black transition-colors duration-200"
>
<svg id="eye-icon" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input
id="remember"
name="remember"
type="checkbox"
class="h-4 w-4 text-mercedes-blue focus:ring-mercedes-blue border-mercedes-silver rounded"
>
<label for="remember" class="ml-2 block text-sm text-mercedes-gray">
Angemeldet bleiben
</label>
</div>
</div>
<div>
<button
type="submit"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-mercedes-black hover:bg-mercedes-gray focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-mercedes-blue mercedes-button transition-all duration-200"
id="loginButton"
>
<span id="loginButtonText">Anmelden</span>
<svg id="loginSpinner" class="hidden animate-spin -mr-1 ml-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</div>
</form>
<!-- Divider -->
<div class="mt-6">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-mercedes-silver"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-mercedes-gray">Oder</span>
</div>
</div>
</div>
<!-- Register Link -->
<div class="mt-6 text-center">
<p class="text-sm text-mercedes-gray">
Noch kein Konto?
<button
onclick="showRegisterForm()"
class="font-medium text-mercedes-blue hover:text-blue-700 transition-colors duration-200"
>
Jetzt registrieren
</button>
</p>
</div>
</div>
<!-- Register Form (Hidden by default) -->
<div id="registerCard" class="mercedes-card rounded-xl shadow-2xl p-8 hidden">
<div class="text-center mb-6">
<h3 class="text-2xl font-bold text-mercedes-black">Registrierung</h3>
<p class="text-sm text-mercedes-gray mt-2">Erstellen Sie Ihr MYP-Konto</p>
</div>
<form id="registerForm" class="space-y-6" onsubmit="handleRegister(event)">
<div>
<label for="reg-email" class="block text-sm font-medium text-mercedes-black mb-2">
E-Mail-Adresse
</label>
<input
id="reg-email"
name="email"
type="email"
required
class="w-full px-4 py-3 border-2 border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue transition-all duration-200 text-mercedes-black placeholder-mercedes-gray"
placeholder="ihre.email@beispiel.de"
>
</div>
<div>
<label for="reg-password" class="block text-sm font-medium text-mercedes-black mb-2">
Passwort
</label>
<input
id="reg-password"
name="password"
type="password"
required
minlength="6"
class="w-full px-4 py-3 border-2 border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue transition-all duration-200 text-mercedes-black placeholder-mercedes-gray"
placeholder="Mindestens 6 Zeichen"
>
</div>
<div>
<label for="reg-password-confirm" class="block text-sm font-medium text-mercedes-black mb-2">
Passwort bestätigen
</label>
<input
id="reg-password-confirm"
name="password_confirm"
type="password"
required
class="w-full px-4 py-3 border-2 border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue transition-all duration-200 text-mercedes-black placeholder-mercedes-gray"
placeholder="Passwort wiederholen"
>
</div>
<div>
<button
type="submit"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-mercedes-green hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-mercedes-green mercedes-button transition-all duration-200"
id="registerButton"
>
<span id="registerButtonText">Registrieren</span>
<svg id="registerSpinner" class="hidden animate-spin -mr-1 ml-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</div>
</form>
<div class="mt-6 text-center">
<button
onclick="showLoginForm()"
class="text-sm font-medium text-mercedes-blue hover:text-blue-700 transition-colors duration-200"
>
← Zurück zur Anmeldung
</button>
</div>
</div>
<!-- Features -->
<div class="mt-12 grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center">
<div class="flex justify-center">
<div class="bg-mercedes-blue p-3 rounded-full">
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
</div>
<h3 class="mt-4 text-sm font-medium text-mercedes-black">Sicher</h3>
<p class="mt-2 text-xs text-mercedes-gray">Ihre Daten sind bei uns sicher</p>
</div>
<div class="text-center">
<div class="flex justify-center">
<div class="bg-mercedes-green p-3 rounded-full">
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
<h3 class="mt-4 text-sm font-medium text-mercedes-black">Schnell</h3>
<p class="mt-2 text-xs text-mercedes-gray">Blitzschnelle Druckaufträge</p>
</div>
<div class="text-center">
<div class="flex justify-center">
<div class="bg-mercedes-yellow p-3 rounded-full">
<svg class="h-6 w-6 text-mercedes-black" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<h3 class="mt-4 text-sm font-medium text-mercedes-black">Zuverlässig</h3>
<p class="mt-2 text-xs text-mercedes-gray">Mercedes Qualitätsstandards</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Toggle password visibility
function togglePassword() {
const passwordInput = document.getElementById('password');
const eyeIcon = document.getElementById('eye-icon');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
eyeIcon.innerHTML = `
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
`;
} else {
passwordInput.type = 'password';
eyeIcon.innerHTML = `
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
`;
}
}
// Show register form
function showRegisterForm() {
document.querySelector('.mercedes-card').classList.add('hidden');
document.getElementById('registerCard').classList.remove('hidden');
}
// Show login form
function showLoginForm() {
document.getElementById('registerCard').classList.add('hidden');
document.querySelector('.mercedes-card').classList.remove('hidden');
}
// Handle login
async function handleLogin(event) {
event.preventDefault();
const button = document.getElementById('loginButton');
const buttonText = document.getElementById('loginButtonText');
const spinner = document.getElementById('loginSpinner');
// Show loading state
button.disabled = true;
buttonText.textContent = 'Anmelden...';
spinner.classList.remove('hidden');
try {
const formData = new FormData(event.target);
const response = await fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: formData.get('email'),
password: formData.get('password'),
remember: formData.get('remember') === 'on'
})
});
const data = await response.json();
if (response.ok) {
showFlashMessage('Erfolgreich angemeldet!', 'success');
setTimeout(() => {
window.location.href = '/dashboard';
}, 1000);
} else {
showFlashMessage(data.error || 'Anmeldung fehlgeschlagen', 'error');
}
} catch (error) {
showFlashMessage('Netzwerkfehler bei der Anmeldung', 'error');
} finally {
// Reset button state
button.disabled = false;
buttonText.textContent = 'Anmelden';
spinner.classList.add('hidden');
}
}
// Handle registration
async function handleRegister(event) {
event.preventDefault();
const button = document.getElementById('registerButton');
const buttonText = document.getElementById('registerButtonText');
const spinner = document.getElementById('registerSpinner');
const formData = new FormData(event.target);
const password = formData.get('password');
const passwordConfirm = formData.get('password_confirm');
// Validate passwords match
if (password !== passwordConfirm) {
showFlashMessage('Passwörter stimmen nicht überein', 'error');
return;
}
// Show loading state
button.disabled = true;
buttonText.textContent = 'Registrieren...';
spinner.classList.remove('hidden');
try {
const response = await fetch('/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: formData.get('email'),
password: password
})
});
const data = await response.json();
if (response.ok) {
showFlashMessage('Registrierung erfolgreich! Sie können sich jetzt anmelden.', 'success');
setTimeout(() => {
showLoginForm();
}, 2000);
} else {
showFlashMessage(data.error || 'Registrierung fehlgeschlagen', 'error');
}
} catch (error) {
showFlashMessage('Netzwerkfehler bei der Registrierung', 'error');
} finally {
// Reset button state
button.disabled = false;
buttonText.textContent = 'Registrieren';
spinner.classList.add('hidden');
}
}
// Auto-focus email field on page load
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('email').focus();
});
</script>
{% endblock %}

View File

@ -0,0 +1,384 @@
{% extends "base.html" %}
{% block title %}Neuer Druckauftrag - MYP Platform{% endblock %}
{% block content %}
<div class="max-w-4xl 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">Neuer Druckauftrag</h1>
<p class="mt-2 text-mercedes-gray">Erstellen Sie einen neuen 3D-Druckauftrag</p>
</div>
<a href="/jobs" class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black 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="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Zurück zu Aufträgen
</a>
</div>
</div>
<!-- Job Creation Form -->
<div class="mercedes-card rounded-xl p-8">
<form id="jobForm" onsubmit="handleJobSubmission(event)" class="space-y-8">
<!-- File Upload Section -->
<div>
<h2 class="text-xl font-bold text-mercedes-black mb-4">Datei hochladen</h2>
<div id="file-upload-area" class="border-2 border-dashed border-mercedes-silver rounded-xl p-8 text-center hover:border-mercedes-blue transition-colors duration-200 cursor-pointer">
<input type="file" id="file-input" name="file" accept=".stl,.gcode,.3mf,.obj" class="hidden" onchange="handleFileSelect(event)">
<div id="upload-placeholder" class="space-y-4">
<svg class="h-16 w-16 text-mercedes-silver mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<div>
<p class="text-lg font-medium text-mercedes-black">Datei hier ablegen oder klicken zum Auswählen</p>
<p class="text-sm text-mercedes-gray mt-2">Unterstützte Formate: STL, GCODE, 3MF, OBJ (max. 100MB)</p>
</div>
</div>
<div id="file-preview" class="hidden space-y-4">
<svg class="h-16 w-16 text-mercedes-green mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p id="file-name" class="text-lg font-medium text-mercedes-black"></p>
<p id="file-size" class="text-sm text-mercedes-gray"></p>
</div>
<button type="button" onclick="clearFile()" class="text-mercedes-red hover:text-red-700 text-sm transition-colors duration-200">
Datei entfernen
</button>
</div>
</div>
</div>
<!-- Job Settings Section -->
<div>
<h2 class="text-xl font-bold text-mercedes-black mb-4">Druckeinstellungen</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Printer Selection -->
<div>
<label for="printer-select" class="block text-sm font-medium text-mercedes-black mb-2">
Drucker auswählen *
</label>
<select id="printer-select" name="printer_id" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="">Drucker auswählen...</option>
</select>
<p class="mt-1 text-xs text-mercedes-gray">Nur verfügbare Drucker werden angezeigt</p>
</div>
<!-- Priority -->
<div>
<label for="priority-select" class="block text-sm font-medium text-mercedes-black mb-2">
Priorität
</label>
<select id="priority-select" name="priority"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="normal">Normal</option>
<option value="high">Hoch</option>
<option value="urgent">Dringend</option>
</select>
</div>
<!-- Material -->
<div>
<label for="material-input" class="block text-sm font-medium text-mercedes-black mb-2">
Material
</label>
<input type="text" id="material-input" name="material" placeholder="z.B. PLA, ABS, PETG"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<!-- Color -->
<div>
<label for="color-input" class="block text-sm font-medium text-mercedes-black mb-2">
Farbe
</label>
<input type="text" id="color-input" name="color" placeholder="z.B. Weiß, Schwarz, Rot"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<!-- Layer Height -->
<div>
<label for="layer-height-select" class="block text-sm font-medium text-mercedes-black mb-2">
Schichthöhe
</label>
<select id="layer-height-select" name="layer_height"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="">Standard</option>
<option value="0.1">0.1mm (Hoch)</option>
<option value="0.2">0.2mm (Normal)</option>
<option value="0.3">0.3mm (Schnell)</option>
</select>
</div>
<!-- Infill -->
<div>
<label for="infill-select" class="block text-sm font-medium text-mercedes-black mb-2">
Füllung
</label>
<select id="infill-select" name="infill"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<option value="">Standard</option>
<option value="10">10% (Leicht)</option>
<option value="20">20% (Normal)</option>
<option value="50">50% (Stabil)</option>
<option value="100">100% (Massiv)</option>
</select>
</div>
</div>
</div>
<!-- Notes Section -->
<div>
<h2 class="text-xl font-bold text-mercedes-black mb-4">Zusätzliche Informationen</h2>
<div>
<label for="notes-textarea" class="block text-sm font-medium text-mercedes-black mb-2">
Notizen
</label>
<textarea id="notes-textarea" name="notes" rows="4"
placeholder="Besondere Anweisungen oder Hinweise für den Druck..."
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue"></textarea>
</div>
</div>
<!-- Submit Section -->
<div class="flex items-center justify-between pt-6 border-t border-mercedes-silver">
<div class="text-sm text-mercedes-gray">
<p>* Pflichtfelder</p>
</div>
<div class="flex space-x-4">
<button type="button" onclick="resetForm()"
class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
Zurücksetzen
</button>
<button type="submit" id="submit-button" disabled
class="bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed">
<span id="submit-text">Auftrag erstellen</span>
<svg id="submit-spinner" class="hidden animate-spin h-5 w-5 inline ml-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>
</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let selectedFile = null;
let printers = [];
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadPrinters();
setupFileUpload();
});
// Load available printers
async function loadPrinters() {
try {
const response = await apiCall('/api/printers');
printers = response.printers || [];
const select = document.getElementById('printer-select');
select.innerHTML = '<option value="">Drucker auswählen...</option>';
// Only show available printers
const availablePrinters = printers.filter(p => p.status === 'available');
if (availablePrinters.length === 0) {
select.innerHTML = '<option value="">Keine verfügbaren Drucker</option>';
select.disabled = true;
showFlashMessage('Derzeit sind keine Drucker verfügbar', 'warning');
return;
}
availablePrinters.forEach(printer => {
const option = document.createElement('option');
option.value = printer.id;
option.textContent = `${printer.name} (${printer.location})`;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading printers:', error);
showFlashMessage('Fehler beim Laden der Drucker', 'error');
}
}
// Setup file upload
function setupFileUpload() {
const uploadArea = document.getElementById('file-upload-area');
const fileInput = document.getElementById('file-input');
// Click to select file
uploadArea.addEventListener('click', () => {
if (!selectedFile) {
fileInput.click();
}
});
// Drag and drop
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('border-mercedes-blue', 'bg-blue-50');
});
uploadArea.addEventListener('dragleave', (e) => {
e.preventDefault();
uploadArea.classList.remove('border-mercedes-blue', 'bg-blue-50');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('border-mercedes-blue', 'bg-blue-50');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileSelect({ target: { files } });
}
});
}
// Handle file selection
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
// Validate file type
const allowedTypes = ['.stl', '.gcode', '.3mf', '.obj'];
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
if (!allowedTypes.includes(fileExtension)) {
showFlashMessage('Ungültiger Dateityp. Erlaubt sind: STL, GCODE, 3MF, OBJ', 'error');
return;
}
// Validate file size (100MB max)
const maxSize = 100 * 1024 * 1024; // 100MB in bytes
if (file.size > maxSize) {
showFlashMessage('Datei ist zu groß. Maximum: 100MB', 'error');
return;
}
selectedFile = file;
showFilePreview(file);
validateForm();
}
// Show file preview
function showFilePreview(file) {
document.getElementById('upload-placeholder').classList.add('hidden');
document.getElementById('file-preview').classList.remove('hidden');
document.getElementById('file-name').textContent = file.name;
document.getElementById('file-size').textContent = formatFileSize(file.size);
}
// Clear selected file
function clearFile() {
selectedFile = null;
document.getElementById('file-input').value = '';
document.getElementById('upload-placeholder').classList.remove('hidden');
document.getElementById('file-preview').classList.add('hidden');
validateForm();
}
// Validate form
function validateForm() {
const printerSelected = document.getElementById('printer-select').value;
const submitButton = document.getElementById('submit-button');
const isValid = selectedFile && printerSelected;
submitButton.disabled = !isValid;
}
// Form validation on change
document.getElementById('printer-select').addEventListener('change', validateForm);
// Handle form submission
async function handleJobSubmission(event) {
event.preventDefault();
if (!selectedFile) {
showFlashMessage('Bitte wählen Sie eine Datei aus', 'error');
return;
}
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('printer_id', document.getElementById('printer-select').value);
formData.append('priority', document.getElementById('priority-select').value);
formData.append('material', document.getElementById('material-input').value);
formData.append('color', document.getElementById('color-input').value);
formData.append('layer_height', document.getElementById('layer-height-select').value);
formData.append('infill', document.getElementById('infill-select').value);
formData.append('notes', document.getElementById('notes-textarea').value);
// Show loading state
const submitButton = document.getElementById('submit-button');
const submitText = document.getElementById('submit-text');
const submitSpinner = document.getElementById('submit-spinner');
submitButton.disabled = true;
submitText.textContent = 'Wird erstellt...';
submitSpinner.classList.remove('hidden');
try {
const response = await fetch('/api/jobs', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
showFlashMessage('Druckauftrag erfolgreich erstellt', 'success');
// Redirect to job details or jobs list
setTimeout(() => {
window.location.href = `/job/${result.job_id}`;
}, 1500);
} else {
throw new Error(result.message || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Error creating job:', error);
showFlashMessage('Fehler beim Erstellen des Auftrags: ' + error.message, 'error');
// Reset button state
submitButton.disabled = false;
submitText.textContent = 'Auftrag erstellen';
submitSpinner.classList.add('hidden');
validateForm();
}
}
// Reset form
function resetForm() {
if (confirm('Sind Sie sicher, dass Sie das Formular zurücksetzen möchten?')) {
document.getElementById('jobForm').reset();
clearFile();
validateForm();
}
}
</script>
{% endblock %}

View File

@ -0,0 +1,362 @@
{% extends "base.html" %}
{% block title %}Drucker - 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">Drucker</h1>
<p class="mt-2 text-mercedes-gray">Verwalten Sie Ihre 3D-Drucker</p>
</div>
<div class="flex space-x-4">
<button onclick="refreshPrinters()" 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>
{% if current_user.is_admin %}
<button onclick="showAddPrinterModal()" 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>
Drucker hinzufügen
</button>
{% endif %}
</div>
</div>
</div>
<!-- Printers Grid -->
<div id="printers-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Loading state -->
<div class="col-span-full text-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-mercedes-blue mx-auto"></div>
<p class="mt-4 text-mercedes-gray">Lade Drucker...</p>
</div>
</div>
</div>
<!-- Add Printer Modal -->
{% if current_user.is_admin %}
<div id="addPrinterModal" 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-md">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-mercedes-black">Neuen Drucker hinzufügen</h2>
<button onclick="hideAddPrinterModal()" 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>
<form id="addPrinterForm" onsubmit="handleAddPrinter(event)" class="space-y-4">
<div>
<label for="printer-name" class="block text-sm font-medium text-mercedes-black mb-2">Name</label>
<input type="text" id="printer-name" name="name" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<div>
<label for="printer-model" class="block text-sm font-medium text-mercedes-black mb-2">Modell</label>
<input type="text" id="printer-model" name="model" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<div>
<label for="printer-location" class="block text-sm font-medium text-mercedes-black mb-2">Standort</label>
<input type="text" id="printer-location" name="location" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<div>
<label for="printer-mac" class="block text-sm font-medium text-mercedes-black mb-2">MAC-Adresse</label>
<input type="text" id="printer-mac" name="mac_address" required
placeholder="AA:BB:CC:DD:EE:FF"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<div>
<label for="printer-ip" class="block text-sm font-medium text-mercedes-black mb-2">Plug IP-Adresse</label>
<input type="text" id="printer-ip" name="plug_ip" required
placeholder="192.168.1.100"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<div class="flex space-x-3 pt-4">
<button type="button" onclick="hideAddPrinterModal()"
class="flex-1 bg-mercedes-silver hover:bg-gray-400 text-mercedes-black py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
Abbrechen
</button>
<button type="submit"
class="flex-1 bg-mercedes-green hover:bg-green-700 text-white py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
Hinzufügen
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<!-- Printer Detail Modal -->
<div id="printerDetailModal" 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-lg">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-mercedes-black">Drucker Details</h2>
<button onclick="hidePrinterDetailModal()" 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="printer-detail-content">
<!-- Content will be loaded here -->
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let printers = [];
// Load printers
async function loadPrinters() {
try {
const response = await apiCall('/api/printers');
printers = response.printers || [];
renderPrinters();
} catch (error) {
console.error('Error loading printers:', error);
showFlashMessage('Fehler beim Laden der Drucker', 'error');
}
}
// Render printers grid
function renderPrinters() {
const grid = document.getElementById('printers-grid');
if (printers.length === 0) {
grid.innerHTML = `
<div class="col-span-full text-center py-12">
<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 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
<p class="text-mercedes-gray text-lg">Keine Drucker vorhanden</p>
{% if current_user.is_admin %}
<button onclick="showAddPrinterModal()" class="mt-4 bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
Ersten Drucker hinzufügen
</button>
{% endif %}
</div>
`;
return;
}
grid.innerHTML = printers.map(printer => {
const statusColor = getPrinterStatusColor(printer.status);
const statusText = getPrinterStatusText(printer.status);
return `
<div class="mercedes-card rounded-xl p-6 mercedes-shadow hover:shadow-lg transition-shadow duration-200">
<div class="flex items-start justify-between mb-4">
<div class="flex-1">
<h3 class="text-lg font-bold text-mercedes-black">${printer.name}</h3>
<p class="text-sm text-mercedes-gray">${printer.model}</p>
</div>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}">
${statusText}
</span>
</div>
<div class="space-y-2 mb-4">
<div class="flex items-center text-sm text-mercedes-gray">
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
${printer.location}
</div>
<div class="flex items-center text-sm text-mercedes-gray">
<svg class="h-4 w-4 mr-2" 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>
${printer.mac_address}
</div>
<div class="flex items-center text-sm text-mercedes-gray">
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9" />
</svg>
${printer.plug_ip}
</div>
</div>
<div class="flex space-x-2">
<button onclick="showPrinterDetail(${printer.id})"
class="flex-1 bg-mercedes-blue hover:bg-blue-700 text-white py-2 px-3 rounded-lg text-sm mercedes-button transition-all duration-200">
Details
</button>
{% if current_user.is_admin %}
<button onclick="deletePrinter(${printer.id})"
class="bg-mercedes-red hover:bg-red-700 text-white py-2 px-3 rounded-lg text-sm mercedes-button transition-all duration-200">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
{% endif %}
</div>
</div>
`;
}).join('');
}
// Helper functions
function getPrinterStatusColor(status) {
switch (status) {
case 'available': return 'bg-mercedes-green text-white';
case 'busy': return 'bg-mercedes-yellow text-mercedes-black';
case 'offline': return 'bg-mercedes-red text-white';
case 'maintenance': return 'bg-mercedes-silver text-mercedes-black';
default: return 'bg-mercedes-gray text-white';
}
}
function getPrinterStatusText(status) {
switch (status) {
case 'available': return 'Verfügbar';
case 'busy': return 'Beschäftigt';
case 'offline': return 'Offline';
case 'maintenance': return 'Wartung';
default: return 'Unbekannt';
}
}
// Modal functions
function showAddPrinterModal() {
document.getElementById('addPrinterModal').classList.remove('hidden');
}
function hideAddPrinterModal() {
document.getElementById('addPrinterModal').classList.add('hidden');
document.getElementById('addPrinterForm').reset();
}
function showPrinterDetail(printerId) {
const printer = printers.find(p => p.id === printerId);
if (!printer) return;
const content = document.getElementById('printer-detail-content');
content.innerHTML = `
<div class="space-y-4">
<div>
<h3 class="font-medium text-mercedes-black">${printer.name}</h3>
<p class="text-sm text-mercedes-gray">${printer.model}</p>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="font-medium text-mercedes-gray">Status:</span>
<span class="ml-2 ${getPrinterStatusColor(printer.status)} px-2 py-1 rounded text-xs">
${getPrinterStatusText(printer.status)}
</span>
</div>
<div>
<span class="font-medium text-mercedes-gray">Standort:</span>
<span class="ml-2 text-mercedes-black">${printer.location}</span>
</div>
<div>
<span class="font-medium text-mercedes-gray">MAC:</span>
<span class="ml-2 text-mercedes-black font-mono text-xs">${printer.mac_address}</span>
</div>
<div>
<span class="font-medium text-mercedes-gray">Plug IP:</span>
<span class="ml-2 text-mercedes-black font-mono text-xs">${printer.plug_ip}</span>
</div>
</div>
<div class="pt-4 border-t border-mercedes-silver">
<p class="text-xs text-mercedes-gray">
Erstellt: ${formatDate(printer.created_at)}
</p>
</div>
</div>
`;
document.getElementById('printerDetailModal').classList.remove('hidden');
}
function hidePrinterDetailModal() {
document.getElementById('printerDetailModal').classList.add('hidden');
}
// Add printer
async function handleAddPrinter(event) {
event.preventDefault();
const formData = new FormData(event.target);
const printerData = {
name: formData.get('name'),
model: formData.get('model'),
location: formData.get('location'),
mac_address: formData.get('mac_address'),
plug_ip: formData.get('plug_ip')
};
try {
await apiCall('/api/printers', {
method: 'POST',
body: JSON.stringify(printerData)
});
showFlashMessage('Drucker erfolgreich hinzugefügt', 'success');
hideAddPrinterModal();
loadPrinters();
} catch (error) {
showFlashMessage('Fehler beim Hinzufügen des Druckers', 'error');
}
}
// Delete printer
async function deletePrinter(printerId) {
if (!confirm('Sind Sie sicher, dass Sie diesen Drucker löschen möchten?')) {
return;
}
try {
await apiCall(`/api/printers/${printerId}`, {
method: 'DELETE'
});
showFlashMessage('Drucker erfolgreich gelöscht', 'success');
loadPrinters();
} catch (error) {
showFlashMessage('Fehler beim Löschen des Druckers', 'error');
}
}
// Refresh printers
function refreshPrinters() {
showFlashMessage('Drucker werden aktualisiert...', 'info');
loadPrinters();
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadPrinters();
});
</script>
{% endblock %}

View File

@ -0,0 +1,410 @@
{% extends "base.html" %}
{% block title %}Profil - MYP Platform{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-mercedes-black">Mein Profil</h1>
<p class="mt-2 text-mercedes-gray">Verwalten Sie Ihre Kontoinformationen und Einstellungen</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Profile Information -->
<div class="lg:col-span-2">
<div class="mercedes-card rounded-xl p-6 mb-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-mercedes-black">Persönliche Informationen</h2>
<button onclick="toggleEditMode()" id="edit-button"
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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Bearbeiten
</button>
</div>
<form id="profile-form" onsubmit="handleProfileUpdate(event)">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="first-name" class="block text-sm font-medium text-mercedes-black mb-2">
Vorname
</label>
<input type="text" id="first-name" name="first_name" value="{{ current_user.first_name or '' }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
</div>
<div>
<label for="last-name" class="block text-sm font-medium text-mercedes-black mb-2">
Nachname
</label>
<input type="text" id="last-name" name="last_name" value="{{ current_user.last_name or '' }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
</div>
<div>
<label for="email" class="block text-sm font-medium text-mercedes-black mb-2">
E-Mail-Adresse
</label>
<input type="email" id="email" name="email" value="{{ current_user.email }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
</div>
<div>
<label for="phone" class="block text-sm font-medium text-mercedes-black mb-2">
Telefonnummer
</label>
<input type="tel" id="phone" name="phone" value="{{ current_user.phone or '' }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
</div>
<div>
<label for="department" class="block text-sm font-medium text-mercedes-black mb-2">
Abteilung
</label>
<input type="text" id="department" name="department" value="{{ current_user.department or '' }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
</div>
<div>
<label for="role" class="block text-sm font-medium text-mercedes-black mb-2">
Rolle
</label>
<input type="text" id="role" value="{{ 'Administrator' if current_user.is_admin else 'Benutzer' }}" disabled
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg bg-mercedes-light cursor-not-allowed">
</div>
</div>
<div id="form-actions" class="hidden mt-6 flex space-x-4">
<button type="submit"
class="bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
Änderungen speichern
</button>
<button type="button" onclick="cancelEdit()"
class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
Abbrechen
</button>
</div>
</form>
</div>
<!-- Password Change -->
<div class="mercedes-card rounded-xl p-6">
<h2 class="text-xl font-bold text-mercedes-black mb-6">Passwort ändern</h2>
<form id="password-form" onsubmit="handlePasswordChange(event)" class="space-y-4">
<div>
<label for="current-password" class="block text-sm font-medium text-mercedes-black mb-2">
Aktuelles Passwort
</label>
<input type="password" id="current-password" name="current_password" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<div>
<label for="new-password" class="block text-sm font-medium text-mercedes-black mb-2">
Neues Passwort
</label>
<input type="password" id="new-password" name="new_password" required minlength="8"
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
<p class="mt-1 text-xs text-mercedes-gray">Mindestens 8 Zeichen</p>
</div>
<div>
<label for="confirm-password" class="block text-sm font-medium text-mercedes-black mb-2">
Passwort bestätigen
</label>
<input type="password" id="confirm-password" name="confirm_password" required
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
</div>
<button type="submit"
class="bg-mercedes-blue hover:bg-blue-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
Passwort ändern
</button>
</form>
</div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Profile Stats -->
<div class="mercedes-card rounded-xl p-6">
<h3 class="text-lg font-bold text-mercedes-black mb-4">Statistiken</h3>
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-sm text-mercedes-gray">Gesamte Aufträge</span>
<span id="total-jobs" class="text-lg font-bold text-mercedes-black">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-mercedes-gray">Abgeschlossene Aufträge</span>
<span id="completed-jobs" class="text-lg font-bold text-mercedes-green">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-mercedes-gray">Aktive Aufträge</span>
<span id="active-jobs" class="text-lg font-bold text-mercedes-blue">-</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-mercedes-gray">Fehlgeschlagene Aufträge</span>
<span id="failed-jobs" class="text-lg font-bold text-mercedes-red">-</span>
</div>
</div>
</div>
<!-- Account Info -->
<div class="mercedes-card rounded-xl p-6">
<h3 class="text-lg font-bold text-mercedes-black mb-4">Kontoinformationen</h3>
<div class="space-y-3 text-sm">
<div>
<span class="text-mercedes-gray">Mitglied seit:</span>
<div class="font-medium text-mercedes-black">{{ current_user.created_at.strftime('%d.%m.%Y') if current_user.created_at else 'Unbekannt' }}</div>
</div>
<div>
<span class="text-mercedes-gray">Letzte Anmeldung:</span>
<div class="font-medium text-mercedes-black">{{ current_user.last_login.strftime('%d.%m.%Y %H:%M') if current_user.last_login else 'Nie' }}</div>
</div>
<div>
<span class="text-mercedes-gray">Konto-Status:</span>
<div class="font-medium">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-mercedes-green text-white">
Aktiv
</span>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="mercedes-card rounded-xl p-6">
<h3 class="text-lg font-bold text-mercedes-black mb-4">Schnellaktionen</h3>
<div class="space-y-3">
<a href="/new-job"
class="block w-full bg-mercedes-green hover:bg-green-700 text-white text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
Neuer Auftrag
</a>
<a href="/my/jobs"
class="block w-full bg-mercedes-blue hover:bg-blue-700 text-white text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
Meine Aufträge
</a>
<button onclick="downloadUserData()"
class="block w-full bg-mercedes-silver hover:bg-gray-400 text-mercedes-black text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
Daten exportieren
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let isEditMode = false;
let originalFormData = {};
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadUserStats();
storeOriginalFormData();
});
// Store original form data
function storeOriginalFormData() {
const form = document.getElementById('profile-form');
const formData = new FormData(form);
originalFormData = {};
for (let [key, value] of formData.entries()) {
originalFormData[key] = value;
}
}
// Toggle edit mode
function toggleEditMode() {
isEditMode = !isEditMode;
const inputs = document.querySelectorAll('#profile-form input:not(#role)');
const editButton = document.getElementById('edit-button');
const formActions = document.getElementById('form-actions');
inputs.forEach(input => {
input.disabled = !isEditMode;
});
if (isEditMode) {
editButton.innerHTML = `
<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="M6 18L18 6M6 6l12 12" />
</svg>
Abbrechen
`;
editButton.onclick = cancelEdit;
formActions.classList.remove('hidden');
} else {
editButton.innerHTML = `
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Bearbeiten
`;
editButton.onclick = toggleEditMode;
formActions.classList.add('hidden');
}
}
// Cancel edit
function cancelEdit() {
// Restore original values
Object.keys(originalFormData).forEach(key => {
const input = document.querySelector(`[name="${key}"]`);
if (input) {
input.value = originalFormData[key];
}
});
toggleEditMode();
}
// Handle profile update
async function handleProfileUpdate(event) {
event.preventDefault();
const formData = new FormData(event.target);
const profileData = {};
for (let [key, value] of formData.entries()) {
profileData[key] = value;
}
try {
const response = await apiCall('/api/user/profile', {
method: 'PUT',
body: JSON.stringify(profileData)
});
if (response.success) {
showFlashMessage('Profil erfolgreich aktualisiert', 'success');
storeOriginalFormData();
toggleEditMode();
} else {
throw new Error(response.message || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Error updating profile:', error);
showFlashMessage('Fehler beim Aktualisieren des Profils: ' + error.message, 'error');
}
}
// Handle password change
async function handlePasswordChange(event) {
event.preventDefault();
const formData = new FormData(event.target);
const newPassword = formData.get('new_password');
const confirmPassword = formData.get('confirm_password');
if (newPassword !== confirmPassword) {
showFlashMessage('Die Passwörter stimmen nicht überein', 'error');
return;
}
const passwordData = {
current_password: formData.get('current_password'),
new_password: newPassword
};
try {
const response = await apiCall('/api/user/password', {
method: 'PUT',
body: JSON.stringify(passwordData)
});
if (response.success) {
showFlashMessage('Passwort erfolgreich geändert', 'success');
document.getElementById('password-form').reset();
} else {
throw new Error(response.message || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Error changing password:', error);
showFlashMessage('Fehler beim Ändern des Passworts: ' + error.message, 'error');
}
}
// Load user statistics
async function loadUserStats() {
try {
const response = await apiCall('/api/user/stats');
if (response.success) {
const stats = response.stats;
document.getElementById('total-jobs').textContent = stats.total_jobs || 0;
document.getElementById('completed-jobs').textContent = stats.completed_jobs || 0;
document.getElementById('active-jobs').textContent = stats.active_jobs || 0;
document.getElementById('failed-jobs').textContent = stats.failed_jobs || 0;
}
} catch (error) {
console.error('Error loading user stats:', error);
// Don't show error message for stats, just keep the dashes
}
}
// Download user data
async function downloadUserData() {
try {
const response = await fetch('/api/user/export', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'meine_daten.json';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showFlashMessage('Daten erfolgreich exportiert', 'success');
} catch (error) {
console.error('Error downloading user data:', error);
showFlashMessage('Fehler beim Exportieren der Daten', 'error');
}
}
// Password confirmation validation
document.getElementById('confirm-password').addEventListener('input', function() {
const newPassword = document.getElementById('new-password').value;
const confirmPassword = this.value;
if (confirmPassword && newPassword !== confirmPassword) {
this.setCustomValidity('Die Passwörter stimmen nicht überein');
} else {
this.setCustomValidity('');
}
});
</script>
{% endblock %}

View File

@ -9,7 +9,7 @@ Flask-Login==0.6.3
SQLAlchemy==2.0.23 SQLAlchemy==2.0.23
# Smart Plug Steuerung # Smart Plug Steuerung
PyP100==0.1.4 PyP100==0.1.2
# Passwort-Hashing (bereits in Flask enthalten, aber explizit für Klarheit) # Passwort-Hashing (bereits in Flask enthalten, aber explizit für Klarheit)
Werkzeug==3.0.1 Werkzeug==3.0.1

View File

@ -0,0 +1,157 @@
#!/bin/bash
# 🔍 MYP Frontend - Backend-Verbindung prüfen
# Entwicklungsumgebung - Überprüft Verbindung zum Raspberry Pi Backend auf 192.168.0.105:5000
set -e
# Farben für Terminal-Ausgabe
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Funktionen
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Arbeitsverzeichnis setzen
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "🔍 MYP Frontend - Backend-Verbindung wird überprüft..."
echo "🍓 Entwicklungsumgebung - Raspberry Pi Backend"
echo "=================================================="
# Backend-URL definieren (Hardcoded für Raspberry Pi)
BACKEND_URL="http://192.168.0.105:5000"
BACKEND_API_URL="$BACKEND_URL/api"
RASPBERRY_PI_HOST="192.168.0.105"
log_info "Ziel-Backend: $BACKEND_URL (Raspberry Pi)"
echo ""
# 1. Netzwerk-Konnektivität zum Raspberry Pi prüfen
log_info "1. Prüfe Netzwerk-Konnektivität zum Raspberry Pi ($RASPBERRY_PI_HOST)..."
if ping -c 1 -W 3 $RASPBERRY_PI_HOST >/dev/null 2>&1; then
log_success "✓ Ping zum Raspberry Pi erfolgreich"
else
log_error "✗ Ping zum Raspberry Pi fehlgeschlagen"
log_error " Stellen Sie sicher, dass der Raspberry Pi erreichbar ist"
log_error " Prüfen Sie die Netzwerkverbindung und IP-Adresse"
fi
# 2. Backend-Service auf Raspberry Pi prüfen
log_info "2. Prüfe Backend-Service auf Raspberry Pi Port 5000..."
if curl -f --connect-timeout 5 "$BACKEND_URL/health" >/dev/null 2>&1; then
log_success "✓ Raspberry Pi Backend-Health-Check erfolgreich"
elif curl -f --connect-timeout 5 "$BACKEND_URL" >/dev/null 2>&1; then
log_warning "⚠ Raspberry Pi Backend erreichbar, aber kein Health-Endpoint"
else
log_error "✗ Raspberry Pi Backend-Service nicht erreichbar"
log_error " Stellen Sie sicher, dass das Backend auf dem Raspberry Pi läuft"
log_error " Prüfen Sie: ssh pi@$RASPBERRY_PI_HOST 'sudo systemctl status myp-backend'"
fi
# 3. API-Endpunkte auf Raspberry Pi prüfen
log_info "3. Prüfe Raspberry Pi Backend-API-Endpunkte..."
for endpoint in "printers" "jobs" "users"; do
if curl -f --connect-timeout 5 "$BACKEND_API_URL/$endpoint" >/dev/null 2>&1; then
log_success "✓ API-Endpunkt /$endpoint auf Raspberry Pi erreichbar"
else
log_warning "⚠ API-Endpunkt /$endpoint auf Raspberry Pi nicht erreichbar"
fi
done
echo ""
# 4. Frontend-Konfigurationsdateien für Entwicklung prüfen
log_info "4. Prüfe Frontend-Konfigurationsdateien (Entwicklungsumgebung)..."
# .env.local prüfen
if [ -f ".env.local" ]; then
if grep -q "NEXT_PUBLIC_API_URL=http://192.168.0.105:5000" .env.local; then
log_success "✓ .env.local korrekt für Raspberry Pi konfiguriert"
else
log_warning "⚠ .env.local existiert, aber Raspberry Pi Backend-URL ist falsch"
log_info " Führen Sie './setup-backend-url.sh' aus"
fi
else
log_warning "⚠ .env.local nicht gefunden"
log_info " Führen Sie './setup-backend-url.sh' aus"
fi
# env.frontend prüfen
if grep -q "NODE_ENV=development" env.frontend && grep -q "NEXT_PUBLIC_API_URL=http://192.168.0.105:5000" env.frontend; then
log_success "✓ env.frontend korrekt für Entwicklungsumgebung konfiguriert"
else
log_error "✗ env.frontend nicht für Entwicklungsumgebung konfiguriert"
fi
# api-config.ts prüfen
if grep -q 'API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://192.168.0.105:5000"' src/utils/api-config.ts; then
log_success "✓ api-config.ts korrekt für Raspberry Pi konfiguriert"
else
log_error "✗ api-config.ts hat falsche Standard-URL"
fi
# Docker-Compose-Dateien für Entwicklung prüfen
if grep -q "NODE_ENV=development" docker-compose.yml && grep -q "NEXT_PUBLIC_API_URL=http://192.168.0.105:5000" docker-compose.yml; then
log_success "✓ docker-compose.yml korrekt für Entwicklungsumgebung konfiguriert"
else
log_warning "⚠ docker-compose.yml nicht für Entwicklungsumgebung konfiguriert"
fi
# Caddy-Konfiguration für Entwicklung prüfen
if grep -q "reverse_proxy 192.168.0.105:5000" docker/caddy/Caddyfile && grep -q "localhost" docker/caddy/Caddyfile; then
log_success "✓ Caddy-Konfiguration korrekt für Entwicklungsumgebung"
else
log_error "✗ Caddy-Konfiguration nicht für Entwicklungsumgebung konfiguriert"
fi
echo ""
# 5. Zusammenfassung und Empfehlungen für Entwicklungsumgebung
log_info "5. Zusammenfassung und Empfehlungen (Entwicklungsumgebung):"
echo ""
if ping -c 1 -W 3 $RASPBERRY_PI_HOST >/dev/null 2>&1 && curl -f --connect-timeout 5 "$BACKEND_URL" >/dev/null 2>&1; then
log_success "🎉 Raspberry Pi Backend ist erreichbar und läuft!"
echo ""
log_info "Nächste Schritte (Entwicklung):"
echo " 1. Frontend starten: ./start-frontend-server.sh"
echo " 2. Frontend testen: http://localhost:3000"
echo " 3. Health-Check: http://localhost:3000/health"
echo " 4. Backend-Health: http://localhost:3000/backend-health"
else
log_error "❌ Raspberry Pi Backend ist nicht erreichbar!"
echo ""
log_info "Fehlerbehebung (Entwicklungsumgebung):"
echo " 1. Prüfen Sie, ob der Raspberry Pi ($RASPBERRY_PI_HOST) läuft"
echo " 2. SSH zum Raspberry Pi: ssh pi@$RASPBERRY_PI_HOST"
echo " 3. Backend-Status prüfen: sudo systemctl status myp-backend"
echo " 4. Backend-Logs prüfen: sudo journalctl -u myp-backend -f"
echo " 5. Netzwerk-Konnektivität prüfen"
echo " 6. Firewall-Einstellungen auf Raspberry Pi prüfen"
fi
echo ""
log_info "Debug-Tools:"
echo " - Debug-Server: ./debug-server/start-debug-server.sh"
echo " - Backend direkt: curl http://192.168.0.105:5000/health"
echo " - SSH zum Raspberry Pi: ssh pi@192.168.0.105"
echo "=================================================="

View File

@ -1,5 +1,5 @@
# 🎨 MYP Frontend - Standalone Server Konfiguration # 🎨 MYP Frontend - Entwicklungsumgebung Konfiguration
# Frontend-Service als vollständig unabhängiger Server # Frontend-Service für die Entwicklung mit Raspberry Pi Backend
version: '3.8' version: '3.8'
@ -8,33 +8,38 @@ services:
frontend: frontend:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile.dev
args: args:
- BUILDKIT_INLINE_CACHE=1 - BUILDKIT_INLINE_CACHE=1
- NODE_ENV=${NODE_ENV:-production} - NODE_ENV=development
image: myp/frontend:latest image: myp/frontend:dev
container_name: myp-frontend-standalone container_name: myp-frontend-dev
restart: unless-stopped restart: unless-stopped
environment: environment:
- NODE_ENV=${NODE_ENV:-production} - NODE_ENV=development
- NEXT_TELEMETRY_DISABLED=1 - NEXT_TELEMETRY_DISABLED=1
# Backend API Konfiguration # Backend API Konfiguration (Raspberry Pi)
- NEXT_PUBLIC_API_URL=${BACKEND_API_URL:-http://localhost:5000/api} - NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
- NEXT_PUBLIC_BACKEND_HOST=${BACKEND_HOST:-localhost:5000} - NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000
# Frontend Server # Frontend Server
- PORT=3000 - PORT=3000
- HOSTNAME=0.0.0.0 - HOSTNAME=0.0.0.0
# Auth Konfiguration # Auth Konfiguration (Entwicklung)
- NEXTAUTH_URL=${FRONTEND_URL:-http://localhost:3000} - NEXTAUTH_URL=http://localhost:3000
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-frontend-auth-secret} - NEXTAUTH_SECRET=dev-frontend-auth-secret
# Debug-Einstellungen
- DEBUG=true
- NEXT_DEBUG=true
volumes: volumes:
- frontend_data:/app/.next - .:/app
- frontend_cache:/app/.next/cache - /app/node_modules
- /app/.next
- ./public:/app/public:ro - ./public:/app/public:ro
ports: ports:
@ -43,6 +48,9 @@ services:
networks: networks:
- frontend-network - frontend-network
extra_hosts:
- "raspberrypi:192.168.0.105"
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"] test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s interval: 30s
@ -52,8 +60,8 @@ services:
labels: labels:
- "service.type=frontend" - "service.type=frontend"
- "service.name=myp-frontend" - "service.name=myp-frontend-dev"
- "service.environment=${NODE_ENV:-production}" - "service.environment=development"
# === FRONTEND CACHE (Optional: Redis für Session Management) === # === FRONTEND CACHE (Optional: Redis für Session Management) ===
frontend-cache: frontend-cache:

View File

@ -5,25 +5,34 @@ services:
frontend: frontend:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile.dev
container_name: myp-rp container_name: myp-rp-dev
restart: unless-stopped restart: unless-stopped
environment: environment:
- NODE_ENV=production - NODE_ENV=development
- NEXT_PUBLIC_API_URL=https://m040tbaraspi001.de040.corpintra.net/api - NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
- NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000
- DEBUG=true
- NEXT_DEBUG=true
volumes:
- .:/app
- /app/node_modules
- /app/.next
ports:
- "3000:3000"
networks: networks:
- myp-network - myp-network
healthcheck: healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"] test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
# Caddy Proxy # Caddy Proxy (Entwicklung)
caddy: caddy:
image: caddy:2.7-alpine image: caddy:2.7-alpine
container_name: myp-caddy container_name: myp-caddy-dev
restart: unless-stopped restart: unless-stopped
ports: ports:
- "80:80" - "80:80"
@ -36,9 +45,10 @@ services:
- myp-network - myp-network
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
- "raspberrypi:192.168.0.105"
environment: environment:
- CADDY_HOST=53.37.211.254 - CADDY_HOST=localhost
- CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net - CADDY_DOMAIN=localhost
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN

View File

@ -2,51 +2,54 @@
debug debug
} }
# Hauptdomain und IP-Adresse für die Anwendung # Entwicklungsumgebung - Localhost und Raspberry Pi Backend
53.37.211.254, m040tbaraspi001.de040.corpintra.net, m040tbaraspi001, de040.corpintra.net, localhost { localhost, 127.0.0.1 {
# API Anfragen zum Backend weiterleiten # API Anfragen zum Raspberry Pi Backend weiterleiten
@api { @api {
path /api/* /health path /api/* /health
} }
handle @api { handle @api {
uri strip_prefix /api uri strip_prefix /api
reverse_proxy 192.168.0.5:5000 reverse_proxy 192.168.0.105:5000 {
header_up Host {upstream_hostport}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
} }
# Alle anderen Anfragen zum Frontend weiterleiten # Alle anderen Anfragen zum Frontend weiterleiten
handle { handle {
reverse_proxy myp-rp:3000 reverse_proxy myp-rp-dev:3000 {
header_up Host {upstream_hostport}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
} }
tls internal { # TLS für Entwicklung deaktiviert
on_demand tls off
}
# Erlaube HTTP -> HTTPS Redirects für OAuth # OAuth Callbacks für Entwicklung
@oauth path /auth/login/callback* @oauth path /auth/login/callback*
handle @oauth { handle @oauth {
header Cache-Control "no-cache" header Cache-Control "no-cache"
reverse_proxy myp-rp:3000 reverse_proxy myp-rp-dev:3000
} }
# Allgemeine Header für Sicherheit und Caching # Entwicklungsfreundliche Header
header { header {
# Sicherheitsheader # Weniger restriktive Sicherheitsheader für Entwicklung
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff" X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN" X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
# Cache-Control für statische Assets # Keine Caches für Entwicklung
@static { Cache-Control "no-store, no-cache, must-revalidate"
path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
}
header @static Cache-Control "public, max-age=86400"
# Keine Caches für dynamische Inhalte # CORS für Entwicklung
@dynamic { Access-Control-Allow-Origin "*"
not path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2 Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
} Access-Control-Allow-Headers "Content-Type, Authorization"
header @dynamic Cache-Control "no-store, no-cache, must-revalidate"
} }
} }

View File

@ -1,8 +1,8 @@
# 🎨 MYP Frontend - Standalone Server Konfiguration # 🎨 MYP Frontend - Entwicklungsumgebung Konfiguration
# Umgebungsvariablen ausschließlich für den Frontend-Server # Umgebungsvariablen für die Verbindung zum Raspberry Pi Backend
# === NODE.JS KONFIGURATION === # === NODE.JS KONFIGURATION ===
NODE_ENV=production NODE_ENV=development
NEXT_TELEMETRY_DISABLED=1 NEXT_TELEMETRY_DISABLED=1
# === FRONTEND SERVER === # === FRONTEND SERVER ===
@ -11,18 +11,18 @@ HOSTNAME=0.0.0.0
FRONTEND_URL=http://localhost:3000 FRONTEND_URL=http://localhost:3000
# === BACKEND API KONFIGURATION === # === BACKEND API KONFIGURATION ===
# Backend-Server Verbindung (HTTP) # Backend-Server Verbindung (Raspberry Pi)
BACKEND_API_URL=http://localhost:5000/api BACKEND_API_URL=http://192.168.0.105:5000/api
BACKEND_HOST=localhost:5000 BACKEND_HOST=192.168.0.105:5000
NEXT_PUBLIC_API_URL=http://localhost:5000/api NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
NEXT_PUBLIC_BACKEND_HOST=localhost:5000 NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000
# === AUTHENTIFIZIERUNG === # === AUTHENTIFIZIERUNG ===
NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=frontend-auth-secret-2024 NEXTAUTH_SECRET=dev-frontend-auth-secret-2024
JWT_SECRET=frontend-jwt-secret-2024 JWT_SECRET=dev-frontend-jwt-secret-2024
# OAuth Provider (falls verwendet) # OAuth Provider (Entwicklung)
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_SECRET=
MICROSOFT_CLIENT_ID= MICROSOFT_CLIENT_ID=
@ -35,7 +35,7 @@ FRONTEND_DB_PATH=db/frontend.db
# === CACHE KONFIGURATION === # === CACHE KONFIGURATION ===
# Frontend-spezifischer Redis Cache (separater Port!) # Frontend-spezifischer Redis Cache (separater Port!)
FRONTEND_REDIS_PASSWORD=frontend_cache_password FRONTEND_REDIS_PASSWORD=dev_frontend_cache_password
FRONTEND_REDIS_HOST=localhost FRONTEND_REDIS_HOST=localhost
FRONTEND_REDIS_PORT=6380 FRONTEND_REDIS_PORT=6380
FRONTEND_REDIS_DB=1 FRONTEND_REDIS_DB=1
@ -45,19 +45,19 @@ CDN_URL=http://localhost:8080
ASSETS_URL=http://localhost:8080/static ASSETS_URL=http://localhost:8080/static
# === SICHERHEIT === # === SICHERHEIT ===
# CSP (Content Security Policy) # CSP (Content Security Policy) - Entwicklung
CSP_SCRIPT_SRC="'self' 'unsafe-inline' 'unsafe-eval'" CSP_SCRIPT_SRC="'self' 'unsafe-inline' 'unsafe-eval'"
CSP_STYLE_SRC="'self' 'unsafe-inline'" CSP_STYLE_SRC="'self' 'unsafe-inline'"
CSP_IMG_SRC="'self' data: https:" CSP_IMG_SRC="'self' data: https:"
CSP_CONNECT_SRC="'self' ws: wss: http://localhost:5000" CSP_CONNECT_SRC="'self' ws: wss: http://192.168.0.105:5000 http://localhost:5000"
# === MONITORING === # === MONITORING ===
ANALYTICS_ENABLED=true ANALYTICS_ENABLED=false
ERROR_REPORTING_ENABLED=true ERROR_REPORTING_ENABLED=true
# === ENTWICKLUNG === # === ENTWICKLUNG ===
DEBUG=false DEBUG=true
NEXT_DEBUG=false NEXT_DEBUG=true
# === BUILD KONFIGURATION === # === BUILD KONFIGURATION ===
ANALYZE=false ANALYZE=false

View File

@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
// Entwicklungsumgebung - weniger restriktive CORS
async headers() { async headers() {
return [ return [
{ {
@ -7,7 +8,7 @@ const nextConfig = {
headers: [ headers: [
{ {
key: "Access-Control-Allow-Origin", key: "Access-Control-Allow-Origin",
value: "m040tbaraspi001.de040.corpintra.net", value: "*",
}, },
{ {
key: "Access-Control-Allow-Methods", key: "Access-Control-Allow-Methods",
@ -21,6 +22,30 @@ const nextConfig = {
}, },
]; ];
}, },
// Rewrites für Backend-API-Aufrufe zum Raspberry Pi
async rewrites() {
return [
{
source: '/api/backend/:path*',
destination: 'http://192.168.0.105:5000/api/:path*',
},
// Direkter Proxy für Health-Checks
{
source: '/backend-health',
destination: 'http://192.168.0.105:5000/health',
},
];
},
// Entwicklungseinstellungen
experimental: {
serverComponentsExternalPackages: [],
},
// Logging für Entwicklung
logging: {
fetches: {
fullUrl: true,
},
},
}; };
export default nextConfig; export default nextConfig;

66
frontend/setup-backend-url.sh Normal file → Executable file
View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Skript zum Setzen der Backend-URL in der Frontend-Konfiguration # Skript zum Setzen der Backend-URL in der Frontend-Konfiguration
# Verwendet für die Verbindung zum Backend-Server unter 192.168.0.105:5000 # Entwicklungsumgebung - Raspberry Pi Backend auf 192.168.0.105:5000
# Farbcodes für Ausgabe # Farbcodes für Ausgabe
RED='\033[0;31m' RED='\033[0;31m'
@ -22,59 +22,57 @@ error_log() {
# Definiere Variablen # Definiere Variablen
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ENV_FILE="$SCRIPT_DIR/.env.local" ENV_FILE="$SCRIPT_DIR/.env.local"
DEFAULT_BACKEND_URL="http://192.168.0.105:5000" # Hardcoded für Entwicklungsumgebung - Raspberry Pi Backend
BACKEND_URL="http://192.168.0.105:5000"
# Falls übergebene Parameter vorhanden sind, Backend-URL anpassen log "Entwicklungsumgebung - Raspberry Pi Backend: ${BACKEND_URL}"
if [ -n "$1" ]; then
BACKEND_URL="$1"
log "Verwende übergebene Backend-URL: ${BACKEND_URL}"
else
BACKEND_URL="$DEFAULT_BACKEND_URL"
log "Verwende Standard-Backend-URL: ${BACKEND_URL}"
fi
# Bestimme den Hostnamen für OAuth # Hostname für OAuth (Entwicklung)
HOSTNAME=$(hostname) FRONTEND_HOSTNAME="localhost"
if [[ "$HOSTNAME" == *"m040tbaraspi001"* ]] || [[ "$HOSTNAME" == *"corpintra"* ]]; then OAUTH_URL="http://localhost:3000/auth/login/callback"
FRONTEND_HOSTNAME="m040tbaraspi001.de040.corpintra.net" log "Frontend-Hostname (Entwicklung): $FRONTEND_HOSTNAME"
OAUTH_URL="http://m040tbaraspi001.de040.corpintra.net/auth/login/callback"
log "Erkannt: Unternehmens-Hostname: $FRONTEND_HOSTNAME"
else
FRONTEND_HOSTNAME="$HOSTNAME"
OAUTH_URL="http://$HOSTNAME:3000/auth/login/callback"
log "Lokaler Hostname: $FRONTEND_HOSTNAME"
fi
# Erstelle .env.local Datei mit Backend-URL # Erstelle .env.local Datei mit Backend-URL
log "${YELLOW}Erstelle .env.local Datei...${NC}" log "${YELLOW}Erstelle .env.local Datei für Entwicklungsumgebung...${NC}"
cat > "$ENV_FILE" << EOL cat > "$ENV_FILE" << EOL
# Backend API Konfiguration # Backend API Konfiguration - Raspberry Pi (Entwicklung)
NEXT_PUBLIC_API_URL=${BACKEND_URL} NEXT_PUBLIC_API_URL=${BACKEND_URL}
# Frontend-URL für OAuth Callback # Frontend-URL für OAuth Callback (Entwicklung)
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME} NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}:3000
# Explizite OAuth Callback URL für GitHub # Explizite OAuth Callback URL für GitHub (Entwicklung)
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL} NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}
# OAuth Konfiguration (falls nötig) # Entwicklungsumgebung
OAUTH_CLIENT_ID=client_id NODE_ENV=development
OAUTH_CLIENT_SECRET=client_secret DEBUG=true
NEXT_DEBUG=true
# OAuth Konfiguration (Entwicklung)
OAUTH_CLIENT_ID=dev_client_id
OAUTH_CLIENT_SECRET=dev_client_secret
# Raspberry Pi Backend Host
NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000
EOL EOL
# Überprüfe, ob die Datei erstellt wurde # Überprüfe, ob die Datei erstellt wurde
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
log "${GREEN}Erfolgreich .env.local Datei mit Backend-URL erstellt: ${BACKEND_URL}${NC}" log "${GREEN}Erfolgreich .env.local Datei für Entwicklungsumgebung erstellt: ${BACKEND_URL}${NC}"
else else
error_log "Konnte .env.local Datei nicht erstellen." error_log "Konnte .env.local Datei nicht erstellen."
exit 1 exit 1
fi fi
# Hinweis für Docker-Installation # Hinweis für Entwicklungsumgebung
log "${YELLOW}WICHTIG: Wenn Sie Docker verwenden, stellen Sie sicher, dass Sie die Umgebungsvariable setzen:${NC}" log "${YELLOW}ENTWICKLUNGSUMGEBUNG KONFIGURIERT:${NC}"
log "NEXT_PUBLIC_API_URL=${BACKEND_URL}" log "Backend (Raspberry Pi): ${BACKEND_URL}"
log "Frontend: http://localhost:3000"
log "OAuth Callback: ${OAUTH_URL}"
log "" log ""
log "${GREEN}Backend-URL wurde erfolgreich konfiguriert. Nach einem Neustart der Anwendung sollte die Verbindung hergestellt werden.${NC}" log "${GREEN}Backend-URL wurde erfolgreich für die Entwicklungsumgebung konfiguriert.${NC}"
log "${GREEN}Das Frontend verbindet sich jetzt mit dem Raspberry Pi Backend.${NC}"
# Berechtigungen setzen # Berechtigungen setzen
chmod 600 "$ENV_FILE" chmod 600 "$ENV_FILE"

View File

@ -7,7 +7,7 @@ import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
// Prüfe Backend-Verbindung // Prüfe Backend-Verbindung
const backendUrl = process.env.BACKEND_API_URL || 'http://localhost:5000'; const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://192.168.0.105:5000';
let backendStatus = 'unknown'; let backendStatus = 'unknown';
try { try {