🎉 Added new files for data collection and documentation 📚, updated error resilience script, and refactored admin schedule template. #123
This commit is contained in:
parent
6ff407a895
commit
c29ef2c075
4397
DATENSAMMLUNG.html
Normal file
4397
DATENSAMMLUNG.html
Normal file
File diff suppressed because one or more lines are too long
196
IHK_DOKUMENTATION.md
Normal file
196
IHK_DOKUMENTATION.md
Normal 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
|
1030
backend/scripts/update_fehlerresilienz.sh
Normal file
1030
backend/scripts/update_fehlerresilienz.sh
Normal file
File diff suppressed because it is too large
Load Diff
@ -12,38 +12,147 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<style>
|
||||
.calendar-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
/* ===== DARK MODE CALENDAR OPTIMIERUNG ===== */
|
||||
.fc {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.fc-event-title {
|
||||
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 {
|
||||
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;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
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 {
|
||||
background: white;
|
||||
background: rgb(255 255 255 / 0.8);
|
||||
border: 1px solid rgb(226 232 240);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 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 {
|
||||
@ -53,24 +162,45 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ===== BUTTONS ===== */
|
||||
.btn-calendar {
|
||||
background: #3b82f6;
|
||||
background: rgb(59 130 246);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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 {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
@ -83,54 +213,133 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: rgb(71 85 105);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .legend-item {
|
||||
color: rgb(203 213 225);
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
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>
|
||||
{% endblock %}
|
||||
|
||||
{% 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 -->
|
||||
<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="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">
|
||||
<li class="flex items-center">
|
||||
<a href="{{ url_for('admin_page') }}" class="hover:text-blue-600">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">
|
||||
<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 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>
|
||||
</li>
|
||||
<li class="text-gray-900 font-semibold">
|
||||
<li class="text-slate-900 dark:text-white font-semibold">
|
||||
Steckdosenschaltzeiten
|
||||
</li>
|
||||
</ol>
|
||||
</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>
|
||||
<h1 class="text-3xl font-bold text-gray-900">
|
||||
<i class="fas fa-plug text-blue-600 mr-3"></i>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">
|
||||
<i class="fas fa-plug text-blue-600 dark:text-blue-400 mr-3"></i>
|
||||
Steckdosenschaltzeiten
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
Aktualisieren
|
||||
</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>
|
||||
Alte Logs löschen
|
||||
</button>
|
||||
@ -141,33 +350,39 @@
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
<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="flex items-center justify-between">
|
||||
<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-xs opacity-70 mt-1">Gesamte Aktivität</p>
|
||||
</div>
|
||||
<div class="text-2xl opacity-60">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
</div>
|
||||
<i class="fas fa-chart-line text-2xl opacity-60"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<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-xs opacity-70 mt-1">Ohne Fehler</p>
|
||||
</div>
|
||||
<div class="text-2xl opacity-60">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<i class="fas fa-check-circle text-2xl opacity-60"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<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">
|
||||
{% if stats.average_response_time_ms %}
|
||||
{{ "%.0f"|format(stats.average_response_time_ms) }}ms
|
||||
@ -175,18 +390,24 @@
|
||||
N/A
|
||||
{% endif %}
|
||||
</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>
|
||||
<i class="fas fa-stopwatch text-2xl opacity-60"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<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-xs opacity-70 mt-1">Letzte 24h</p>
|
||||
</div>
|
||||
<div class="text-2xl opacity-60">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<i class="fas fa-exclamation-triangle text-2xl opacity-60"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -194,21 +415,25 @@
|
||||
<!-- Filter und Steuerung -->
|
||||
<div class="control-panel">
|
||||
<div class="filter-group">
|
||||
<label for="printerFilter" class="font-medium text-gray-700">Drucker filtern:</label>
|
||||
<select id="printerFilter" class="border border-gray-300 rounded-lg px-3 py-2 bg-white">
|
||||
<div class="flex items-center gap-3">
|
||||
<label for="printerFilter" class="font-medium text-slate-700 dark:text-slate-300 whitespace-nowrap">Drucker filtern:</label>
|
||||
<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">
|
||||
<option value="">Alle Drucker</option>
|
||||
{% for printer in printers %}
|
||||
<option value="{{ printer.id }}">{{ printer.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="border-l border-gray-300 pl-4 ml-4">
|
||||
<label class="font-medium text-gray-700 mr-3">Ansicht:</label>
|
||||
<div class="border-l border-slate-300 dark:border-slate-600 pl-4 ml-4">
|
||||
<label class="font-medium text-slate-700 dark:text-slate-300 mr-3">Ansicht:</label>
|
||||
<div class="inline-flex gap-1">
|
||||
<button id="monthView" class="btn-calendar active">Monat</button>
|
||||
<button id="weekView" class="btn-calendar">Woche</button>
|
||||
<button id="dayView" class="btn-calendar">Tag</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legende -->
|
||||
<div class="legend">
|
||||
@ -237,22 +462,22 @@
|
||||
</div>
|
||||
|
||||
<!-- 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="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">
|
||||
<h3 class="text-lg font-semibold">Schaltung Details</h3>
|
||||
<button id="closeModal" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times"></i>
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Schaltung Details</h3>
|
||||
<button id="closeModal" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors">
|
||||
<i class="fas fa-times text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="modalContent">
|
||||
<div id="modalContent" class="text-slate-700 dark:text-slate-300">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
</div>
|
||||
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
@ -329,75 +554,75 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const startTime = new Date(event.start).toLocaleString('de-DE');
|
||||
|
||||
let detailsHtml = `
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center">
|
||||
<span class="w-4 h-4 rounded mr-3" style="background-color: ${event.backgroundColor}"></span>
|
||||
<span class="font-medium">${event.title}</span>
|
||||
<div class="space-y-4">
|
||||
<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 flex-shrink-0" style="background-color: ${event.backgroundColor}"></span>
|
||||
<span class="font-medium text-slate-900 dark:text-white">${event.title}</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Zeitpunkt:</span>
|
||||
<p>${startTime}</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Zeitpunkt:</span>
|
||||
<p class="text-slate-900 dark:text-white">${startTime}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Drucker:</span>
|
||||
<p>${props.printer_name}</p>
|
||||
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Drucker:</span>
|
||||
<p class="text-slate-900 dark:text-white">${props.printer_name}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Status:</span>
|
||||
<p class="capitalize">${props.status}</p>
|
||||
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Status:</span>
|
||||
<p class="text-slate-900 dark:text-white capitalize font-medium">${props.status}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Quelle:</span>
|
||||
<p class="capitalize">${props.source}</p>
|
||||
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Quelle:</span>
|
||||
<p class="text-slate-900 dark:text-white capitalize">${props.source}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (props.user_name) {
|
||||
detailsHtml += `
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Benutzer:</span>
|
||||
<p>${props.user_name}</p>
|
||||
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Benutzer:</span>
|
||||
<p class="text-slate-900 dark:text-white">${props.user_name}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (props.response_time_ms) {
|
||||
detailsHtml += `
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Antwortzeit:</span>
|
||||
<p>${props.response_time_ms}ms</p>
|
||||
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Antwortzeit:</span>
|
||||
<p class="text-slate-900 dark:text-white">${props.response_time_ms}ms</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (props.power_consumption) {
|
||||
detailsHtml += `
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Verbrauch:</span>
|
||||
<p>${props.power_consumption}W</p>
|
||||
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Verbrauch:</span>
|
||||
<p class="text-slate-900 dark:text-white">${props.power_consumption}W</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (props.voltage) {
|
||||
detailsHtml += `
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Spannung:</span>
|
||||
<p>${props.voltage}V</p>
|
||||
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Spannung:</span>
|
||||
<p class="text-slate-900 dark:text-white">${props.voltage}V</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (props.current) {
|
||||
detailsHtml += `
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Strom:</span>
|
||||
<p>${props.current}A</p>
|
||||
<div class="bg-slate-50 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<span class="font-medium text-slate-600 dark:text-slate-400 block mb-1">Strom:</span>
|
||||
<p class="text-slate-900 dark:text-white">${props.current}A</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -406,18 +631,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (props.notes) {
|
||||
detailsHtml += `
|
||||
<div class="mt-4">
|
||||
<span class="font-medium text-gray-600">Notizen:</span>
|
||||
<p class="text-sm bg-gray-50 p-2 rounded mt-1">${props.notes}</p>
|
||||
<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-blue-800 dark:text-blue-200 block mb-2">Notizen:</span>
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">${props.notes}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (props.error_message) {
|
||||
detailsHtml += `
|
||||
<div class="mt-4">
|
||||
<span class="font-medium text-red-600">Fehlermeldung:</span>
|
||||
<p class="text-sm bg-red-50 text-red-700 p-2 rounded mt-1">${props.error_message}</p>
|
||||
<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-800 dark:text-red-200 block mb-2">Fehlermeldung:</span>
|
||||
<p class="text-sm text-red-700 dark:text-red-300">${props.error_message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -435,6 +660,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('closeModal').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
|
||||
document.getElementById('printerFilter').addEventListener('change', function() {
|
||||
currentPrinterFilter = this.value;
|
||||
@ -466,13 +705,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Aktualisieren-Button
|
||||
document.getElementById('refreshData').addEventListener('click', function() {
|
||||
calendar.refetchEvents();
|
||||
loadStatistics();
|
||||
const btn = this;
|
||||
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
|
||||
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', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -484,23 +738,44 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
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();
|
||||
loadStatistics();
|
||||
} else {
|
||||
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 => {
|
||||
console.error('Fehler:', error);
|
||||
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
|
||||
function loadStatistics() {
|
||||
fetch('/api/admin/plug-schedules/statistics')
|
||||
return fetch('/api/admin/plug-schedules/statistics')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
@ -519,6 +794,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Kalender initialisieren
|
||||
initCalendar();
|
||||
|
||||
// Initial Statistics Load
|
||||
loadStatistics();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user