🎉 Added new files for data collection and documentation 📚, updated error resilience script, and refactored admin schedule template. #123

This commit is contained in:
Till Tomczak 2025-06-02 15:10:39 +02:00
parent 6ff407a895
commit c29ef2c075
4 changed files with 6002 additions and 101 deletions

4397
DATENSAMMLUNG.html Normal file

File diff suppressed because one or more lines are too long

196
IHK_DOKUMENTATION.md Normal file
View File

@ -0,0 +1,196 @@
# MYP Manage Your Printer
**Digitalisierung des 3D-Drucker-Reservierungsprozesses durch Etablierung der cyberphysischen Kommunikation mit relevanten Hardwarekomponenten**
**Abschlussprüfung Sommer 2025**
**Fachinformatiker für digitale Vernetzung**
**Abgabedatum: 5. Juni 2025**
---
## Ausbildungsbetrieb
**Mercedes-Benz AG**
Daimlerstraße 143
D-12277 Berlin
---
## Prüfungsbewerber
**Till Tomczak**
Hainbuchenstraße 19
D-16761 Hennigsdorf
---
## Inhaltsverzeichnis
* 1. Einleitung 3
* 1.1 Analyse des Projektauftrag 3
* 1.2 Ableitung der Projektziele 3
* 1.3 Projektabgrenzung 3
* 1.4 Projektumfeld 3
* 1.5 Betriebliche Schnittstellen 3
* 1.6 Analyse der IT-sicherheitsrelevante Bedingungen 3
* 1.7 Darstellung der vorhandenen Systemarchitektur 3
* 2. Projektplanung 3
* 2.1 Terminplanung 3
* 2.2 Ressourcenplanung 3
* 2.3 Planung der Qualitätssicherung 3
* 2.4 Bewertung der heterogenen IT-Landschaft 3
* 2.5 Anforderungsgerechte Auswahl der Übertragungssysteme 4
* 2.6 Planung der Prozess-/ und Systemschnittstellen 4
* 2.7 Planung der IT-Sicherheitsmaßnahmen 4
* 3. Durchführung und Auftragsbearbeitung 4
* 3.1 Prozess-Schritte und Vorgehensweise 4
* * 3.1.1 Datenabfrage der Sensoren 4
* * 3.1.2 Verarbeiten der Daten 4
* 3.2 Abweichung, Anpassung und Entscheidungen 4
* 3.3 Maßnahmen zur Qualitätskontrolle 4
* 3.4 Implementierung, Konfiguration und Inbetriebnahme von Schnittstellen und unterschiedlicher Prozesse und Systeme 4
* 3.5 Konfiguration von Übertragungssystemen und Integration in die Gesamtinfrastruktur 4
* 3.6 Erfüllen der Anforderungen an die Informationssicherheit 4
* 4. Projektabschluss 4
* 4.1 Soll-Ist-Vergleich (Abweichung, Anpassungen) 4
* 4.2 Fazit 4
* 4.3 Optimierungsmöglichkeiten 5
* 4.4 Abnahme 5
---
# 1 Einleitung
MYP (Manage Your Printer) entstand, weil mein Ausbilder an der Technischen Berufsausbildungsstätte (TBA) dringend eine Lösung für die bis dato de facto nicht vorhandenen Reservierungsprozesse der 3D-Drucker brauchte; ein früheres Frontend-Gerüst eines anderen Azubis war lediglich ein Prototyp und Proof of Concept ohne Backend-Anbindung oder produktionstaugliche Vernetzung, sodass es im Praxisbetrieb keine verlässliche Basis bot. Zudem wurde noch keine Hardware aufgebaut, sodass die Kollegen das tatsächlich hätten nutzen können. MYP schließt diese Lücke mit einem Flask-Backend, einer leichten SQLite-Datenbank und einer PWA-fähigen Oberfläche, die auch ohne Internet funktioniert ein Offline-Betrieb, der in der industriellen Umgebung der TBA aus Sicherheitsgründen zwingend notwendig ist. Über TP-Link-Tapo-P110-Smart-Plugs regelt das System ausschließlich die Stromzufuhr der Drucker und bleibt damit herstellerunabhängig. Ein klares Rollenmodell trennt Administrierende, die Drucker und Nutzer anlegen, von den Benutzerinnen und Benutzern, die einfach ihre Zeitfenster buchen und ihre Druckjobs verwalten. Sobald eine Reservierung aktiv wird, schaltet MYP den betreffenden Drucker automatisch ein und nach Ablauf wieder aus; gleichzeitig protokolliert es sämtliche Laufzeiten, um präzise Statistiken über Auslastung und Gesamtdruckdauer zu liefern. Ein dedizierter Kiosk-Modus auf einem Raspberry Pi zeigt auf einem Monitor neben den Geräten im Vollbild aktuelle Belegungen und den Systemstatus an und fügt sich so nahtlos in den Produktionsalltag der TBA ein.
## 1.1 Analyse des Projektauftrag
Ziel war die Automatisierung des 3D-Drucker-Reservierungssystems mittels vernetzter Steckdosen und einer zentralen Verwaltungsplattform
## 1.2 Ableitung der Projektziele
Die Projektziele beinhalteten unter anderem die automatische Schaltung über Smart-Plugs, eine Offline-fähige Lösung und eine rollenbasierte Benutzerverwaltung
## 1.3 Projektabgrenzung
Nicht Bestandteil des Projekts war die direkte Steuerung oder Überwachung der Druckvorgänge selbst
## 1.4 Projektumfeld
Das Projekt wurde bei Mercedes-Benz AG, Werk Berlin, im Bereich Ausbildung Digitale Vernetzung durchgeführt
## 1.5 Betriebliche Schnittstellen
Schnittstellen bestanden zur IT-Abteilung (Netzwerkintegration), Ausbildungsleitung (Abnahme) und ggf. zu Endnutzern (Feedback zur Oberfläche)
## 1.6 Analyse der IT-sicherheitsrelevante Bedingungen
Da das System im Intranet betrieben wird, galten besondere Anforderungen an Datenschutz und sichere Authentifizierung
## 1.7 Darstellung der vorhandenen Systemarchitektur
Siehe Abbildung im Kapitel Infrastruktur
---
# 2 Projektplanung
Die Projektplanung erfolgte auf Basis des betrieblichen Ablaufs und der gegebenen Ressourcen
## 2.1 Terminplanung
Die Umsetzung erfolgte innerhalb von ca. 5 Wochen mit klar definierten Meilensteinen
## 2.2 Ressourcenplanung
Zur Verfügung standen ein Raspberry Pi 4, sechs TP-Link P110 Steckdosen und eine lokale Testumgebung
## 2.3 Planung der Qualitätssicherung
Geplant war eine manuelle Funktionsprüfung, End-to-End Tests und Dokumentation der Ergebnisse
## 2.4 Bewertung der heterogenen IT-Landschaft
Die Integration erfolgte in ein bestehendes Firmennetz mit segmentiertem WLAN für IoT-Geräte
## 2.5 Anforderungsgerechte Auswahl der Übertragungssysteme
Es wurde WLAN genutzt; die Steckdosen arbeiten auf Basis TCP/IP mit herstellerspezifischer API
## 2.6 Planung der Prozess-/ und Systemschnittstellen
Schnittstellen bestanden zwischen Flask-Backend, SQLite-Datenbank und REST-API zum Frontend
## 2.7 Planung der IT-Sicherheitsmaßnahmen
Passwort-Hashing, Rollenverwaltung und lokal beschränkter Netzwerkzugriff wurden umgesetzt
---
# 3 Durchführung und Auftragsbearbeitung
In diesem Kapitel wird die konkrete Umsetzung beschrieben
## 3.1 Prozess-Schritte und Vorgehensweise
Installation der Hardware, Entwicklung des Backends, API-Anbindung, Testphase
### 3.1.1 Datenabfrage der Sensoren
Tapo Steckdosen P110 (Scheduler
### 3.1.2 Verarbeiten der Daten
Reservierungsdaten wurden verarbeitet, gespeichert und in Schaltbefehle umgewandelt
## 3.2 Abweichung, Anpassung und Entscheidungen
Ein zusätzliches Offline-Frontend wurde notwendig, da die geplante Integration ins Intranet scheiterte
## 3.3 Maßnahmen zur Qualitätskontrolle
Funktionsüberprüfung jeder API-Ressource sowie Live-Tests mit den Steckdosen
## 3.4 Implementierung, Konfiguration und Inbetriebnahme von Schnittstellen und unterschiedlicher Prozesse und Systeme
REST-API, Flask, systemd-Service, Autostart im Kiosk-Modus
## 3.5 Konfiguration von Übertragungssystemen und Integration in die Gesamtinfrastruktur
Pi wurde mit fester IP in das Firmennetz eingebunden, WLAN isoliert konfiguriert
## 3.6 Erfüllen der Anforderungen an die Informationssicherheit
Zugriffsrechte, Authentifizierung, selbstsigniertes SSL-Zertifikat
---
# 4 Projektabschluss
Dieses Kapitel beschreibt das Ergebnis, die Bewertung sowie das Fazit
## 4.1 Soll-Ist-Vergleich (Abweichung, Anpassungen)
Die Hauptziele wurden erreicht, jedoch wurde eine temporäre Notlösung für das Frontend notwendig
## 4.2 Fazit
Das Projekt war erfolgreich, der Nutzen für den Ausbildungsbetrieb konnte nachgewiesen werden
## 4.3 Optimierungsmöglichkeiten
Echtzeitdaten, Druckerkommunikation, AD-Integration
## 4.4 Abnahme
System wurde erfolgreich durch Ausbilder abgenommen und in einer Testumgebung demonstriert

File diff suppressed because it is too large Load Diff

View File

@ -12,38 +12,147 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style> <style>
.calendar-container { /* ===== DARK MODE CALENDAR OPTIMIERUNG ===== */
background: white; .fc {
border-radius: 12px; background: transparent;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
padding: 20px;
margin-bottom: 20px; .fc-theme-standard .fc-view-harness {
background: transparent;
}
.fc-theme-standard .fc-scrollgrid {
border-color: rgb(148 163 184 / 0.3);
}
.dark .fc-theme-standard .fc-scrollgrid {
border-color: rgb(71 85 105 / 0.4);
}
.fc-theme-standard td,
.fc-theme-standard th {
border-color: rgb(148 163 184 / 0.2);
}
.dark .fc-theme-standard td,
.dark .fc-theme-standard th {
border-color: rgb(71 85 105 / 0.3);
}
.fc-col-header-cell {
background: rgb(248 250 252);
color: rgb(51 65 85);
}
.dark .fc-col-header-cell {
background: rgb(15 23 42);
color: rgb(203 213 225);
}
.fc-daygrid-day {
background: transparent;
}
.fc-day-today {
background: rgb(59 130 246 / 0.1) !important;
}
.dark .fc-day-today {
background: rgb(59 130 246 / 0.2) !important;
}
.fc-button-primary {
background: rgb(59 130 246);
border-color: rgb(59 130 246);
color: white;
}
.fc-button-primary:hover {
background: rgb(37 99 235);
border-color: rgb(37 99 235);
}
.fc-button-primary:disabled {
background: rgb(148 163 184);
border-color: rgb(148 163 184);
}
.dark .fc-button-primary:disabled {
background: rgb(71 85 105);
border-color: rgb(71 85 105);
} }
.fc-event { .fc-event {
font-size: 11px; font-size: 11px;
border-radius: 4px; border-radius: 4px;
padding: 2px 4px; padding: 2px 4px;
}
.fc-event-title {
font-weight: 500; font-weight: 500;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
.dark .fc-event {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.fc-h-event {
border: none !important;
}
/* ===== KALENDER CONTAINER ===== */
.calendar-container {
background: rgb(255 255 255);
border: 1px solid rgb(226 232 240);
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
padding: 20px;
margin-bottom: 20px;
backdrop-filter: blur(12px);
}
.dark .calendar-container {
background: rgb(15 23 42);
border-color: rgb(51 65 85);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.15);
}
/* ===== STATISTIK KARTEN ===== */
.stats-card { .stats-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, rgb(59 130 246) 0%, rgb(99 102 241) 100%);
border-radius: 12px; border-radius: 12px;
padding: 20px; padding: 20px;
color: white; color: white;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
} }
.stats-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px -8px rgba(59, 130, 246, 0.3);
}
.dark .stats-card {
background: linear-gradient(135deg, rgb(37 99 235) 0%, rgb(79 70 229) 100%);
border-color: rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.25);
}
/* ===== KONTROLLPANEL ===== */
.control-panel { .control-panel {
background: white; background: rgb(255 255 255 / 0.8);
border: 1px solid rgb(226 232 240);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px; padding: 20px;
margin-bottom: 20px; margin-bottom: 20px;
backdrop-filter: blur(12px);
}
.dark .control-panel {
background: rgb(15 23 42 / 0.8);
border-color: rgb(51 65 85);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
} }
.filter-group { .filter-group {
@ -53,24 +162,45 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
/* ===== BUTTONS ===== */
.btn-calendar { .btn-calendar {
background: #3b82f6; background: rgb(59 130 246);
color: white; color: white;
border: none; border: none;
padding: 8px 16px; padding: 8px 16px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: all 0.2s ease;
font-weight: 500;
border: 1px solid transparent;
} }
.btn-calendar:hover { .btn-calendar:hover {
background: #2563eb; background: rgb(37 99 235);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
} }
.btn-calendar.active { .btn-calendar.active {
background: #1d4ed8; background: rgb(29 78 216);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
} }
.dark .btn-calendar {
background: rgb(37 99 235);
border-color: rgb(59 130 246);
}
.dark .btn-calendar:hover {
background: rgb(29 78 216);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.6);
}
.dark .btn-calendar.active {
background: rgb(30 64 175);
}
/* ===== LEGENDE ===== */
.legend { .legend {
display: flex; display: flex;
gap: 20px; gap: 20px;
@ -83,54 +213,133 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 14px; font-size: 14px;
color: rgb(71 85 105);
transition: color 0.3s ease;
}
.dark .legend-item {
color: rgb(203 213 225);
} }
.legend-color { .legend-color {
width: 16px; width: 16px;
height: 16px; height: 16px;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.dark .legend-color {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
/* ===== FORM ELEMENTS ===== */
select, input {
background: rgb(255 255 255);
border: 1px solid rgb(203 213 225);
color: rgb(51 65 85);
transition: all 0.2s ease;
}
select:focus, input:focus {
border-color: rgb(59 130 246);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
outline: none;
}
.dark select, .dark input {
background: rgb(30 41 59);
border-color: rgb(71 85 105);
color: rgb(203 213 225);
}
.dark select:focus, .dark input:focus {
border-color: rgb(99 102 241);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
/* ===== MODAL STYLING ===== */
.modal-overlay {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.dark .modal-overlay {
background: rgba(0, 0, 0, 0.7);
}
.modal-content {
background: rgb(255 255 255);
border: 1px solid rgb(226 232 240);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.dark .modal-content {
background: rgb(15 23 42);
border-color: rgb(51 65 85);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
}
/* ===== RESPONSIVE IMPROVEMENTS ===== */
@media (max-width: 768px) {
.calendar-container {
padding: 15px;
}
.control-panel {
padding: 15px;
}
.filter-group {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.legend {
gap: 15px;
}
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="min-h-screen"> <div class="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 dark:from-slate-900 dark:to-slate-800 transition-colors duration-300">
<!-- Header mit Breadcrumb --> <!-- Header mit Breadcrumb -->
<div> <div class="border-b border-slate-200 dark:border-slate-700 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6"> <div class="py-6">
<nav class="text-sm font-medium text-gray-600 mb-4"> <nav class="text-sm font-medium text-slate-600 dark:text-slate-400 mb-4">
<ol class="list-none p-0 inline-flex"> <ol class="list-none p-0 inline-flex">
<li class="flex items-center"> <li class="flex items-center">
<a href="{{ url_for('admin_page') }}" class="hover:text-blue-600">Admin-Dashboard</a> <a href="{{ url_for('admin_page') }}" 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" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"> <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"/> <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> </svg>
</li> </li>
<li class="text-gray-900 font-semibold"> <li class="text-slate-900 dark:text-white font-semibold">
Steckdosenschaltzeiten Steckdosenschaltzeiten
</li> </li>
</ol> </ol>
</nav> </nav>
<div class="flex justify-between items-center"> <div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div> <div>
<h1 class="text-3xl font-bold text-gray-900"> <h1 class="text-3xl font-bold text-slate-900 dark:text-white">
<i class="fas fa-plug text-blue-600 mr-3"></i> <i class="fas fa-plug text-blue-600 dark:text-blue-400 mr-3"></i>
Steckdosenschaltzeiten Steckdosenschaltzeiten
</h1> </h1>
<p class="mt-2 text-gray-600"> <p class="mt-2 text-slate-600 dark:text-slate-400">
Kalenderübersicht aller Drucker-Steckdosenschaltungen mit detaillierter Analyse Kalenderübersicht aller Drucker-Steckdosenschaltungen mit detaillierter Analyse
</p> </p>
</div> </div>
<div class="flex gap-3"> <div class="flex gap-3">
<button id="refreshData" class="btn-calendar"> <button id="refreshData" class="btn-calendar flex items-center">
<i class="fas fa-sync-alt mr-2"></i> <i class="fas fa-sync-alt mr-2"></i>
Aktualisieren Aktualisieren
</button> </button>
<button id="cleanupLogs" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700"> <button id="cleanupLogs" class="bg-red-600 dark:bg-red-700 text-white px-4 py-2 rounded-lg hover:bg-red-700 dark:hover:bg-red-600 transition-all duration-200 font-medium flex items-center">
<i class="fas fa-trash mr-2"></i> <i class="fas fa-trash mr-2"></i>
Alte Logs löschen Alte Logs löschen
</button> </button>
@ -141,33 +350,39 @@
</div> </div>
<!-- Haupt-Container --> <!-- Haupt-Container -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Statistik-Karten --> <!-- Statistik-Karten -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<div class="stats-card"> <div class="stats-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm opacity-80">Schaltungen (24h)</p> <p class="text-sm opacity-80 font-medium">Schaltungen (24h)</p>
<p class="text-2xl font-bold" id="totalLogs">{{ stats.total_logs or 0 }}</p> <p class="text-2xl font-bold" id="totalLogs">{{ stats.total_logs or 0 }}</p>
<p class="text-xs opacity-70 mt-1">Gesamte Aktivität</p>
</div>
<div class="text-2xl opacity-60">
<i class="fas fa-chart-line"></i>
</div> </div>
<i class="fas fa-chart-line text-2xl opacity-60"></i>
</div> </div>
</div> </div>
<div class="stats-card"> <div class="stats-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm opacity-80">Erfolgsrate</p> <p class="text-sm opacity-80 font-medium">Erfolgsrate</p>
<p class="text-2xl font-bold" id="successRate">{{ "%.1f"|format(100 - stats.error_rate) }}%</p> <p class="text-2xl font-bold" id="successRate">{{ "%.1f"|format(100 - stats.error_rate) }}%</p>
<p class="text-xs opacity-70 mt-1">Ohne Fehler</p>
</div>
<div class="text-2xl opacity-60">
<i class="fas fa-check-circle"></i>
</div> </div>
<i class="fas fa-check-circle text-2xl opacity-60"></i>
</div> </div>
</div> </div>
<div class="stats-card"> <div class="stats-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm opacity-80">Ø Antwortzeit</p> <p class="text-sm opacity-80 font-medium">Ø Antwortzeit</p>
<p class="text-2xl font-bold" id="avgResponseTime"> <p class="text-2xl font-bold" id="avgResponseTime">
{% if stats.average_response_time_ms %} {% if stats.average_response_time_ms %}
{{ "%.0f"|format(stats.average_response_time_ms) }}ms {{ "%.0f"|format(stats.average_response_time_ms) }}ms
@ -175,18 +390,24 @@
N/A N/A
{% endif %} {% endif %}
</p> </p>
<p class="text-xs opacity-70 mt-1">Durchschnitt</p>
</div>
<div class="text-2xl opacity-60">
<i class="fas fa-stopwatch"></i>
</div> </div>
<i class="fas fa-stopwatch text-2xl opacity-60"></i>
</div> </div>
</div> </div>
<div class="stats-card"> <div class="stats-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm opacity-80">Fehlerzahl</p> <p class="text-sm opacity-80 font-medium">Fehlerzahl</p>
<p class="text-2xl font-bold" id="errorCount">{{ stats.error_count or 0 }}</p> <p class="text-2xl font-bold" id="errorCount">{{ stats.error_count or 0 }}</p>
<p class="text-xs opacity-70 mt-1">Letzte 24h</p>
</div>
<div class="text-2xl opacity-60">
<i class="fas fa-exclamation-triangle"></i>
</div> </div>
<i class="fas fa-exclamation-triangle text-2xl opacity-60"></i>
</div> </div>
</div> </div>
</div> </div>
@ -194,19 +415,23 @@
<!-- Filter und Steuerung --> <!-- Filter und Steuerung -->
<div class="control-panel"> <div class="control-panel">
<div class="filter-group"> <div class="filter-group">
<label for="printerFilter" class="font-medium text-gray-700">Drucker filtern:</label> <div class="flex items-center gap-3">
<select id="printerFilter" class="border border-gray-300 rounded-lg px-3 py-2 bg-white"> <label for="printerFilter" class="font-medium text-slate-700 dark:text-slate-300 whitespace-nowrap">Drucker filtern:</label>
<option value="">Alle Drucker</option> <select id="printerFilter" class="border border-slate-300 dark:border-slate-600 rounded-lg px-3 py-2 bg-white dark:bg-slate-800 text-slate-900 dark:text-white min-w-40">
{% for printer in printers %} <option value="">Alle Drucker</option>
<option value="{{ printer.id }}">{{ printer.name }}</option> {% for printer in printers %}
{% endfor %} <option value="{{ printer.id }}">{{ printer.name }}</option>
</select> {% endfor %}
</select>
</div>
<div class="border-l border-gray-300 pl-4 ml-4"> <div class="border-l border-slate-300 dark:border-slate-600 pl-4 ml-4">
<label class="font-medium text-gray-700 mr-3">Ansicht:</label> <label class="font-medium text-slate-700 dark:text-slate-300 mr-3">Ansicht:</label>
<button id="monthView" class="btn-calendar active">Monat</button> <div class="inline-flex gap-1">
<button id="weekView" class="btn-calendar">Woche</button> <button id="monthView" class="btn-calendar active">Monat</button>
<button id="dayView" class="btn-calendar">Tag</button> <button id="weekView" class="btn-calendar">Woche</button>
<button id="dayView" class="btn-calendar">Tag</button>
</div>
</div> </div>
</div> </div>
@ -237,22 +462,22 @@
</div> </div>
<!-- Detailansicht Modal --> <!-- Detailansicht Modal -->
<div id="eventDetailModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50"> <div id="eventDetailModal" class="fixed inset-0 modal-overlay hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4"> <div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg max-w-lg w-full p-6"> <div class="modal-content rounded-lg max-w-lg w-full p-6">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Schaltung Details</h3> <h3 class="text-lg font-semibold text-slate-900 dark:text-white">Schaltung Details</h3>
<button id="closeModal" class="text-gray-400 hover:text-gray-600"> <button id="closeModal" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors">
<i class="fas fa-times"></i> <i class="fas fa-times text-lg"></i>
</button> </button>
</div> </div>
<div id="modalContent"> <div id="modalContent" class="text-slate-700 dark:text-slate-300">
<!-- Wird dynamisch gefüllt --> <!-- Wird dynamisch gefüllt -->
</div> </div>
<div class="mt-6 flex justify-end"> <div class="mt-6 flex justify-end">
<button id="closeModalBtn" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400"> <button id="closeModalBtn" class="px-4 py-2 bg-slate-300 dark:bg-slate-600 text-slate-700 dark:text-slate-200 rounded-lg hover:bg-slate-400 dark:hover:bg-slate-500 transition-colors font-medium">
Schließen Schließen
</button> </button>
</div> </div>
@ -329,75 +554,75 @@ document.addEventListener('DOMContentLoaded', function() {
const startTime = new Date(event.start).toLocaleString('de-DE'); const startTime = new Date(event.start).toLocaleString('de-DE');
let detailsHtml = ` let detailsHtml = `
<div class="space-y-3"> <div class="space-y-4">
<div class="flex items-center"> <div class="flex items-center p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
<span class="w-4 h-4 rounded mr-3" style="background-color: ${event.backgroundColor}"></span> <span class="w-4 h-4 rounded mr-3 flex-shrink-0" style="background-color: ${event.backgroundColor}"></span>
<span class="font-medium">${event.title}</span> <span class="font-medium text-slate-900 dark:text-white">${event.title}</span>
</div> </div>
<div class="grid grid-cols-2 gap-4 text-sm"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div> <div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-gray-600">Zeitpunkt:</span> <span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Zeitpunkt:</span>
<p>${startTime}</p> <p class="text-slate-900 dark:text-white">${startTime}</p>
</div> </div>
<div> <div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-gray-600">Drucker:</span> <span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Drucker:</span>
<p>${props.printer_name}</p> <p class="text-slate-900 dark:text-white">${props.printer_name}</p>
</div> </div>
<div> <div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-gray-600">Status:</span> <span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Status:</span>
<p class="capitalize">${props.status}</p> <p class="text-slate-900 dark:text-white capitalize font-medium">${props.status}</p>
</div> </div>
<div> <div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-gray-600">Quelle:</span> <span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Quelle:</span>
<p class="capitalize">${props.source}</p> <p class="text-slate-900 dark:text-white capitalize">${props.source}</p>
</div> </div>
`; `;
if (props.user_name) { if (props.user_name) {
detailsHtml += ` detailsHtml += `
<div> <div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-gray-600">Benutzer:</span> <span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Benutzer:</span>
<p>${props.user_name}</p> <p class="text-slate-900 dark:text-white">${props.user_name}</p>
</div> </div>
`; `;
} }
if (props.response_time_ms) { if (props.response_time_ms) {
detailsHtml += ` detailsHtml += `
<div> <div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-gray-600">Antwortzeit:</span> <span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Antwortzeit:</span>
<p>${props.response_time_ms}ms</p> <p class="text-slate-900 dark:text-white">${props.response_time_ms}ms</p>
</div> </div>
`; `;
} }
if (props.power_consumption) { if (props.power_consumption) {
detailsHtml += ` detailsHtml += `
<div> <div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-gray-600">Verbrauch:</span> <span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Verbrauch:</span>
<p>${props.power_consumption}W</p> <p class="text-slate-900 dark:text-white">${props.power_consumption}W</p>
</div> </div>
`; `;
} }
if (props.voltage) { if (props.voltage) {
detailsHtml += ` detailsHtml += `
<div> <div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-gray-600">Spannung:</span> <span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Spannung:</span>
<p>${props.voltage}V</p> <p class="text-slate-900 dark:text-white">${props.voltage}V</p>
</div> </div>
`; `;
} }
if (props.current) { if (props.current) {
detailsHtml += ` detailsHtml += `
<div> <div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
<span class="font-medium text-gray-600">Strom:</span> <span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Strom:</span>
<p>${props.current}A</p> <p class="text-slate-900 dark:text-white">${props.current}A</p>
</div> </div>
`; `;
} }
@ -406,18 +631,18 @@ document.addEventListener('DOMContentLoaded', function() {
if (props.notes) { if (props.notes) {
detailsHtml += ` detailsHtml += `
<div class="mt-4"> <div class="mt-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<span class="font-medium text-gray-600">Notizen:</span> <span class="font-medium text-blue-800 dark:text-blue-200 block mb-2">Notizen:</span>
<p class="text-sm bg-gray-50 p-2 rounded mt-1">${props.notes}</p> <p class="text-sm text-blue-700 dark:text-blue-300">${props.notes}</p>
</div> </div>
`; `;
} }
if (props.error_message) { if (props.error_message) {
detailsHtml += ` detailsHtml += `
<div class="mt-4"> <div class="mt-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
<span class="font-medium text-red-600">Fehlermeldung:</span> <span class="font-medium text-red-800 dark:text-red-200 block mb-2">Fehlermeldung:</span>
<p class="text-sm bg-red-50 text-red-700 p-2 rounded mt-1">${props.error_message}</p> <p class="text-sm text-red-700 dark:text-red-300">${props.error_message}</p>
</div> </div>
`; `;
} }
@ -435,6 +660,20 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('closeModal').addEventListener('click', closeModal); document.getElementById('closeModal').addEventListener('click', closeModal);
document.getElementById('closeModalBtn').addEventListener('click', closeModal); document.getElementById('closeModalBtn').addEventListener('click', closeModal);
// Escape-Taste zum Schließen des Modals
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
}
});
// Modal schließen bei Klick außerhalb
document.getElementById('eventDetailModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
// Drucker-Filter // Drucker-Filter
document.getElementById('printerFilter').addEventListener('change', function() { document.getElementById('printerFilter').addEventListener('change', function() {
currentPrinterFilter = this.value; currentPrinterFilter = this.value;
@ -466,13 +705,28 @@ document.addEventListener('DOMContentLoaded', function() {
// Aktualisieren-Button // Aktualisieren-Button
document.getElementById('refreshData').addEventListener('click', function() { document.getElementById('refreshData').addEventListener('click', function() {
calendar.refetchEvents(); const btn = this;
loadStatistics(); const icon = btn.querySelector('i');
// Button-State während des Ladens
btn.disabled = true;
icon.classList.add('fa-spin');
Promise.all([
calendar.refetchEvents(),
loadStatistics()
]).finally(() => {
btn.disabled = false;
icon.classList.remove('fa-spin');
});
}); });
// Cleanup-Button // Cleanup-Button
document.getElementById('cleanupLogs').addEventListener('click', function() { document.getElementById('cleanupLogs').addEventListener('click', function() {
if (confirm('Möchten Sie wirklich alte Logs löschen? (älter als 30 Tage)')) { if (confirm('Möchten Sie wirklich alte Logs löschen? (älter als 30 Tage)\n\nDieser Vorgang kann nicht rückgängig gemacht werden.')) {
const btn = this;
btn.disabled = true;
fetch('/api/admin/plug-schedules/cleanup', { fetch('/api/admin/plug-schedules/cleanup', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -484,23 +738,44 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
alert(`Erfolgreich ${data.deleted_count} alte Einträge gelöscht`); if (typeof showToast === 'function') {
showToast(`Erfolgreich ${data.deleted_count} alte Einträge gelöscht`, 'success', 5000, {
title: 'Bereinigung erfolgreich'
});
} else {
alert(`Erfolgreich ${data.deleted_count} alte Einträge gelöscht`);
}
calendar.refetchEvents(); calendar.refetchEvents();
loadStatistics(); loadStatistics();
} else { } else {
alert('Fehler beim Löschen: ' + data.error); if (typeof showToast === 'function') {
showToast('Fehler beim Löschen: ' + data.error, 'error', 5000, {
title: 'Bereinigung fehlgeschlagen'
});
} else {
alert('Fehler beim Löschen: ' + data.error);
}
} }
}) })
.catch(error => { .catch(error => {
console.error('Fehler:', error); console.error('Fehler:', error);
alert('Fehler beim Löschen der Logs'); if (typeof showToast === 'function') {
showToast('Fehler beim Löschen der Logs', 'error', 5000, {
title: 'Netzwerkfehler'
});
} else {
alert('Fehler beim Löschen der Logs');
}
})
.finally(() => {
btn.disabled = false;
}); });
} }
}); });
// Statistiken laden // Statistiken laden
function loadStatistics() { function loadStatistics() {
fetch('/api/admin/plug-schedules/statistics') return fetch('/api/admin/plug-schedules/statistics')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
@ -519,6 +794,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Kalender initialisieren // Kalender initialisieren
initCalendar(); initCalendar();
// Initial Statistics Load
loadStatistics();
}); });
</script> </script>
{% endblock %} {% endblock %}