"""
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:
parent
ead75ae451
commit
62e131c02f
@ -7,7 +7,7 @@ from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple, Any, Union
|
||||
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
|
||||
import sqlalchemy.exc
|
||||
from PyP100 import PyP110
|
||||
@ -972,6 +972,81 @@ def stop_scheduler_api():
|
||||
"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
|
||||
def start_scheduler():
|
||||
"""Initialisiert und startet den Scheduler mit den erforderlichen Tasks."""
|
||||
|
65
backend/app/static/css/tailwind.min.css
vendored
Normal file
65
backend/app/static/css/tailwind.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
13
backend/app/static/mercedes.svg
Normal file
13
backend/app/static/mercedes.svg
Normal 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 |
504
backend/app/templates/base.html
Normal file
504
backend/app/templates/base.html
Normal 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>
|
461
backend/app/templates/dashboard.html
Normal file
461
backend/app/templates/dashboard.html
Normal 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 %}
|
469
backend/app/templates/jobs.html
Normal file
469
backend/app/templates/jobs.html
Normal file
@ -0,0 +1,469 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Druckaufträge - MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-mercedes-black">Druckaufträge</h1>
|
||||
<p class="mt-2 text-mercedes-gray">Verwalten Sie Ihre 3D-Druckaufträge</p>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<button onclick="refreshJobs()" class="bg-mercedes-blue hover:bg-blue-700 text-white px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Aktualisieren
|
||||
</button>
|
||||
<a href="/new-job" class="bg-mercedes-green hover:bg-green-700 text-white px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Neuer Auftrag
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="mb-6 mercedes-card rounded-xl p-4">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<label for="status-filter" class="text-sm font-medium text-mercedes-black">Status:</label>
|
||||
<select id="status-filter" onchange="filterJobs()" class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
<option value="">Alle</option>
|
||||
<option value="pending">Wartend</option>
|
||||
<option value="printing">Druckt</option>
|
||||
<option value="completed">Abgeschlossen</option>
|
||||
<option value="failed">Fehlgeschlagen</option>
|
||||
<option value="cancelled">Abgebrochen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<label for="printer-filter" class="text-sm font-medium text-mercedes-black">Drucker:</label>
|
||||
<select id="printer-filter" onchange="filterJobs()" class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
<option value="">Alle Drucker</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<label for="search-input" class="text-sm font-medium text-mercedes-black">Suche:</label>
|
||||
<input type="text" id="search-input" placeholder="Dateiname..." onkeyup="filterJobs()"
|
||||
class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
</div>
|
||||
|
||||
<button onclick="clearFilters()" class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-3 py-1 rounded-lg text-sm mercedes-button transition-all duration-200">
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jobs Table -->
|
||||
<div class="mercedes-card rounded-xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-mercedes-silver">
|
||||
<thead class="bg-mercedes-light">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Datei
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Drucker
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Fortschritt
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Erstellt
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-mercedes-gray uppercase tracking-wider">
|
||||
Aktionen
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jobs-table-body" class="bg-white divide-y divide-mercedes-silver">
|
||||
<!-- Loading state -->
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-mercedes-blue mx-auto"></div>
|
||||
<p class="mt-4 text-mercedes-gray">Lade Aufträge...</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div id="pagination" class="mt-6 flex items-center justify-between">
|
||||
<div class="text-sm text-mercedes-gray">
|
||||
<span id="pagination-info">Zeige 0 von 0 Aufträgen</span>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button id="prev-page" onclick="changePage(-1)" disabled
|
||||
class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-mercedes-light transition-all duration-200">
|
||||
Zurück
|
||||
</button>
|
||||
<span id="page-numbers" class="flex space-x-1">
|
||||
<!-- Page numbers will be inserted here -->
|
||||
</span>
|
||||
<button id="next-page" onclick="changePage(1)" disabled
|
||||
class="px-3 py-1 border border-mercedes-silver rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed hover:bg-mercedes-light transition-all duration-200">
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Detail Modal -->
|
||||
<div id="jobDetailModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="mercedes-card rounded-xl p-6 w-full max-w-2xl max-h-screen overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-mercedes-black">Auftrag Details</h2>
|
||||
<button onclick="hideJobDetailModal()" class="text-mercedes-gray hover:text-mercedes-black">
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="job-detail-content">
|
||||
<!-- Content will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let jobs = [];
|
||||
let filteredJobs = [];
|
||||
let printers = [];
|
||||
let currentPage = 1;
|
||||
const jobsPerPage = 10;
|
||||
|
||||
// Load data
|
||||
async function loadJobs() {
|
||||
try {
|
||||
const [jobsResponse, printersResponse] = await Promise.all([
|
||||
apiCall('/api/jobs'),
|
||||
apiCall('/api/printers')
|
||||
]);
|
||||
|
||||
jobs = jobsResponse.jobs || [];
|
||||
printers = printersResponse.printers || [];
|
||||
|
||||
populatePrinterFilter();
|
||||
filterJobs();
|
||||
} catch (error) {
|
||||
console.error('Error loading jobs:', error);
|
||||
showFlashMessage('Fehler beim Laden der Aufträge', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Populate printer filter
|
||||
function populatePrinterFilter() {
|
||||
const select = document.getElementById('printer-filter');
|
||||
select.innerHTML = '<option value="">Alle Drucker</option>';
|
||||
|
||||
printers.forEach(printer => {
|
||||
const option = document.createElement('option');
|
||||
option.value = printer.id;
|
||||
option.textContent = printer.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter jobs
|
||||
function filterJobs() {
|
||||
const statusFilter = document.getElementById('status-filter').value;
|
||||
const printerFilter = document.getElementById('printer-filter').value;
|
||||
const searchTerm = document.getElementById('search-input').value.toLowerCase();
|
||||
|
||||
filteredJobs = jobs.filter(job => {
|
||||
const matchesStatus = !statusFilter || job.status === statusFilter;
|
||||
const matchesPrinter = !printerFilter || job.printer_id == printerFilter;
|
||||
const matchesSearch = !searchTerm || job.filename.toLowerCase().includes(searchTerm);
|
||||
|
||||
return matchesStatus && matchesPrinter && matchesSearch;
|
||||
});
|
||||
|
||||
currentPage = 1;
|
||||
renderJobs();
|
||||
updatePagination();
|
||||
}
|
||||
|
||||
// Clear filters
|
||||
function clearFilters() {
|
||||
document.getElementById('status-filter').value = '';
|
||||
document.getElementById('printer-filter').value = '';
|
||||
document.getElementById('search-input').value = '';
|
||||
filterJobs();
|
||||
}
|
||||
|
||||
// Render jobs table
|
||||
function renderJobs() {
|
||||
const tbody = document.getElementById('jobs-table-body');
|
||||
|
||||
if (filteredJobs.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center">
|
||||
<svg class="h-16 w-16 text-mercedes-silver mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p class="text-mercedes-gray text-lg">Keine Aufträge gefunden</p>
|
||||
<a href="/new-job" class="mt-4 inline-block bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
Ersten Auftrag erstellen
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = (currentPage - 1) * jobsPerPage;
|
||||
const endIndex = startIndex + jobsPerPage;
|
||||
const pageJobs = filteredJobs.slice(startIndex, endIndex);
|
||||
|
||||
tbody.innerHTML = pageJobs.map(job => {
|
||||
const printer = printers.find(p => p.id === job.printer_id);
|
||||
const statusColor = getJobStatusColor(job.status);
|
||||
const statusText = getJobStatusText(job.status);
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-mercedes-light transition-colors duration-200">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<svg class="h-8 w-8 text-mercedes-blue mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-mercedes-black">${job.filename}</div>
|
||||
<div class="text-sm text-mercedes-gray">${formatFileSize(job.file_size)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}">
|
||||
${statusText}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-mercedes-black">
|
||||
${printer ? printer.name : 'Unbekannt'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="w-full bg-mercedes-silver rounded-full h-2">
|
||||
<div class="bg-mercedes-blue h-2 rounded-full" style="width: ${job.progress || 0}%"></div>
|
||||
</div>
|
||||
<div class="text-xs text-mercedes-gray mt-1">${job.progress || 0}%</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-mercedes-gray">
|
||||
${formatDate(job.created_at)}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="showJobDetail(${job.id})"
|
||||
class="text-mercedes-blue hover:text-blue-700 transition-colors duration-200">
|
||||
Details
|
||||
</button>
|
||||
${job.status === 'pending' ? `
|
||||
<button onclick="cancelJob(${job.id})"
|
||||
class="text-mercedes-red hover:text-red-700 transition-colors duration-200">
|
||||
Abbrechen
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function getJobStatusColor(status) {
|
||||
switch (status) {
|
||||
case 'pending': return 'bg-mercedes-yellow text-mercedes-black';
|
||||
case 'printing': return 'bg-mercedes-blue text-white';
|
||||
case 'completed': return 'bg-mercedes-green text-white';
|
||||
case 'failed': return 'bg-mercedes-red text-white';
|
||||
case 'cancelled': return 'bg-mercedes-gray text-white';
|
||||
default: return 'bg-mercedes-silver text-mercedes-black';
|
||||
}
|
||||
}
|
||||
|
||||
function getJobStatusText(status) {
|
||||
switch (status) {
|
||||
case 'pending': return 'Wartend';
|
||||
case 'printing': return 'Druckt';
|
||||
case 'completed': return 'Abgeschlossen';
|
||||
case 'failed': return 'Fehlgeschlagen';
|
||||
case 'cancelled': return 'Abgebrochen';
|
||||
default: return 'Unbekannt';
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
function updatePagination() {
|
||||
const totalPages = Math.ceil(filteredJobs.length / jobsPerPage);
|
||||
const startIndex = (currentPage - 1) * jobsPerPage + 1;
|
||||
const endIndex = Math.min(currentPage * jobsPerPage, filteredJobs.length);
|
||||
|
||||
document.getElementById('pagination-info').textContent =
|
||||
`Zeige ${startIndex}-${endIndex} von ${filteredJobs.length} Aufträgen`;
|
||||
|
||||
document.getElementById('prev-page').disabled = currentPage === 1;
|
||||
document.getElementById('next-page').disabled = currentPage === totalPages || totalPages === 0;
|
||||
|
||||
// Update page numbers
|
||||
const pageNumbers = document.getElementById('page-numbers');
|
||||
pageNumbers.innerHTML = '';
|
||||
|
||||
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = i;
|
||||
button.onclick = () => goToPage(i);
|
||||
button.className = `px-3 py-1 border rounded-lg text-sm transition-all duration-200 ${
|
||||
i === currentPage
|
||||
? 'bg-mercedes-blue text-white border-mercedes-blue'
|
||||
: 'border-mercedes-silver hover:bg-mercedes-light'
|
||||
}`;
|
||||
pageNumbers.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
function changePage(delta) {
|
||||
const totalPages = Math.ceil(filteredJobs.length / jobsPerPage);
|
||||
const newPage = currentPage + delta;
|
||||
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
currentPage = newPage;
|
||||
renderJobs();
|
||||
updatePagination();
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(page) {
|
||||
currentPage = page;
|
||||
renderJobs();
|
||||
updatePagination();
|
||||
}
|
||||
|
||||
// Job actions
|
||||
async function showJobDetail(jobId) {
|
||||
const job = jobs.find(j => j.id === jobId);
|
||||
if (!job) return;
|
||||
|
||||
const printer = printers.find(p => p.id === job.printer_id);
|
||||
const statusColor = getJobStatusColor(job.status);
|
||||
const statusText = getJobStatusText(job.status);
|
||||
|
||||
const content = document.getElementById('job-detail-content');
|
||||
content.innerHTML = `
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 class="font-medium text-mercedes-black mb-2">Dateiinformationen</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div><span class="font-medium text-mercedes-gray">Name:</span> ${job.filename}</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Größe:</span> ${formatFileSize(job.file_size)}</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Typ:</span> ${job.file_type || 'Unbekannt'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-medium text-mercedes-black mb-2">Druckstatus</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-mercedes-gray">Status:</span>
|
||||
<span class="ml-2 ${statusColor} px-2 py-1 rounded text-xs">${statusText}</span>
|
||||
</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Fortschritt:</span> ${job.progress || 0}%</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Drucker:</span> ${printer ? printer.name : 'Unbekannt'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-medium text-mercedes-black mb-2">Zeitstempel</h3>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div><span class="font-medium text-mercedes-gray">Erstellt:</span> ${formatDate(job.created_at)}</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Gestartet:</span> ${job.started_at ? formatDate(job.started_at) : 'Noch nicht gestartet'}</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Beendet:</span> ${job.completed_at ? formatDate(job.completed_at) : 'Noch nicht beendet'}</div>
|
||||
<div><span class="font-medium text-mercedes-gray">Geschätzte Zeit:</span> ${job.estimated_time ? formatDuration(job.estimated_time) : 'Unbekannt'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${job.error_message ? `
|
||||
<div>
|
||||
<h3 class="font-medium text-mercedes-red mb-2">Fehlermeldung</h3>
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">
|
||||
${job.error_message}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="flex space-x-3 pt-4 border-t border-mercedes-silver">
|
||||
${job.status === 'pending' ? `
|
||||
<button onclick="cancelJob(${job.id}); hideJobDetailModal();"
|
||||
class="bg-mercedes-red hover:bg-red-700 text-white py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
|
||||
Auftrag abbrechen
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
<button onclick="hideJobDetailModal()"
|
||||
class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('jobDetailModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideJobDetailModal() {
|
||||
document.getElementById('jobDetailModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function cancelJob(jobId) {
|
||||
if (!confirm('Sind Sie sicher, dass Sie diesen Auftrag abbrechen möchten?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiCall(`/api/jobs/${jobId}/cancel`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
showFlashMessage('Auftrag erfolgreich abgebrochen', 'success');
|
||||
loadJobs();
|
||||
} catch (error) {
|
||||
showFlashMessage('Fehler beim Abbrechen des Auftrags', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh jobs
|
||||
function refreshJobs() {
|
||||
showFlashMessage('Aufträge werden aktualisiert...', 'info');
|
||||
loadJobs();
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadJobs();
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(loadJobs, 30000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
384
backend/app/templates/login.html
Normal file
384
backend/app/templates/login.html
Normal 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 %}
|
384
backend/app/templates/new_job.html
Normal file
384
backend/app/templates/new_job.html
Normal 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 %}
|
362
backend/app/templates/printers.html
Normal file
362
backend/app/templates/printers.html
Normal 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 %}
|
410
backend/app/templates/profile.html
Normal file
410
backend/app/templates/profile.html
Normal 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 %}
|
@ -9,7 +9,7 @@ Flask-Login==0.6.3
|
||||
SQLAlchemy==2.0.23
|
||||
|
||||
# Smart Plug Steuerung
|
||||
PyP100==0.1.4
|
||||
PyP100==0.1.2
|
||||
|
||||
# Passwort-Hashing (bereits in Flask enthalten, aber explizit für Klarheit)
|
||||
Werkzeug==3.0.1
|
||||
|
157
frontend/check-backend-connection.sh
Executable file
157
frontend/check-backend-connection.sh
Executable 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 "=================================================="
|
@ -1,5 +1,5 @@
|
||||
# 🎨 MYP Frontend - Standalone Server Konfiguration
|
||||
# Frontend-Service als vollständig unabhängiger Server
|
||||
# 🎨 MYP Frontend - Entwicklungsumgebung Konfiguration
|
||||
# Frontend-Service für die Entwicklung mit Raspberry Pi Backend
|
||||
|
||||
version: '3.8'
|
||||
|
||||
@ -8,33 +8,38 @@ services:
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
- BUILDKIT_INLINE_CACHE=1
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
image: myp/frontend:latest
|
||||
container_name: myp-frontend-standalone
|
||||
- NODE_ENV=development
|
||||
image: myp/frontend:dev
|
||||
container_name: myp-frontend-dev
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
- NODE_ENV=development
|
||||
- NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Backend API Konfiguration
|
||||
- NEXT_PUBLIC_API_URL=${BACKEND_API_URL:-http://localhost:5000/api}
|
||||
- NEXT_PUBLIC_BACKEND_HOST=${BACKEND_HOST:-localhost:5000}
|
||||
# Backend API Konfiguration (Raspberry Pi)
|
||||
- NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
|
||||
- NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000
|
||||
|
||||
# Frontend Server
|
||||
- PORT=3000
|
||||
- HOSTNAME=0.0.0.0
|
||||
|
||||
# Auth Konfiguration
|
||||
- NEXTAUTH_URL=${FRONTEND_URL:-http://localhost:3000}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-frontend-auth-secret}
|
||||
# Auth Konfiguration (Entwicklung)
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- NEXTAUTH_SECRET=dev-frontend-auth-secret
|
||||
|
||||
# Debug-Einstellungen
|
||||
- DEBUG=true
|
||||
- NEXT_DEBUG=true
|
||||
|
||||
volumes:
|
||||
- frontend_data:/app/.next
|
||||
- frontend_cache:/app/.next/cache
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
- ./public:/app/public:ro
|
||||
|
||||
ports:
|
||||
@ -43,6 +48,9 @@ services:
|
||||
networks:
|
||||
- frontend-network
|
||||
|
||||
extra_hosts:
|
||||
- "raspberrypi:192.168.0.105"
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
@ -52,8 +60,8 @@ services:
|
||||
|
||||
labels:
|
||||
- "service.type=frontend"
|
||||
- "service.name=myp-frontend"
|
||||
- "service.environment=${NODE_ENV:-production}"
|
||||
- "service.name=myp-frontend-dev"
|
||||
- "service.environment=development"
|
||||
|
||||
# === FRONTEND CACHE (Optional: Redis für Session Management) ===
|
||||
frontend-cache:
|
||||
|
@ -5,25 +5,34 @@ services:
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: myp-rp
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: myp-rp-dev
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- NEXT_PUBLIC_API_URL=https://m040tbaraspi001.de040.corpintra.net/api
|
||||
- NODE_ENV=development
|
||||
- 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:
|
||||
- myp-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
|
||||
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# Caddy Proxy
|
||||
# Caddy Proxy (Entwicklung)
|
||||
caddy:
|
||||
image: caddy:2.7-alpine
|
||||
container_name: myp-caddy
|
||||
container_name: myp-caddy-dev
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
@ -36,9 +45,10 @@ services:
|
||||
- myp-network
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
- "raspberrypi:192.168.0.105"
|
||||
environment:
|
||||
- CADDY_HOST=53.37.211.254
|
||||
- CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net
|
||||
- CADDY_HOST=localhost
|
||||
- CADDY_DOMAIN=localhost
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
||||
|
@ -2,51 +2,54 @@
|
||||
debug
|
||||
}
|
||||
|
||||
# Hauptdomain und IP-Adresse für die Anwendung
|
||||
53.37.211.254, m040tbaraspi001.de040.corpintra.net, m040tbaraspi001, de040.corpintra.net, localhost {
|
||||
# API Anfragen zum Backend weiterleiten
|
||||
# Entwicklungsumgebung - Localhost und Raspberry Pi Backend
|
||||
localhost, 127.0.0.1 {
|
||||
# API Anfragen zum Raspberry Pi Backend weiterleiten
|
||||
@api {
|
||||
path /api/* /health
|
||||
}
|
||||
handle @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
|
||||
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 {
|
||||
on_demand
|
||||
}
|
||||
# TLS für Entwicklung deaktiviert
|
||||
tls off
|
||||
|
||||
# Erlaube HTTP -> HTTPS Redirects für OAuth
|
||||
# OAuth Callbacks für Entwicklung
|
||||
@oauth path /auth/login/callback*
|
||||
handle @oauth {
|
||||
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 {
|
||||
# Sicherheitsheader
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
# Weniger restriktive Sicherheitsheader für Entwicklung
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
|
||||
# Cache-Control für statische Assets
|
||||
@static {
|
||||
path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
|
||||
}
|
||||
header @static Cache-Control "public, max-age=86400"
|
||||
# Keine Caches für Entwicklung
|
||||
Cache-Control "no-store, no-cache, must-revalidate"
|
||||
|
||||
# Keine Caches für dynamische Inhalte
|
||||
@dynamic {
|
||||
not path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
|
||||
}
|
||||
header @dynamic Cache-Control "no-store, no-cache, must-revalidate"
|
||||
# CORS für Entwicklung
|
||||
Access-Control-Allow-Origin "*"
|
||||
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
Access-Control-Allow-Headers "Content-Type, Authorization"
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
# 🎨 MYP Frontend - Standalone Server Konfiguration
|
||||
# Umgebungsvariablen ausschließlich für den Frontend-Server
|
||||
# 🎨 MYP Frontend - Entwicklungsumgebung Konfiguration
|
||||
# Umgebungsvariablen für die Verbindung zum Raspberry Pi Backend
|
||||
|
||||
# === NODE.JS KONFIGURATION ===
|
||||
NODE_ENV=production
|
||||
NODE_ENV=development
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# === FRONTEND SERVER ===
|
||||
@ -11,18 +11,18 @@ HOSTNAME=0.0.0.0
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
|
||||
# === BACKEND API KONFIGURATION ===
|
||||
# Backend-Server Verbindung (HTTP)
|
||||
BACKEND_API_URL=http://localhost:5000/api
|
||||
BACKEND_HOST=localhost:5000
|
||||
NEXT_PUBLIC_API_URL=http://localhost:5000/api
|
||||
NEXT_PUBLIC_BACKEND_HOST=localhost:5000
|
||||
# Backend-Server Verbindung (Raspberry Pi)
|
||||
BACKEND_API_URL=http://192.168.0.105:5000/api
|
||||
BACKEND_HOST=192.168.0.105:5000
|
||||
NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
|
||||
NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000
|
||||
|
||||
# === AUTHENTIFIZIERUNG ===
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_SECRET=frontend-auth-secret-2024
|
||||
JWT_SECRET=frontend-jwt-secret-2024
|
||||
NEXTAUTH_SECRET=dev-frontend-auth-secret-2024
|
||||
JWT_SECRET=dev-frontend-jwt-secret-2024
|
||||
|
||||
# OAuth Provider (falls verwendet)
|
||||
# OAuth Provider (Entwicklung)
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
MICROSOFT_CLIENT_ID=
|
||||
@ -35,7 +35,7 @@ FRONTEND_DB_PATH=db/frontend.db
|
||||
|
||||
# === CACHE KONFIGURATION ===
|
||||
# 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_PORT=6380
|
||||
FRONTEND_REDIS_DB=1
|
||||
@ -45,19 +45,19 @@ CDN_URL=http://localhost:8080
|
||||
ASSETS_URL=http://localhost:8080/static
|
||||
|
||||
# === SICHERHEIT ===
|
||||
# CSP (Content Security Policy)
|
||||
# CSP (Content Security Policy) - Entwicklung
|
||||
CSP_SCRIPT_SRC="'self' 'unsafe-inline' 'unsafe-eval'"
|
||||
CSP_STYLE_SRC="'self' 'unsafe-inline'"
|
||||
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 ===
|
||||
ANALYTICS_ENABLED=true
|
||||
ANALYTICS_ENABLED=false
|
||||
ERROR_REPORTING_ENABLED=true
|
||||
|
||||
# === ENTWICKLUNG ===
|
||||
DEBUG=false
|
||||
NEXT_DEBUG=false
|
||||
DEBUG=true
|
||||
NEXT_DEBUG=true
|
||||
|
||||
# === BUILD KONFIGURATION ===
|
||||
ANALYZE=false
|
||||
|
@ -1,5 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Entwicklungsumgebung - weniger restriktive CORS
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@ -7,7 +8,7 @@ const nextConfig = {
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "m040tbaraspi001.de040.corpintra.net",
|
||||
value: "*",
|
||||
},
|
||||
{
|
||||
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;
|
||||
|
66
frontend/setup-backend-url.sh
Normal file → Executable file
66
frontend/setup-backend-url.sh
Normal file → Executable file
@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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
|
||||
RED='\033[0;31m'
|
||||
@ -22,59 +22,57 @@ error_log() {
|
||||
# Definiere Variablen
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
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
|
||||
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
|
||||
log "Entwicklungsumgebung - Raspberry Pi Backend: ${BACKEND_URL}"
|
||||
|
||||
# Bestimme den Hostnamen für OAuth
|
||||
HOSTNAME=$(hostname)
|
||||
if [[ "$HOSTNAME" == *"m040tbaraspi001"* ]] || [[ "$HOSTNAME" == *"corpintra"* ]]; then
|
||||
FRONTEND_HOSTNAME="m040tbaraspi001.de040.corpintra.net"
|
||||
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
|
||||
# Hostname für OAuth (Entwicklung)
|
||||
FRONTEND_HOSTNAME="localhost"
|
||||
OAUTH_URL="http://localhost:3000/auth/login/callback"
|
||||
log "Frontend-Hostname (Entwicklung): $FRONTEND_HOSTNAME"
|
||||
|
||||
# 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
|
||||
# Backend API Konfiguration
|
||||
# Backend API Konfiguration - Raspberry Pi (Entwicklung)
|
||||
NEXT_PUBLIC_API_URL=${BACKEND_URL}
|
||||
|
||||
# Frontend-URL für OAuth Callback
|
||||
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}
|
||||
# Frontend-URL für OAuth Callback (Entwicklung)
|
||||
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}
|
||||
|
||||
# OAuth Konfiguration (falls nötig)
|
||||
OAUTH_CLIENT_ID=client_id
|
||||
OAUTH_CLIENT_SECRET=client_secret
|
||||
# Entwicklungsumgebung
|
||||
NODE_ENV=development
|
||||
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
|
||||
|
||||
# Überprüfe, ob die Datei erstellt wurde
|
||||
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
|
||||
error_log "Konnte .env.local Datei nicht erstellen."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Hinweis für Docker-Installation
|
||||
log "${YELLOW}WICHTIG: Wenn Sie Docker verwenden, stellen Sie sicher, dass Sie die Umgebungsvariable setzen:${NC}"
|
||||
log "NEXT_PUBLIC_API_URL=${BACKEND_URL}"
|
||||
# Hinweis für Entwicklungsumgebung
|
||||
log "${YELLOW}ENTWICKLUNGSUMGEBUNG KONFIGURIERT:${NC}"
|
||||
log "Backend (Raspberry Pi): ${BACKEND_URL}"
|
||||
log "Frontend: http://localhost:3000"
|
||||
log "OAuth Callback: ${OAUTH_URL}"
|
||||
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
|
||||
chmod 600 "$ENV_FILE"
|
||||
|
@ -7,7 +7,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// 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';
|
||||
|
||||
try {
|
||||
|
Loading…
x
Reference in New Issue
Block a user