🎉 Refactored backend structure: Removed unused files including app_cleaned.py, admin_api.py, admin.py, user.py, and others. Updated settings.local.json to include additional Bash commands. Enhanced admin templates for better navigation and functionality. Improved logging and error handling across various modules.

This commit is contained in:
2025-06-09 19:33:06 +02:00
parent 876b5a64e4
commit c7f9738bbe
115 changed files with 23507 additions and 9958 deletions

View File

@ -232,7 +232,7 @@ document.addEventListener('DOMContentLoaded', function() {
<!-- Navigation Tabs -->
<div class="bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-3xl border border-white/20 dark:border-slate-700/50 p-4 mb-12 shadow-xl">
<nav class="flex space-x-2" aria-label="Tabs">
<a href="{{ url_for('admin_page', tab='users') }}"
<a href="{{ url_for('admin.users_overview') }}"
class="group flex items-center px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300 {{ 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg' if active_tab == 'users' else 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-white' }}">
<svg class="w-5 h-5 mr-2 {{ 'text-white' if active_tab == 'users' else 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
@ -240,7 +240,7 @@ document.addEventListener('DOMContentLoaded', function() {
Benutzer
</a>
<a href="{{ url_for('admin_page', tab='printers') }}"
<a href="{{ url_for('admin.printers_overview') }}"
class="group flex items-center px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300 {{ 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg' if active_tab == 'printers' else 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-white' }}">
<svg class="w-5 h-5 mr-2 {{ 'text-white' if active_tab == 'printers' else 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
@ -248,7 +248,7 @@ document.addEventListener('DOMContentLoaded', function() {
Drucker-Steckdosen
</a>
<a href="{{ url_for('admin_page', tab='jobs') }}"
<a href="{{ url_for('jobs_page') }}"
class="group flex items-center px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300 {{ 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg' if active_tab == 'jobs' else 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-white' }}">
<svg class="w-5 h-5 mr-2 {{ 'text-white' if active_tab == 'jobs' else 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
@ -256,7 +256,7 @@ document.addEventListener('DOMContentLoaded', function() {
Reservierungen
</a>
<a href="{{ url_for('admin_page', tab='system') }}"
<a href="{{ url_for('admin.advanced_settings') }}"
class="group flex items-center px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300 {{ 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg' if active_tab == 'system' else 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-white' }}">
<svg class="w-5 h-5 mr-2 {{ 'text-white' if active_tab == 'system' else 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"/>
@ -265,7 +265,7 @@ document.addEventListener('DOMContentLoaded', function() {
System
</a>
<a href="{{ url_for('admin_page', tab='logs') }}"
<a href="{{ url_for('admin.logs_overview') }}"
class="group flex items-center px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300 {{ 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg' if active_tab == 'logs' else 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-white' }}">
<svg class="w-5 h-5 mr-2 {{ 'text-white' if active_tab == 'logs' else 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"/>
@ -273,7 +273,7 @@ document.addEventListener('DOMContentLoaded', function() {
Logs
</a>
<a href="{{ url_for('admin_guest_requests') }}"
<a href="{{ url_for('admin.guest_requests') }}"
class="group flex items-center px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300 {{ 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg' if active_tab == 'guest_requests' else 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:text-slate-900 dark:hover:text-white' }}">
<svg class="w-5 h-5 mr-2 {{ 'text-white' if active_tab == 'guest_requests' else 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>

View File

@ -29,7 +29,7 @@
<i class="fas fa-print text-blue-600 dark:text-blue-400 text-2xl"></i>
<h1 class="text-2xl font-bold text-slate-800 dark:text-white">Neuen Drucker hinzufügen</h1>
</div>
<a href="{{ url_for('admin_page', tab='printers') }}"
<a href="{{ url_for('admin.admin_dashboard', tab='printers') }}"
class="bg-slate-500 hover:bg-slate-600 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-arrow-left mr-2"></i>Zurück
</a>
@ -38,7 +38,7 @@
<!-- Formular -->
<div class="form-container rounded-lg p-6">
<form action="{{ url_for('admin_create_printer_form') }}" method="POST" class="space-y-6">
<form action="{{ url_for('admin.create_printer_api') }}" method="POST" class="space-y-6">
<!-- CSRF Token -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
@ -142,7 +142,7 @@
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-save mr-2"></i>Drucker erstellen
</button>
<a href="{{ url_for('admin_page', tab='printers') }}"
<a href="{{ url_for('admin.admin_dashboard', tab='printers') }}"
class="flex-1 bg-slate-500 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-center transition-colors">
<i class="fas fa-times mr-2"></i>Abbrechen
</a>

View File

@ -153,7 +153,7 @@
</div>
<!-- Zurück Button -->
<a href="{{ url_for('admin_page', tab='users') }}"
<a href="{{ url_for('admin.admin_dashboard', tab='users') }}"
class="btn-mercedes-secondary text-white px-6 py-3 rounded-xl font-medium transition-all duration-300 flex items-center space-x-2 hover:scale-105">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
@ -165,7 +165,7 @@
<!-- Hauptformular -->
<div class="admin-form-container rounded-2xl p-8 transition-all duration-300">
<form id="userForm" action="{{ url_for('admin_create_user_form') }}" method="POST" class="space-y-8">
<form id="userForm" action="{{ url_for('admin.create_user_api') }}" method="POST" class="space-y-8">
<!-- CSRF Token -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
@ -343,7 +343,7 @@
</div>
</button>
<a href="{{ url_for('admin_page', tab='users') }}"
<a href="{{ url_for('admin.admin_dashboard', tab='users') }}"
class="btn-mercedes-secondary text-white px-8 py-4 rounded-xl font-semibold text-center transition-all duration-300 flex items-center justify-center space-x-3 hover:scale-105">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>

View File

@ -345,7 +345,7 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<a href="{{ url_for('admin_page') }}" class="p-3 hover:bg-black/5 dark:hover:bg-white/5 rounded-2xl transition-all duration-300 group">
<a href="{{ url_for('admin.admin_dashboard') }}" class="p-3 hover:bg-black/5 dark:hover:bg-white/5 rounded-2xl transition-all duration-300 group">
<svg class="w-6 h-6 text-slate-600 dark:text-slate-400 group-hover:text-slate-900 dark:group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>

View File

@ -29,7 +29,7 @@
<i class="fas fa-edit text-blue-600 dark:text-blue-400 text-2xl"></i>
<h1 class="text-2xl font-bold text-slate-800 dark:text-white">Drucker bearbeiten</h1>
</div>
<a href="{{ url_for('admin_page', tab='printers') }}"
<a href="{{ url_for('admin.admin_dashboard', tab='printers') }}"
class="bg-slate-500 hover:bg-slate-600 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-arrow-left mr-2"></i>Zurück
</a>
@ -45,7 +45,7 @@
<!-- Formular -->
<div class="form-container rounded-lg p-6">
<form action="{{ url_for('admin_update_printer_form', printer_id=printer.id) }}" method="POST" class="space-y-6">
<form action="{{ url_for('admin.update_printer_api', printer_id=printer.id) }}" method="POST" class="space-y-6">
<!-- CSRF Token -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
@ -185,7 +185,7 @@
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-save mr-2"></i>Änderungen speichern
</button>
<a href="{{ url_for('admin_page', tab='printers') }}"
<a href="{{ url_for('admin.admin_dashboard', tab='printers') }}"
class="flex-1 bg-slate-500 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-center transition-colors">
<i class="fas fa-times mr-2"></i>Abbrechen
</a>

View File

@ -221,7 +221,7 @@ input:checked + .toggle-slider:before {
</p>
</div>
</div>
<a href="{{ url_for('admin_page', tab='users') }}"
<a href="{{ url_for('admin.admin_dashboard', tab='users') }}"
class="group inline-flex items-center px-6 py-3 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm text-slate-700 dark:text-slate-300 rounded-2xl hover:bg-white dark:hover:bg-slate-700 transition-all duration-300 shadow-lg hover:shadow-xl border border-white/20 dark:border-slate-700/50">
<svg class="w-5 h-5 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
@ -234,7 +234,7 @@ input:checked + .toggle-slider:before {
<!-- Modern Form with Glass Effect -->
<div class="glass-card rounded-3xl p-10 shadow-2xl">
<form method="POST" action="{{ url_for('admin_update_user_form', user_id=user.id) }}" class="space-y-8" id="userEditForm">
<form method="POST" action="{{ url_for('admin.update_user_api', user_id=user.id) }}" class="space-y-8" id="userEditForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="_method" value="PUT"/>
@ -494,7 +494,7 @@ input:checked + .toggle-slider:before {
</div>
<div class="flex items-center space-x-4">
<a href="{{ url_for('admin_page', tab='users') }}"
<a href="{{ url_for('admin.admin_dashboard', tab='users') }}"
class="group inline-flex items-center px-8 py-4 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm text-slate-700 dark:text-slate-300 rounded-2xl hover:bg-white dark:hover:bg-slate-700 transition-all duration-300 shadow-lg hover:shadow-xl border border-white/20 dark:border-slate-700/50">
<svg class="w-5 h-5 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>

View File

@ -71,7 +71,7 @@
<!-- Back to Admin Button -->
<div>
<a href="{{ url_for('admin_page') }}"
<a href="{{ url_for('admin.admin_dashboard') }}"
class="inline-flex items-center px-6 py-3 bg-white/15 dark:bg-white/10 backdrop-blur-sm border border-white/30 dark:border-white/20 rounded-xl text-white hover:bg-white/25 dark:hover:bg-white/15 transition-all duration-300 hover:scale-105">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>

View File

@ -18,7 +18,7 @@
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">{{ printer.name }} verwalten</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2">Verwaltung und Überwachung des Druckers</p>
</div>
<a href="{{ url_for('admin_page', tab='printers') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<a href="{{ url_for('admin.admin_dashboard', tab='printers') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
@ -63,7 +63,7 @@
class="w-full px-4 py-2 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-all duration-300">
Verbindung testen
</button>
<a href="{{ url_for('admin_printer_settings_page', printer_id=printer.id) }}"
<a href="{{ url_for('admin.edit_printer_page', printer_id=printer.id) }}"
class="block w-full px-4 py-2 bg-slate-500 text-white rounded-xl hover:bg-slate-600 transition-all duration-300 text-center">
Einstellungen
</a>

View File

@ -311,7 +311,7 @@
<nav class="text-sm font-medium text-slate-600 dark:text-slate-400 mb-4">
<ol class="list-none p-0 inline-flex">
<li class="flex items-center">
<a href="{{ url_for('admin_page') }}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">Admin-Dashboard</a>
<a href="{{ url_for('admin.admin_dashboard') }}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">Admin-Dashboard</a>
<svg class="fill-current w-3 h-3 mx-3 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
<path d="m285.476 272.971c4.686 4.686 4.686 12.284 0 16.97l-133.952 133.954c-4.686 4.686-12.284 4.686-16.97 0l-133.952-133.954c-4.686-4.686-4.686-12.284 0-16.97 4.686-4.686 12.284-4.686 16.97 0l125.462 125.463 125.462-125.463c4.686-4.686 12.284-4.686 16.97 0z"/>
</svg>

View File

@ -18,7 +18,7 @@
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">{{ printer.name }} - Einstellungen</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2">Konfiguration und Einstellungen des Druckers</p>
</div>
<a href="{{ url_for('admin_manage_printer_page', printer_id=printer.id) }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<a href="{{ url_for('admin.admin_dashboard', tab='printers') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
@ -29,7 +29,7 @@
<!-- Form -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl p-8">
<form method="POST" action="{{ url_for('admin_update_printer_form', printer_id=printer.id) }}" class="space-y-6">
<form method="POST" action="{{ url_for('admin.update_printer_api', printer_id=printer.id) }}" class="space-y-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="_method" value="PUT"/>
@ -103,7 +103,7 @@
<!-- Buttons -->
<div class="flex items-center justify-end space-x-4 pt-4">
<a href="{{ url_for('admin_manage_printer_page', printer_id=printer.id) }}"
<a href="{{ url_for('admin.admin_dashboard', tab='printers') }}"
class="px-6 py-3 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700 transition-all duration-300">
Abbrechen
</a>

View File

@ -18,7 +18,7 @@
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Admin-Einstellungen</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2">Systemkonfiguration und Verwaltungsoptionen</p>
</div>
<a href="{{ url_for('admin_page') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<a href="{{ url_for('admin.admin_dashboard') }}" class="inline-flex items-center px-4 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>

View File

@ -84,11 +84,11 @@
{% if current_user.is_authenticated %}
<!-- Simplified navigation links -->
<a href="{{ url_for('dashboard') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Dashboard</a>
<a href="{{ url_for('printers') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Drucker</a>
<a href="{{ url_for('jobs') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Aufträge</a>
<a href="{{ url_for('printers_page') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Drucker</a>
<a href="{{ url_for('jobs_page') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Aufträge</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Admin</a>
<a href="{{ url_for('admin.admin_dashboard') }}" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800">Admin</a>
{% endif %}
<!-- User menu -->
@ -99,7 +99,7 @@
</div>
</button>
<div id="user-dropdown" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-slate-800 rounded-lg shadow-lg">
<a href="{{ url_for('user_profile') }}" class="block px-4 py-2 text-sm hover:bg-slate-100 dark:hover:bg-slate-700">Profil</a>
<a href="{{ url_for('users.user_profile') }}" class="block px-4 py-2 text-sm hover:bg-slate-100 dark:hover:bg-slate-700">Profil</a>
<a href="{{ url_for('auth.logout') }}" class="block px-4 py-2 text-sm hover:bg-slate-100 dark:hover:bg-slate-700">Abmelden</a>
</div>
</div>

View File

@ -343,8 +343,8 @@
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin_page') }}"
class="nav-item {{ 'active' if request.endpoint == 'admin_page' else '' }}">
<a href="{{ url_for('admin.admin_dashboard') }}"
class="nav-item {{ 'active' if request.endpoint == 'admin.admin_dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
@ -489,7 +489,7 @@
<!-- Menu Items -->
<div class="py-1">
<a href="{{ url_for('user_profile') }}"
<a href="{{ url_for('users.user_profile') }}"
class="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
@ -497,7 +497,7 @@
Mein Profil
</a>
<a href="{{ url_for('user_settings') }}"
<a href="{{ url_for('users.user_settings') }}"
class="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
@ -552,7 +552,7 @@
</div>
{% else %}
<!-- Login Button - kompakteres Design -->
<a href="{{ url_for('login') }}"
<a href="{{ url_for('auth.login') }}"
class="flex items-center space-x-1 py-1 px-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white text-xs">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
@ -625,8 +625,8 @@
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'admin_page' else '' }}">
<a href="{{ url_for('admin.admin_dashboard') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'admin.admin_dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
@ -702,7 +702,7 @@
</div>
{% if current_user.is_authenticated and current_user.is_admin %}
<div class="pt-2 border-t border-slate-200 dark:border-slate-600">
<a href="{{ url_for('admin_plug_schedules') }}"
<a href="{{ url_for('admin.admin_plug_schedules') }}"
class="flex items-center space-x-2 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400">
<span class="text-sm">🔌</span>
<span class="text-xs">Steckdosenschaltzeiten</span>
@ -807,7 +807,7 @@
// Logout-Formular erstellen und absenden
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("auth_logout") }}';
form.action = '{{ url_for("auth.logout") }}';
form.style.display = 'none';
// CSRF-Token hinzufügen falls verfügbar

View File

@ -349,8 +349,8 @@
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin_page') }}"
class="nav-item {{ 'active' if request.endpoint == 'admin_page' else '' }}">
<a href="{{ url_for('admin.admin_dashboard') }}"
class="nav-item {{ 'active' if request.endpoint == 'admin.admin_dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
@ -503,7 +503,7 @@
<!-- Menu Items -->
<div class="py-1">
<a href="{{ url_for('user_profile') }}"
<a href="{{ url_for('users.user_profile') }}"
class="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
@ -511,7 +511,7 @@
Mein Profil
</a>
<a href="{{ url_for('user_settings') }}"
<a href="{{ url_for('users.user_settings') }}"
class="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
@ -566,7 +566,7 @@
</div>
{% else %}
<!-- Login Button - kompakteres Design -->
<a href="{{ url_for('login') }}"
<a href="{{ url_for('auth.login') }}"
class="flex items-center space-x-1 py-1 px-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white text-xs transition-colors duration-200">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
@ -639,8 +639,8 @@
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'admin_page' else '' }}">
<a href="{{ url_for('admin.admin_dashboard') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'admin.admin_dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
@ -716,7 +716,7 @@
</div>
{% if current_user.is_authenticated and current_user.is_admin %}
<div class="pt-2 border-t border-slate-200 dark:border-slate-600">
<a href="{{ url_for('admin_plug_schedules') }}"
<a href="{{ url_for('admin.admin_dashboard') }}"
class="flex items-center space-x-2 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200">
<span class="text-sm">🔌</span>
<span class="text-xs">Steckdosenschaltzeiten</span>

View File

@ -308,10 +308,18 @@
<a href="{{ url_for('printers_page') }}"
class="nav-item {{ 'active' if request.endpoint == 'printers_page' else '' }}">
<i class="fas fa-plug w-5 h-5"></i>
<span>Drucker-Steckdosen</span>
<i class="fas fa-print w-5 h-5"></i>
<span>Drucker</span>
</a>
{% if current_user.is_authenticated and current_user.has_permission('CONTROL_PRINTER') %}
<a href="{{ url_for('tapo.tapo_dashboard') }}"
class="nav-item {{ 'active' if request.endpoint == 'tapo.tapo_dashboard' else '' }}">
<i class="fas fa-plug w-5 h-5"></i>
<span>Tapo-Steckdosen</span>
</a>
{% endif %}
<a href="{{ url_for('jobs_page') }}"
class="nav-item {{ 'active' if request.endpoint == 'jobs_page' else '' }}">
<i class="fas fa-clipboard-list w-5 h-5"></i>
@ -343,8 +351,8 @@
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin_page') }}"
class="nav-item {{ 'active' if request.endpoint == 'admin_page' else '' }}">
<a href="{{ url_for('admin.admin_dashboard') }}"
class="nav-item {{ 'active' if request.endpoint == 'admin.admin_dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
@ -460,7 +468,7 @@
<!-- Menu Items -->
<div class="py-1">
<a href="{{ url_for('user_profile') }}"
<a href="{{ url_for('users.user_profile') }}"
class="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
@ -468,7 +476,7 @@
Mein Profil
</a>
<a href="{{ url_for('user_settings') }}"
<a href="{{ url_for('users.user_settings') }}"
class="flex items-center px-4 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
@ -536,7 +544,7 @@
</button>
<!-- Login Button - kompakteres Design -->
<a href="{{ url_for('login') }}"
<a href="{{ url_for('auth.login') }}"
class="flex items-center space-x-1 py-1 px-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white text-xs transition-colors duration-200">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
@ -565,9 +573,19 @@
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<span>Drucker-Steckdosen</span>
<span>Drucker</span>
</a>
{% if current_user.is_authenticated and current_user.has_permission('CONTROL_PRINTER') %}
<a href="{{ url_for('tapo.tapo_dashboard') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'tapo.tapo_dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-3a2 2 0 00-2 2v1a2 2 0 002 2h3m4-6v1a2 2 0 01-2 2H9a2 2 0 01-2-2v-1m8-2V9a2 2 0 00-2-2H9a2 2 0 00-2 2v3.5M13 9h3l2 5H5l2-5h3"/>
</svg>
<span>Tapo-Steckdosen</span>
</a>
{% endif %}
<a href="{{ url_for('jobs_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'jobs_page' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
@ -609,8 +627,8 @@
</a>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin_page') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'admin_page' else '' }}">
<a href="{{ url_for('admin.admin_dashboard') }}"
class="mobile-nav-item {{ 'active' if request.endpoint == 'admin.admin_dashboard' else '' }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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"/>
@ -686,10 +704,10 @@
</div>
{% if current_user.is_authenticated and current_user.is_admin %}
<div class="pt-2 border-t border-slate-200 dark:border-slate-600">
<a href="{{ url_for('admin_plug_schedules') }}"
<a href="{{ url_for('admin.admin_dashboard') }}"
class="flex items-center space-x-2 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200">
<span class="text-sm">🔌</span>
<span class="text-xs">Steckdosenschaltzeiten</span>
<span class="text-sm">⚙️</span>
<span class="text-xs">Admin-Dashboard</span>
</a>
</div>
{% endif %}
@ -795,7 +813,7 @@
// Logout-Formular erstellen und absenden
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("auth_logout") }}';
form.action = '{{ url_for("auth.logout") }}';
form.style.display = 'none';
// CSRF-Token hinzufügen falls verfügbar

View File

@ -401,7 +401,7 @@
<div class="text-xs text-right mt-1 text-slate-500 dark:text-slate-400">{{ job.progress }}%</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<a href="{{ url_for('job_detail', job_id=job.id) }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 font-medium">Details</a>
<a href="{{ url_for('jobs.get_job', job_id=job.id) }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 font-medium">Details</a>
</td>
</tr>
{% endfor %}

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>400 - Ungültige Anfrage | MYP System</title>
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/glassmorphism.min.css') }}" rel="stylesheet">
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 min-h-screen flex items-center justify-center">
<div class="glass-card max-w-md w-full mx-4 p-8 text-center">
<div class="mb-6">
<div class="text-6xl font-bold text-red-500 mb-4">400</div>
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-2">Ungültige Anfrage</h1>
<p class="text-gray-600 dark:text-gray-300">
Die Anfrage konnte nicht verarbeitet werden. Bitte überprüfen Sie Ihre Eingaben.
</p>
</div>
<div class="space-y-4">
<a href="{{ url_for('dashboard') if current_user.is_authenticated else url_for('auth.login') }}"
class="block w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
{% if current_user.is_authenticated %}
Zum Dashboard
{% else %}
Zur Anmeldung
{% endif %}
</a>
<button onclick="history.back()"
class="block w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors">
Zurück
</button>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>405 - Methode nicht erlaubt | MYP System</title>
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/glassmorphism.min.css') }}" rel="stylesheet">
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 min-h-screen flex items-center justify-center">
<div class="glass-card max-w-md w-full mx-4 p-8 text-center">
<div class="mb-6">
<div class="text-6xl font-bold text-orange-500 mb-4">405</div>
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-2">Methode nicht erlaubt</h1>
<p class="text-gray-600 dark:text-gray-300">
Die verwendete HTTP-Methode ist für diese URL nicht erlaubt.
</p>
</div>
<div class="space-y-4">
<a href="{{ url_for('dashboard') if current_user.is_authenticated else url_for('auth.login') }}"
class="block w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
{% if current_user.is_authenticated %}
Zum Dashboard
{% else %}
Zur Anmeldung
{% endif %}
</a>
<button onclick="history.back()"
class="block w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors">
Zurück
</button>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>413 - Datei zu groß | MYP System</title>
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/glassmorphism.min.css') }}" rel="stylesheet">
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 min-h-screen flex items-center justify-center">
<div class="glass-card max-w-md w-full mx-4 p-8 text-center">
<div class="mb-6">
<div class="text-6xl font-bold text-yellow-500 mb-4">413</div>
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-2">Datei zu groß</h1>
<p class="text-gray-600 dark:text-gray-300">
Die hochgeladene Datei ist zu groß. Maximale Dateigröße: 16 MB.
</p>
</div>
<div class="space-y-4">
<a href="{{ url_for('dashboard') if current_user.is_authenticated else url_for('auth.login') }}"
class="block w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
{% if current_user.is_authenticated %}
Zum Dashboard
{% else %}
Zur Anmeldung
{% endif %}
</a>
<button onclick="history.back()"
class="block w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors">
Zurück
</button>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>429 - Zu viele Anfragen | MYP System</title>
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/glassmorphism.min.css') }}" rel="stylesheet">
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 min-h-screen flex items-center justify-center">
<div class="glass-card max-w-md w-full mx-4 p-8 text-center">
<div class="mb-6">
<div class="text-6xl font-bold text-red-500 mb-4">429</div>
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-2">Zu viele Anfragen</h1>
<p class="text-gray-600 dark:text-gray-300">
Sie haben zu viele Anfragen gesendet. Bitte warten Sie einen Moment und versuchen Sie es erneut.
</p>
</div>
<div class="mb-6">
<div class="bg-yellow-100 dark:bg-yellow-900 border border-yellow-400 text-yellow-700 dark:text-yellow-300 px-4 py-3 rounded">
<p class="text-sm">
<strong>Tipp:</strong> Warten Sie 60 Sekunden und versuchen Sie es dann erneut.
</p>
</div>
</div>
<div class="space-y-4">
<a href="{{ url_for('dashboard') if current_user.is_authenticated else url_for('auth.login') }}"
class="block w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
{% if current_user.is_authenticated %}
Zum Dashboard
{% else %}
Zur Anmeldung
{% endif %}
</a>
<button onclick="setTimeout(() => history.back(), 1000)"
class="block w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors">
Zurück (in 1 Sekunde)
</button>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>502 - Gateway-Fehler | MYP System</title>
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/glassmorphism.min.css') }}" rel="stylesheet">
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 min-h-screen flex items-center justify-center">
<div class="glass-card max-w-md w-full mx-4 p-8 text-center">
<div class="mb-6">
<div class="text-6xl font-bold text-red-600 mb-4">502</div>
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-2">Gateway-Fehler</h1>
<p class="text-gray-600 dark:text-gray-300">
Der Server ist vorübergehend nicht verfügbar. Bitte versuchen Sie es in wenigen Minuten erneut.
</p>
</div>
<div class="mb-6">
<div class="bg-red-100 dark:bg-red-900 border border-red-400 text-red-700 dark:text-red-300 px-4 py-3 rounded">
<p class="text-sm">
<strong>Hinweis:</strong> Dies ist ein temporärer Fehler. Der Service wird automatisch wiederhergestellt.
</p>
</div>
</div>
<div class="space-y-4">
<button onclick="location.reload()"
class="block w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
Seite neu laden
</button>
<a href="{{ url_for('dashboard') if current_user.is_authenticated else url_for('auth.login') }}"
class="block w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors">
{% if current_user.is_authenticated %}
Zum Dashboard
{% else %}
Zur Anmeldung
{% endif %}
</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>503 - Service nicht verfügbar | MYP System</title>
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/glassmorphism.min.css') }}" rel="stylesheet">
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 min-h-screen flex items-center justify-center">
<div class="glass-card max-w-md w-full mx-4 p-8 text-center">
<div class="mb-6">
<div class="text-6xl font-bold text-orange-600 mb-4">503</div>
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-2">Service nicht verfügbar</h1>
<p class="text-gray-600 dark:text-gray-300">
Der Service ist vorübergehend nicht verfügbar. Wir arbeiten an der Behebung des Problems.
</p>
</div>
<div class="mb-6">
<div class="bg-orange-100 dark:bg-orange-900 border border-orange-400 text-orange-700 dark:text-orange-300 px-4 py-3 rounded">
<p class="text-sm">
<strong>Wartung:</strong> Der Service wird in Kürze wieder verfügbar sein.
</p>
</div>
</div>
<div class="space-y-4">
<button onclick="setTimeout(() => location.reload(), 5000)"
class="block w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
Automatisch neu laden (5s)
</button>
<button onclick="location.reload()"
class="block w-full bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
Jetzt neu laden
</button>
<a href="{{ url_for('dashboard') if current_user.is_authenticated else url_for('auth.login') }}"
class="block w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors">
{% if current_user.is_authenticated %}
Zum Dashboard
{% else %}
Zur Anmeldung
{% endif %}
</a>
</div>
</div>
<script>
// Automatisches Neuladen nach 30 Sekunden
setTimeout(() => {
location.reload();
}, 30000);
</script>
</body>
</html>

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="de" x-data="{ darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches || localStorage.getItem('theme') === 'dark' }" x-bind:class="{ 'dark': darkMode }">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>505 - HTTP-Version nicht unterstützt | MYP 3D-Druck-Management</title>
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/glassmorphism.min.css') }}" rel="stylesheet">
<style>
.error-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
</style>
</head>
<body class="min-h-screen bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-900 dark:to-slate-800 flex items-center justify-center px-4 error-bg">
<div class="max-w-lg w-full">
<div class="glassmorphism-card text-center p-8">
<div class="mb-6">
<div class="w-24 h-24 mx-auto bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mb-4">
<svg class="w-12 h-12 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 19c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
<h1 class="text-4xl font-bold text-slate-800 dark:text-white mb-2">505</h1>
<h2 class="text-xl font-semibold text-slate-700 dark:text-slate-300 mb-4">HTTP-Version nicht unterstützt</h2>
<p class="text-slate-600 dark:text-slate-400 leading-relaxed">
Die verwendete HTTP-Version wird vom Server nicht unterstützt. Bitte verwenden Sie einen aktuellen Browser.
</p>
</div>
<div class="space-y-3">
<a href="{{ url_for('dashboard') if current_user.is_authenticated else url_for('auth.login') }}"
class="inline-flex items-center justify-center w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>
Zur Startseite
</a>
<button onclick="history.back()"
class="inline-flex items-center justify-center w-full px-6 py-3 bg-slate-600 hover:bg-slate-700 dark:bg-slate-500 dark:hover:bg-slate-600 text-white font-medium rounded-lg transition-all duration-200">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Zurück
</button>
</div>
</div>
</div>
</body>
</html>

View File

@ -404,7 +404,7 @@
Zum Dashboard
</a>
{% else %}
<a href="{{ url_for('login') if url_for else '/login' }}"
<a href="{{ url_for('auth.login') if url_for else '/login' }}"
class="btn-primary group">
<svg class="w-6 h-6 mr-3 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
@ -711,7 +711,7 @@
Zum Dashboard
</a>
{% else %}
<a href="{{ url_for('login') if url_for else '/login' }}"
<a href="{{ url_for('auth.login') if url_for else '/login' }}"
class="btn-primary group">
<svg class="w-6 h-6 mr-3 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>

View File

@ -21,7 +21,7 @@
<!-- Job Creation Form -->
<div class="dashboard-card p-8">
<form id="newJobForm" action="{{ url_for('create_job') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
<form id="newJobForm" action="{{ url_for('jobs.create_job') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">

View File

@ -459,7 +459,7 @@
<i class="fas fa-chart-line mr-2"></i>
Dashboard
</a>
<a href="{{ url_for('user_settings') }}" class="inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors">
<a href="{{ url_for('users.user_settings') }}" class="inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors">
<i class="fas fa-cog mr-2"></i>
Einstellungen
</a>

View File

@ -285,7 +285,7 @@
<!-- Login Form -->
<div class="mercedes-card p-8">
<form id="loginForm" method="POST" action="{{ url_for('login') if url_for else '#' }}" class="space-y-6">
<form id="loginForm" method="POST" action="{{ url_for('auth.login') if url_for else '#' }}" class="space-y-6">
<!-- CSRF Token -->
{% if form %}
{{ form.hidden_tag() }}
@ -382,7 +382,7 @@
</div>
<div class="text-sm">
<a href="{{ url_for('reset_password_request') if url_for and url_for('reset_password_request') else '#' }}"
<a href="{{ url_for('auth.reset_password_request') if url_for and url_for('auth.reset_password_request') else '#' }}"
class="text-mercedes-blue hover:text-blue-700 transition-colors">
Passwort vergessen?
</a>

View File

@ -0,0 +1,464 @@
{% extends "base.html" %}
{% block title %}
Tapo-Steckdosen-Steuerung | MYP Platform
{% endblock %}
{% block page_heading %}
<div class="flex items-center space-x-4">
<div class="bg-gradient-to-br from-orange-500 to-red-600 p-3 rounded-xl shadow-lg">
<i class="fas fa-plug text-white text-2xl"></i>
</div>
<div>
<h1 class="text-3xl font-bold text-slate-800 dark:text-white">
Tapo-Steckdosen-Steuerung
</h1>
<p class="text-slate-600 dark:text-slate-300">
Direkte Kontrolle aller TP-Link Tapo-Steckdosen
</p>
</div>
</div>
{% endblock %}
{% block page_actions %}
<div class="flex flex-wrap gap-3">
<button onclick="refreshAllStatus()"
class="btn-secondary flex items-center space-x-2">
<i class="fas fa-sync-alt"></i>
<span>Status aktualisieren</span>
</button>
{% if current_user.is_authenticated and current_user.has_permission('ADMIN') %}
<button onclick="discoverOutlets()"
class="btn-primary flex items-center space-x-2">
<i class="fas fa-search"></i>
<span>Steckdosen suchen</span>
</button>
<a href="{{ url_for('tapo.manual_control') }}"
class="btn-outline flex items-center space-x-2">
<i class="fas fa-tools"></i>
<span>Manuelle Steuerung</span>
</a>
{% endif %}
</div>
{% endblock %}
{% block content %}
<!-- Statistik-Übersicht -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="card">
<div class="flex items-center space-x-4">
<div class="bg-blue-100 dark:bg-blue-900 p-3 rounded-lg">
<i class="fas fa-plug text-blue-600 dark:text-blue-300 text-xl"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-slate-700 dark:text-slate-300">
Gesamt
</h3>
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="total-count">
{{ total_outlets }}
</p>
</div>
</div>
</div>
<div class="card">
<div class="flex items-center space-x-4">
<div class="bg-green-100 dark:bg-green-900 p-3 rounded-lg">
<i class="fas fa-wifi text-green-600 dark:text-green-300 text-xl"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-slate-700 dark:text-slate-300">
Online
</h3>
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="online-count">
{{ online_outlets }}
</p>
</div>
</div>
</div>
<div class="card">
<div class="flex items-center space-x-4">
<div class="bg-orange-100 dark:bg-orange-900 p-3 rounded-lg">
<i class="fas fa-bolt text-orange-600 dark:text-orange-300 text-xl"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-slate-700 dark:text-slate-300">
Aktive
</h3>
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="active-count">
0
</p>
</div>
</div>
</div>
</div>
<!-- Steckdosen-Grid -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold text-slate-800 dark:text-white flex items-center">
<i class="fas fa-list mr-3"></i>
Alle Tapo-Steckdosen
</h2>
</div>
<div class="card-body">
{% if outlets %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="outlets-grid">
{% for ip, outlet in outlets.items() %}
<div class="outlet-card border rounded-lg p-4 {{ 'border-green-300 bg-green-50 dark:bg-green-900/20' if outlet.reachable else 'border-red-300 bg-red-50 dark:bg-red-900/20' }}"
data-ip="{{ ip }}">
<!-- Steckdosen-Header -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-3">
<div class="status-indicator w-4 h-4 rounded-full {{ 'bg-green-500' if outlet.reachable else 'bg-red-500' }}"
data-ip="{{ ip }}"></div>
<div>
<h3 class="font-semibold text-slate-800 dark:text-white">
{{ outlet.printer_name }}
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400">
{{ ip }}
</p>
</div>
</div>
<div class="flex items-center space-x-2">
<button onclick="refreshOutletStatus('{{ ip }}')"
class="p-2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors"
title="Status aktualisieren">
<i class="fas fa-sync-alt text-sm"></i>
</button>
<button onclick="testConnection('{{ ip }}')"
class="p-2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors"
title="Verbindung testen">
<i class="fas fa-network-wired text-sm"></i>
</button>
</div>
</div>
<!-- Status-Info -->
<div class="mb-4">
<div class="flex items-center justify-between">
<span class="text-sm text-slate-600 dark:text-slate-400">Status:</span>
<span class="status-text font-medium" data-ip="{{ ip }}">
{% if outlet.reachable %}
{% if outlet.status == 'on' %}
<span class="text-green-600 dark:text-green-400">
<i class="fas fa-power-off mr-1"></i>EIN
</span>
{% elif outlet.status == 'off' %}
<span class="text-slate-600 dark:text-slate-400">
<i class="fas fa-power-off mr-1"></i>AUS
</span>
{% else %}
<span class="text-yellow-600 dark:text-yellow-400">
<i class="fas fa-question mr-1"></i>UNBEKANNT
</span>
{% endif %}
{% else %}
<span class="text-red-600 dark:text-red-400">
<i class="fas fa-exclamation-triangle mr-1"></i>OFFLINE
</span>
{% endif %}
</span>
</div>
<div class="flex items-center justify-between mt-2">
<span class="text-sm text-slate-600 dark:text-slate-400">Standort:</span>
<span class="text-sm font-medium text-slate-800 dark:text-white">
{{ outlet.location }}
</span>
</div>
</div>
<!-- Steuerungsbuttons -->
<div class="flex space-x-3">
<button onclick="controlOutlet('{{ ip }}', 'on')"
class="flex-1 py-2 px-3 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium text-sm"
{% if not outlet.reachable %}disabled{% endif %}>
<i class="fas fa-power-off mr-1"></i>
EIN
</button>
<button onclick="controlOutlet('{{ ip }}', 'off')"
class="flex-1 py-2 px-3 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-colors font-medium text-sm"
{% if not outlet.reachable %}disabled{% endif %}>
<i class="fas fa-power-off mr-1"></i>
AUS
</button>
</div>
{% if outlet.error %}
<div class="mt-3 p-2 bg-red-100 dark:bg-red-900/30 border border-red-300 dark:border-red-700 rounded text-sm text-red-700 dark:text-red-300">
<i class="fas fa-exclamation-triangle mr-1"></i>
{{ outlet.error }}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-12">
<div class="text-slate-400 dark:text-slate-500 mb-4">
<i class="fas fa-plug text-6xl"></i>
</div>
<h3 class="text-xl font-semibold text-slate-600 dark:text-slate-400 mb-2">
Keine Tapo-Steckdosen konfiguriert
</h3>
<p class="text-slate-500 dark:text-slate-400 mb-6">
Es wurden noch keine Drucker mit Tapo-Steckdosen eingerichtet.
</p>
{% if current_user.is_authenticated and current_user.has_permission('ADMIN') %}
<div class="flex justify-center space-x-4">
<button onclick="discoverOutlets()"
class="btn-primary">
<i class="fas fa-search mr-2"></i>
Steckdosen suchen
</button>
<a href="{{ url_for('admin.manage_printers') }}"
class="btn-secondary">
<i class="fas fa-plus mr-2"></i>
Drucker hinzufügen
</a>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-lg">
<div class="flex items-center space-x-3">
<i class="fas fa-spinner fa-spin text-blue-600 text-xl"></i>
<span class="text-slate-800 dark:text-white font-medium" id="loading-text">
Lädt...
</span>
</div>
</div>
</div>
<script>
// Globale Variablen
let outlets = {{ outlets | tojson }};
// Utility-Funktionen
function showLoading(text = 'Lädt...') {
document.getElementById('loading-text').textContent = text;
document.getElementById('loading-overlay').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
function updateOutletDisplay(ip, data) {
const card = document.querySelector(`[data-ip="${ip}"]`);
if (!card) return;
const statusIndicator = card.querySelector('.status-indicator');
const statusText = card.querySelector('.status-text');
const buttons = card.querySelectorAll('button[onclick*="controlOutlet"]');
// Status-Indikator aktualisieren
statusIndicator.className = `status-indicator w-4 h-4 rounded-full ${data.reachable ? 'bg-green-500' : 'bg-red-500'}`;
// Status-Text aktualisieren
if (data.reachable) {
let statusIcon, statusColor;
if (data.status === 'on') {
statusIcon = 'fas fa-power-off';
statusColor = 'text-green-600 dark:text-green-400';
statusText.innerHTML = `<span class="${statusColor}"><i class="${statusIcon} mr-1"></i>EIN</span>`;
} else if (data.status === 'off') {
statusIcon = 'fas fa-power-off';
statusColor = 'text-slate-600 dark:text-slate-400';
statusText.innerHTML = `<span class="${statusColor}"><i class="${statusIcon} mr-1"></i>AUS</span>`;
} else {
statusIcon = 'fas fa-question';
statusColor = 'text-yellow-600 dark:text-yellow-400';
statusText.innerHTML = `<span class="${statusColor}"><i class="${statusIcon} mr-1"></i>UNBEKANNT</span>`;
}
// Buttons aktivieren
buttons.forEach(btn => btn.disabled = false);
} else {
statusText.innerHTML = '<span class="text-red-600 dark:text-red-400"><i class="fas fa-exclamation-triangle mr-1"></i>OFFLINE</span>';
// Buttons deaktivieren
buttons.forEach(btn => btn.disabled = true);
}
// Kartenrand aktualisieren
card.className = card.className.replace(/border-\w+-300|bg-\w+-50|dark:bg-\w+-900\/20/g, '');
if (data.reachable) {
card.className += ' border-green-300 bg-green-50 dark:bg-green-900/20';
} else {
card.className += ' border-red-300 bg-red-50 dark:bg-red-900/20';
}
}
function updateStatistics() {
const totalCount = Object.keys(outlets).length;
const onlineCount = Object.values(outlets).filter(o => o.reachable).length;
const activeCount = Object.values(outlets).filter(o => o.status === 'on').length;
document.getElementById('total-count').textContent = totalCount;
document.getElementById('online-count').textContent = onlineCount;
document.getElementById('active-count').textContent = activeCount;
}
// API-Funktionen
async function controlOutlet(ip, action) {
try {
showLoading(`Steckdose wird ${action === 'on' ? 'eingeschaltet' : 'ausgeschaltet'}...`);
const response = await fetch('/tapo/control', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ ip, action })
});
const data = await response.json();
if (data.success) {
showFlashMessage(data.message, 'success');
// Status in lokalen Daten aktualisieren
if (outlets[ip]) {
outlets[ip].status = action;
outlets[ip].reachable = true;
updateOutletDisplay(ip, outlets[ip]);
updateStatistics();
}
} else {
showFlashMessage(data.error, 'error');
}
} catch (error) {
showFlashMessage('Fehler bei der Steckdosen-Steuerung: ' + error.message, 'error');
} finally {
hideLoading();
}
}
async function refreshOutletStatus(ip) {
try {
const response = await fetch(`/tapo/status/${ip}`);
const data = await response.json();
if (data.success) {
if (outlets[ip]) {
outlets[ip].status = data.status;
outlets[ip].reachable = data.reachable;
updateOutletDisplay(ip, outlets[ip]);
updateStatistics();
}
} else {
showFlashMessage(`Fehler beim Status-Check für ${ip}: ${data.error}`, 'error');
}
} catch (error) {
showFlashMessage('Fehler beim Status-Check: ' + error.message, 'error');
}
}
async function refreshAllStatus() {
showLoading('Status aller Steckdosen wird aktualisiert...');
try {
const response = await fetch('/tapo/all-status');
const data = await response.json();
if (data.success) {
// Lokale Daten aktualisieren
for (const [ip, status] of Object.entries(data.outlets)) {
if (outlets[ip]) {
outlets[ip].status = status.status;
outlets[ip].reachable = status.reachable;
updateOutletDisplay(ip, outlets[ip]);
}
}
updateStatistics();
showFlashMessage('Status aller Steckdosen aktualisiert', 'success');
} else {
showFlashMessage('Fehler beim Aktualisieren: ' + data.error, 'error');
}
} catch (error) {
showFlashMessage('Fehler beim Status-Update: ' + error.message, 'error');
} finally {
hideLoading();
}
}
async function testConnection(ip) {
try {
showLoading(`Verbindung zu ${ip} wird getestet...`);
const response = await fetch(`/tapo/test/${ip}`, { method: 'POST' });
const data = await response.json();
if (data.success) {
const testResult = data.test_result;
if (testResult.success) {
showFlashMessage(`✅ Verbindung zu ${ip} erfolgreich!`, 'success');
} else {
showFlashMessage(`❌ Verbindung zu ${ip} fehlgeschlagen: ${testResult.error}`, 'error');
}
} else {
showFlashMessage(`Verbindungstest fehlgeschlagen: ${data.error}`, 'error');
}
} catch (error) {
showFlashMessage('Fehler beim Verbindungstest: ' + error.message, 'error');
} finally {
hideLoading();
}
}
async function discoverOutlets() {
try {
showLoading('Suche nach neuen Tapo-Steckdosen...');
const response = await fetch('/tapo/discover', { method: 'POST' });
const data = await response.json();
if (data.success) {
showFlashMessage(data.message, 'success');
// Seite neu laden wenn neue Steckdosen gefunden wurden
if (data.discovered_count > 0) {
setTimeout(() => {
window.location.reload();
}, 2000);
}
} else {
showFlashMessage('Fehler bei der Erkennung: ' + data.error, 'error');
}
} catch (error) {
showFlashMessage('Fehler bei der Steckdosen-Suche: ' + error.message, 'error');
} finally {
hideLoading();
}
}
// Auto-Refresh alle 30 Sekunden
setInterval(() => {
if (Object.keys(outlets).length > 0) {
refreshAllStatus();
}
}, 30000);
// Initial statistics update
document.addEventListener('DOMContentLoaded', function() {
updateStatistics();
});
</script>
{% endblock %}

View File

@ -0,0 +1,365 @@
{% extends "base.html" %}
{% block title %}
Manuelle Tapo-Steuerung | MYP Platform
{% endblock %}
{% block page_heading %}
<div class="flex items-center space-x-4">
<div class="bg-gradient-to-br from-red-500 to-orange-600 p-3 rounded-xl shadow-lg">
<i class="fas fa-tools text-white text-2xl"></i>
</div>
<div>
<h1 class="text-3xl font-bold text-slate-800 dark:text-white">
Manuelle Tapo-Steuerung
</h1>
<p class="text-slate-600 dark:text-slate-300">
Direkte Kontrolle beliebiger Tapo-Steckdosen (Admin-Bereich)
</p>
</div>
</div>
{% endblock %}
{% block page_actions %}
<div class="flex flex-wrap gap-3">
<a href="{{ url_for('tapo.tapo_dashboard') }}"
class="btn-secondary flex items-center space-x-2">
<i class="fas fa-arrow-left"></i>
<span>Zurück zur Übersicht</span>
</a>
</div>
{% endblock %}
{% block content %}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Manuelle Steuerung -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold text-slate-800 dark:text-white flex items-center">
<i class="fas fa-hand-paper mr-3"></i>
Manuelle Steuerung
</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('tapo.manual_control') }}">
<div class="space-y-6">
<!-- IP-Adresse -->
<div>
<label for="ip" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
<i class="fas fa-network-wired mr-2"></i>IP-Adresse *
</label>
<input type="text"
id="ip"
name="ip"
required
pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
placeholder="192.168.1.100">
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
IP-Adresse der Tapo-Steckdose eingeben
</p>
</div>
<!-- Aktion auswählen -->
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">
<i class="fas fa-cog mr-2"></i>Aktion auswählen *
</label>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<label class="relative">
<input type="radio" name="action" value="on" class="sr-only peer" required>
<div class="p-4 border-2 border-slate-300 dark:border-slate-600 rounded-lg cursor-pointer peer-checked:border-green-500 peer-checked:bg-green-50 dark:peer-checked:bg-green-900/20 transition-all">
<div class="text-center">
<i class="fas fa-power-off text-green-600 text-2xl mb-2"></i>
<div class="font-medium text-slate-800 dark:text-white">Einschalten</div>
</div>
</div>
</label>
<label class="relative">
<input type="radio" name="action" value="off" class="sr-only peer">
<div class="p-4 border-2 border-slate-300 dark:border-slate-600 rounded-lg cursor-pointer peer-checked:border-slate-500 peer-checked:bg-slate-50 dark:peer-checked:bg-slate-900/20 transition-all">
<div class="text-center">
<i class="fas fa-power-off text-slate-600 text-2xl mb-2"></i>
<div class="font-medium text-slate-800 dark:text-white">Ausschalten</div>
</div>
</div>
</label>
<label class="relative">
<input type="radio" name="action" value="status" class="sr-only peer">
<div class="p-4 border-2 border-slate-300 dark:border-slate-600 rounded-lg cursor-pointer peer-checked:border-blue-500 peer-checked:bg-blue-50 dark:peer-checked:bg-blue-900/20 transition-all">
<div class="text-center">
<i class="fas fa-info-circle text-blue-600 text-2xl mb-2"></i>
<div class="font-medium text-slate-800 dark:text-white">Status prüfen</div>
</div>
</div>
</label>
</div>
</div>
<!-- Submit Button -->
<div class="pt-4">
<button type="submit" class="w-full btn-primary">
<i class="fas fa-play mr-2"></i>
Aktion ausführen
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold text-slate-800 dark:text-white flex items-center">
<i class="fas fa-bolt mr-3"></i>
Schnellaktionen
</h2>
</div>
<div class="card-body">
<div class="space-y-4">
<!-- Alle ausschalten -->
<div class="p-4 border border-orange-300 dark:border-orange-700 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<div class="flex items-center justify-between">
<div>
<h3 class="font-medium text-slate-800 dark:text-white">
<i class="fas fa-power-off mr-2 text-orange-600"></i>
Alle Steckdosen ausschalten
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
Schaltet alle konfigurierten Tapo-Steckdosen aus
</p>
</div>
<button onclick="emergencyShutdown()"
class="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded-lg transition-colors">
<i class="fas fa-power-off mr-1"></i>
Alle AUS
</button>
</div>
</div>
<!-- Verbindung testen -->
<div class="p-4 border border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div class="flex items-center justify-between">
<div>
<h3 class="font-medium text-slate-800 dark:text-white">
<i class="fas fa-network-wired mr-2 text-blue-600"></i>
Verbindung testen
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
Testet die IP-Adresse im Eingabefeld
</p>
</div>
<button onclick="testManualConnection()"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
<i class="fas fa-search mr-1"></i>
Testen
</button>
</div>
</div>
<!-- Status aller prüfen -->
<div class="p-4 border border-green-300 dark:border-green-700 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div class="flex items-center justify-between">
<div>
<h3 class="font-medium text-slate-800 dark:text-white">
<i class="fas fa-sync-alt mr-2 text-green-600"></i>
Status aller prüfen
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
Aktualisiert den Status aller Steckdosen
</p>
</div>
<button onclick="refreshAllOutlets()"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
<i class="fas fa-sync-alt mr-1"></i>
Aktualisieren
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Warnung Box -->
<div class="card mt-8">
<div class="card-body">
<div class="flex items-start space-x-4 p-4 border border-yellow-400 dark:border-yellow-600 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<div class="flex-shrink-0">
<i class="fas fa-exclamation-triangle text-yellow-600 dark:text-yellow-400 text-xl"></i>
</div>
<div>
<h3 class="font-medium text-yellow-800 dark:text-yellow-200 mb-2">
Wichtige Hinweise zur manuellen Steuerung
</h3>
<ul class="text-sm text-yellow-700 dark:text-yellow-300 space-y-1 list-disc list-inside">
<li>Diese Funktion ist nur für Administratoren verfügbar</li>
<li>IP-Adressen müssen gültig und erreichbar sein</li>
<li>Steckdosen müssen mit den globalen Tapo-Anmeldedaten konfiguriert sein</li>
<li>Alle Aktionen werden protokolliert</li>
<li>Bei Problemen immer erst die Verbindung testen</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-lg">
<div class="flex items-center space-x-3">
<i class="fas fa-spinner fa-spin text-blue-600 text-xl"></i>
<span class="text-slate-800 dark:text-white font-medium" id="loading-text">
Lädt...
</span>
</div>
</div>
</div>
<script>
// Utility-Funktionen
function showLoading(text = 'Lädt...') {
document.getElementById('loading-text').textContent = text;
document.getElementById('loading-overlay').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
// Schnellaktionen
async function emergencyShutdown() {
if (!confirm('Möchten Sie wirklich ALLE Tapo-Steckdosen ausschalten?\n\nDies kann alle angeschlossenen Geräte abschalten!')) {
return;
}
try {
showLoading('Schalte alle Steckdosen aus...');
const response = await fetch('/tapo/all-status');
const data = await response.json();
if (!data.success) {
throw new Error(data.error);
}
let successCount = 0;
let errorCount = 0;
// Alle verfügbaren Steckdosen ausschalten
for (const [ip, status] of Object.entries(data.outlets)) {
if (status.reachable) {
try {
const controlResponse = await fetch('/tapo/control', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ ip, action: 'off' })
});
const controlData = await controlResponse.json();
if (controlData.success) {
successCount++;
} else {
errorCount++;
}
} catch (error) {
errorCount++;
}
}
}
if (successCount > 0) {
showFlashMessage(`${successCount} Steckdosen erfolgreich ausgeschaltet${errorCount > 0 ? `, ${errorCount} Fehler` : ''}`, 'success');
} else {
showFlashMessage('❌ Keine Steckdosen konnten ausgeschaltet werden', 'error');
}
} catch (error) {
showFlashMessage('Fehler beim Notaus: ' + error.message, 'error');
} finally {
hideLoading();
}
}
async function testManualConnection() {
const ipInput = document.getElementById('ip');
const ip = ipInput.value.trim();
if (!ip) {
showFlashMessage('Bitte geben Sie eine IP-Adresse ein', 'error');
ipInput.focus();
return;
}
// IP-Validation
if (!/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ip)) {
showFlashMessage('Ungültige IP-Adresse', 'error');
ipInput.focus();
return;
}
try {
showLoading(`Teste Verbindung zu ${ip}...`);
const response = await fetch(`/tapo/test/${ip}`, { method: 'POST' });
const data = await response.json();
if (data.success) {
const testResult = data.test_result;
if (testResult.success) {
showFlashMessage(`✅ Verbindung zu ${ip} erfolgreich!`, 'success');
} else {
showFlashMessage(`❌ Verbindung zu ${ip} fehlgeschlagen: ${testResult.error}`, 'error');
}
} else {
showFlashMessage(`Verbindungstest fehlgeschlagen: ${data.error}`, 'error');
}
} catch (error) {
showFlashMessage('Fehler beim Verbindungstest: ' + error.message, 'error');
} finally {
hideLoading();
}
}
async function refreshAllOutlets() {
try {
showLoading('Aktualisiere Status aller Steckdosen...');
const response = await fetch('/tapo/all-status');
const data = await response.json();
if (data.success) {
showFlashMessage(`✅ Status von ${data.count} Steckdosen aktualisiert`, 'success');
} else {
showFlashMessage('Fehler beim Status-Update: ' + data.error, 'error');
}
} catch (error) {
showFlashMessage('Fehler beim Status-Update: ' + error.message, 'error');
} finally {
hideLoading();
}
}
// IP-Eingabe Auto-Format
document.getElementById('ip').addEventListener('input', function(e) {
let value = e.target.value.replace(/[^0-9.]/g, '');
e.target.value = value;
});
// Enter-Taste für Test
document.getElementById('ip').addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
testManualConnection();
}
});
</script>
{% endblock %}