📝 Commit Details:
2
backend/.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
fund=false
|
||||
audit-level=moderate
|
BIN
backend/__pycache__/app.cpython-313.pyc
Normal file
BIN
backend/__pycache__/models.cpython-311.pyc
Normal file
BIN
backend/__pycache__/models.cpython-313.pyc
Normal file
@ -1,595 +0,0 @@
|
||||
# Häufige Fehler und Lösungen
|
||||
|
||||
Dieses Dokument enthält häufig auftretende Fehler und deren Lösungen für das MYP-3D-Druck-Management-System.
|
||||
|
||||
## Drucker-Status-Check mit 7-Sekunden-Timeout
|
||||
|
||||
### Implementierung (2024-12-19)
|
||||
|
||||
**Problem:** Drucker-Status wurde nur basierend auf hardkodierten Konfigurationen bestimmt, ohne echte Netzwerk-Überprüfung.
|
||||
|
||||
**Lösung:** Implementierung eines echten Ping-basierten Status-Checks mit 7-Sekunden-Timeout:
|
||||
|
||||
#### Backend-Änderungen:
|
||||
1. **Neue Funktionen in `app.py`:**
|
||||
- `check_printer_status(ip_address, timeout=7)`: Überprüft einzelnen Drucker via Ping
|
||||
- `check_multiple_printers_status(printers, timeout=7)`: Parallel-Check mehrerer Drucker
|
||||
|
||||
2. **Aktualisierte API-Endpunkte:**
|
||||
- `/api/printers`: Verwendet jetzt echten Status-Check
|
||||
- `/api/printers/status`: Spezieller Endpunkt für Status-Überprüfung
|
||||
|
||||
3. **Features:**
|
||||
- 7-Sekunden-Timeout pro Drucker
|
||||
- Parallel-Ausführung mit ThreadPoolExecutor
|
||||
- Windows- und Unix-kompatible Ping-Befehle
|
||||
- Detailliertes Logging der Status-Checks
|
||||
- Automatische Datenbank-Aktualisierung
|
||||
|
||||
#### Frontend-Änderungen:
|
||||
1. **Erweiterte Drucker-Seite (`templates/printers.html`):**
|
||||
- Funktionsfähiger "Aktualisieren"-Button
|
||||
- Loading-States mit Timeout-Information
|
||||
- Status-Nachrichten mit Erfolgs-/Fehler-Feedback
|
||||
- Zeitstempel der letzten Überprüfung
|
||||
|
||||
2. **Neue JavaScript-Funktionen:**
|
||||
- `refreshPrinters()`: Vollständiger Status-Check mit UI-Feedback
|
||||
- `loadPrintersWithStatusCheck()`: Erweiterte Lade-Funktion
|
||||
- `showStatusMessage()`: Toast-Nachrichten für Benutzer-Feedback
|
||||
- `formatTime()`: Relative Zeitanzeige für Status-Checks
|
||||
|
||||
#### Benutzer-Erfahrung:
|
||||
- **Vor dem Update:** Drucker-Status basierte nur auf Konfiguration
|
||||
- **Nach dem Update:**
|
||||
- Echter Ping-Test mit 7-Sekunden-Timeout
|
||||
- Visuelles Feedback während der Überprüfung
|
||||
- Erfolgs-/Fehler-Nachrichten
|
||||
- Zeitstempel der letzten Überprüfung
|
||||
- Button-Deaktivierung während des Checks
|
||||
|
||||
#### Technische Details:
|
||||
- **Timeout:** 7 Sekunden pro Drucker (konfigurierbar)
|
||||
- **Parallel-Verarbeitung:** Bis zu 10 gleichzeitige Checks
|
||||
- **Plattform-Unterstützung:** Windows (`ping -n 1 -w 7000`) und Unix (`ping -c 1 -W 7`)
|
||||
- **Fehlerbehandlung:** Graceful Fallback auf "offline" bei Timeouts oder Fehlern
|
||||
- **Logging:** Detaillierte Protokollierung aller Status-Checks
|
||||
|
||||
#### Konfiguration:
|
||||
```python
|
||||
# Timeout kann in den API-Aufrufen angepasst werden
|
||||
status_results = check_multiple_printers_status(printer_data, timeout=7)
|
||||
```
|
||||
|
||||
#### Fehlerbehebung:
|
||||
1. **Timeout-Probleme:** Timeout kann in der Funktion angepasst werden
|
||||
2. **Netzwerk-Probleme:** Logs in `logs/printers/printers.log` prüfen
|
||||
3. **Performance:** Bei vielen Druckern ThreadPool-Größe anpassen
|
||||
|
||||
## Job-Scheduler und Steckdosensteuerung
|
||||
|
||||
### Steckdose kann nicht eingeschaltet werden
|
||||
|
||||
**Problem:**
|
||||
Log-Eintrag: `Fehler beim Starten von Job X: Steckdose konnte nicht eingeschaltet werden`
|
||||
|
||||
**Mögliche Ursachen und Lösungen:**
|
||||
1. **Netzwerkverbindung zu Steckdose unterbrochen**
|
||||
- Prüfen Sie, ob die Steckdose mit dem Netzwerk verbunden ist
|
||||
- Ping-Test: `ping [IP-Adresse der Steckdose]`
|
||||
- Steckdose neu starten (physischer Reset-Knopf)
|
||||
|
||||
2. **Falsche Zugangsdaten für Steckdose**
|
||||
- Überprüfen Sie in der Datenbank, ob die korrekten Zugangsdaten (Benutzername/Passwort) für die Steckdose hinterlegt sind
|
||||
- Admin-Bereich → Drucker → Steckdosenkonfiguration bearbeiten
|
||||
|
||||
3. **PyP110-Bibliothek veraltet**
|
||||
- Bibliothek aktualisieren: `pip install PyP100 --upgrade`
|
||||
|
||||
### Job wird nicht gestartet oder beendet
|
||||
|
||||
**Problem:**
|
||||
Ein geplanter Job startet nicht oder ein laufender Job wird nicht beendet
|
||||
|
||||
**Mögliche Ursachen und Lösungen:**
|
||||
1. **Scheduler-Thread läuft nicht**
|
||||
- Prüfen Sie den Status des Schedulers im Admin-Bereich
|
||||
- Wenn nicht aktiv: Scheduler starten
|
||||
- Bei wiederholtem Problem: Server neu starten
|
||||
|
||||
2. **Datenbankprobleme**
|
||||
- Überprüfen Sie die Datenbank auf Konsistenz
|
||||
- Backup einspielen, falls notwendig
|
||||
|
||||
### Job-Verlängerung funktioniert nicht
|
||||
|
||||
**Problem:**
|
||||
Nach dem Verlängern eines Jobs wird er trotzdem zum ursprünglichen Zeitpunkt beendet
|
||||
|
||||
**Lösung:**
|
||||
1. Prüfen Sie die Datenbankeinträge, ob die `end_at`-Zeit korrekt aktualisiert wurde
|
||||
2. Prüfen Sie die Log-Dateien auf Fehler beim Aktualisieren des Jobs
|
||||
3. Verlängern Sie den Job erneut mit größerem Zeitpuffer
|
||||
|
||||
## Sicherheitshinweise
|
||||
|
||||
### Sicherheitsmaßnahmen bei Stromausfällen
|
||||
|
||||
**Wichtig:**
|
||||
- Bei Stromausfällen kehren die Steckdosen in ihren Standardzustand zurück (meist AUS)
|
||||
- Nach Wiederherstellung der Stromversorgung wird der Scheduler automatisch Jobs wieder aufnehmen
|
||||
- Laufende Jobs werden jedoch **nicht** automatisch fortgesetzt, um Schäden zu vermeiden
|
||||
- Administrator muss laufende Jobs manuell neu starten
|
||||
|
||||
### 5-Minuten Sicherheitspuffer
|
||||
|
||||
Das System verwendet einen 5-Minuten Sicherheitspuffer, bevor es einen Job beendet. Dies verhindert, dass ein Druck zu früh beendet wird. Die tatsächliche Ausschaltzeit ist also immer 5 Minuten nach der im System angezeigten Endzeit.
|
||||
|
||||
## API-Endpunkte für Fehlerbehebung
|
||||
|
||||
| Endpunkt | Beschreibung |
|
||||
|----------|--------------|
|
||||
| `GET /api/scheduler/status` | Status des Job-Schedulers abfragen |
|
||||
| `POST /api/scheduler/start` | Scheduler manuell starten (Admin) |
|
||||
| `POST /api/scheduler/stop` | Scheduler manuell stoppen (Admin) |
|
||||
| `GET /api/jobs/active` | Alle aktiven Jobs anzeigen |
|
||||
| `POST /api/jobs/<id>/extend` | Job-Laufzeit verlängern |
|
||||
| `POST /api/jobs/<id>/finish` | Job manuell beenden (Admin) |
|
||||
|
||||
## Flask-Login Fehler
|
||||
|
||||
### AttributeError: 'User' object has no attribute 'is_authenticated'
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
AttributeError: 'User' object has no attribute 'is_authenticated'
|
||||
```
|
||||
|
||||
**Ursache:** Die User-Klasse erbt nicht von `UserMixin` oder implementiert nicht die erforderlichen Flask-Login Attribute.
|
||||
|
||||
**Lösungen:**
|
||||
1. Stellen Sie sicher, dass die User-Klasse von `UserMixin` erbt:
|
||||
```python
|
||||
from flask_login import UserMixin
|
||||
|
||||
class User(UserMixin, Base):
|
||||
# ... Rest der Klasse
|
||||
```
|
||||
2. Implementieren Sie die erforderlichen Methoden manuell:
|
||||
```python
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.active
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
def get_id(self):
|
||||
return str(self.id)
|
||||
```
|
||||
3. Führen Sie die Datenbankmigration aus:
|
||||
```bash
|
||||
python3.11 migrate_db.py
|
||||
```
|
||||
|
||||
### Fehlende username-Spalte
|
||||
|
||||
**Problem:** Die Anwendung versucht auf `username` zuzugreifen, aber die Spalte existiert nicht in der Datenbank.
|
||||
|
||||
**Lösungen:**
|
||||
1. Führen Sie das Migrationsskript aus:
|
||||
```bash
|
||||
python3.11 migrate_db.py
|
||||
```
|
||||
2. Falls die Migration fehlschlägt, initialisieren Sie die Datenbank neu:
|
||||
```bash
|
||||
python3.11 init_db.py
|
||||
```
|
||||
|
||||
## Datenbank-Fehler
|
||||
|
||||
### SQLite Database is locked
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) database is locked
|
||||
```
|
||||
|
||||
**Ursache:** Dieser Fehler tritt auf, wenn mehrere Prozesse gleichzeitig auf die Datenbank zugreifen.
|
||||
|
||||
**Lösungen:**
|
||||
1. Stellen Sie sicher, dass Sie alle Datenbankverbindungen mit `db_session.close()` schließen
|
||||
2. Verwenden Sie `with get_db_session() as session:` um Sessions automatisch zu schließen
|
||||
3. Prüfen Sie, ob der Scheduler und die Hauptanwendung gleichzeitig auf die Datenbank zugreifen
|
||||
|
||||
### Detached Instance Error
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
sqlalchemy.orm.exc.DetachedInstanceError: Instance <Job at 0x...> is not bound to a Session
|
||||
```
|
||||
|
||||
**Ursache:** Zugriff auf ein SQLAlchemy-Objekt nach dem Schließen der Session.
|
||||
|
||||
**Lösungen:**
|
||||
1. Verwenden Sie `joinedload` für eager loading: `db_session.query(Job).options(joinedload(Job.user))`
|
||||
2. Konvertieren Sie Objekte zu Dictionaries, bevor Sie die Session schließen: `job_dict = job.to_dict()`
|
||||
3. Verwenden Sie `session.expunge_all()` gefolgt von `session.close()`, wenn Sie Objekte weiter verwenden müssen
|
||||
|
||||
## Tapo Smart Plug Fehler
|
||||
|
||||
### Verbindungsfehler
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
PyP100.PyP100.P100Exception: Could not connect to device
|
||||
```
|
||||
|
||||
**Ursache:** Smart Plug ist nicht erreichbar oder Authentifizierungsprobleme.
|
||||
|
||||
**Lösungen:**
|
||||
1. Überprüfen Sie die IP-Adresse des Smart Plugs
|
||||
2. Stellen Sie sicher, dass der Smart Plug eingeschaltet und mit dem WLAN verbunden ist
|
||||
3. Überprüfen Sie die Zugangsdaten in `config/settings.py`
|
||||
4. Verwenden Sie einen Try-Except-Block zur Fehlerbehandlung:
|
||||
```python
|
||||
try:
|
||||
p110 = PyP110.P110(ip, username, password)
|
||||
p110.handshake()
|
||||
p110.login()
|
||||
p110.turnOn()
|
||||
except Exception as e:
|
||||
printers_logger.error(f"Smart Plug Fehler: {str(e)}")
|
||||
```
|
||||
|
||||
## Flask-Anwendungsfehler
|
||||
|
||||
### Interner Server-Fehler (500)
|
||||
|
||||
**Problem:** Die Anwendung gibt einen 500-Fehler zurück.
|
||||
|
||||
**Ursache:** Verschiedene mögliche Ursachen, von Datenbank- bis hin zu Programmierfehlern.
|
||||
|
||||
**Lösungen:**
|
||||
1. Überprüfen Sie die Logs unter `logs/app/app.log` und `logs/errors/errors.log`
|
||||
2. Starten Sie die Anwendung im Debug-Modus: `FLASK_DEBUG=True python3.11 app.py`
|
||||
3. Implementieren Sie bessere Fehlerbehandlung mit Try-Except-Blöcken
|
||||
|
||||
### Authentifizierungsfehler
|
||||
|
||||
**Problem:** Benutzer können sich nicht anmelden oder erhalten unbefugte Zugriffsfehler.
|
||||
|
||||
**Ursachen:**
|
||||
- Ungültige Anmeldedaten
|
||||
- Session-Probleme
|
||||
- Cookie-Probleme
|
||||
|
||||
**Lösungen:**
|
||||
1. Überprüfen Sie die Logs unter `logs/auth/auth.log`
|
||||
2. Setzen Sie das Passwort zurück mit:
|
||||
```python
|
||||
from models import User, get_db_session
|
||||
|
||||
db_session = get_db_session()
|
||||
user = db_session.query(User).filter(User.username == "admin").first()
|
||||
user.set_password("neues_passwort")
|
||||
db_session.commit()
|
||||
db_session.close()
|
||||
```
|
||||
3. Löschen Sie Browser-Cookies und -Cache
|
||||
|
||||
## CSS und Frontend-Fehler
|
||||
|
||||
### Tailwind-Stile werden nicht angewendet
|
||||
|
||||
**Problem:** Die Tailwind-Klassen haben keine Wirkung auf das Styling.
|
||||
|
||||
**Lösungen:**
|
||||
1. Stellen Sie sicher, dass die generierte CSS-Datei korrekt eingebunden ist:
|
||||
```html
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind-dark-consolidated.min.css') }}">
|
||||
```
|
||||
2. Bauen Sie die CSS-Datei neu: `npm run build:css`
|
||||
3. Überprüfen Sie in der Browser-Konsole, ob Fehler beim Laden der CSS-Datei auftreten
|
||||
|
||||
### Dark Mode funktioniert nicht
|
||||
|
||||
**Problem:** Die Dark-Mode-Stile werden nicht angewendet.
|
||||
|
||||
**Lösungen:**
|
||||
1. Stellen Sie sicher, dass das HTML-Element die entsprechende Klasse hat:
|
||||
```html
|
||||
<html class="dark" data-theme="dark">
|
||||
```
|
||||
2. Überprüfen Sie, ob die Tailwind-Konfiguration Dark Mode aktiviert hat:
|
||||
```js
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
darkMode: 'class',
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Scheduler-Fehler
|
||||
|
||||
### Scheduler führt keine Jobs aus
|
||||
|
||||
**Problem:** Der Scheduler schaltet Drucker nicht ein/aus wie erwartet.
|
||||
|
||||
**Ursachen:**
|
||||
- Scheduler ist nicht gestartet
|
||||
- Fehler bei der Tapo-Verbindung
|
||||
- Fehler in der Scheduler-Logik
|
||||
|
||||
**Lösungen:**
|
||||
1. Überprüfen Sie die Logs unter `logs/scheduler/scheduler.log`
|
||||
2. Stellen Sie sicher, dass `SCHEDULER_ENABLED = True` in `config/settings.py` gesetzt ist
|
||||
3. Starten Sie die Anwendung neu
|
||||
|
||||
## Performance-Probleme
|
||||
|
||||
### Langsame Ladezeiten
|
||||
|
||||
**Problem:** Die Anwendung lädt langsam oder reagiert träge.
|
||||
|
||||
**Lösungen:**
|
||||
1. Optimieren Sie Datenbankabfragen mit geeigneten Indizes
|
||||
2. Reduzieren Sie die Anzahl der Datenbankabfragen durch Eager Loading
|
||||
3. Implementieren Sie Caching für häufig abgerufene Daten
|
||||
4. Überprüfen Sie die Logfiles auf übermäßige Logging-Aktivitäten
|
||||
|
||||
## Content Security Policy (CSP) Fehler
|
||||
|
||||
### Script-src-elem 'none' Fehler
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
Refused to load the script because it violates the following Content Security Policy directive: "script-src-elem 'none'".
|
||||
```
|
||||
|
||||
**Ursache:** Die Content Security Policy ist zu restriktiv eingestellt und blockiert das Laden von Skripten.
|
||||
|
||||
**Lösungen:**
|
||||
1. Überprüfen Sie die CSP-Konfiguration in `config/security.py`:
|
||||
```python
|
||||
'Content-Security-Policy': (
|
||||
"default-src 'self'; "
|
||||
"script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
|
||||
"script-src-elem 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
|
||||
# ... weitere Regeln
|
||||
)
|
||||
```
|
||||
2. Stellen Sie sicher, dass die CSP in der `after_request`-Funktion richtig angewendet wird:
|
||||
```python
|
||||
@app.after_request
|
||||
def add_security_headers(response):
|
||||
from config.security import get_security_headers
|
||||
security_headers = get_security_headers()
|
||||
|
||||
for header, value in security_headers.items():
|
||||
response.headers[header] = value
|
||||
|
||||
# CSP-Report-Only Header entfernen
|
||||
if 'Content-Security-Policy-Report-Only' in response.headers:
|
||||
del response.headers['Content-Security-Policy-Report-Only']
|
||||
|
||||
return response
|
||||
```
|
||||
3. Vermeiden Sie mehrere konkurrierende Service Worker Registrierungen, die CSP-Konflikte verursachen können.
|
||||
|
||||
### Inline Script Fehler
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src-elem 'none'"
|
||||
```
|
||||
|
||||
**Lösungen:**
|
||||
1. Fügen Sie 'unsafe-inline' zur script-src Direktive hinzu
|
||||
2. Alternativ fügen Sie einen Hash oder Nonce für das Inline-Skript hinzu:
|
||||
```html
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
// Inline JavaScript
|
||||
</script>
|
||||
```
|
||||
und in der CSP:
|
||||
```
|
||||
script-src 'nonce-{{ csp_nonce }}';
|
||||
```
|
||||
|
||||
### Service Worker Registration Fehler
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
Refused to create a worker from 'URL' because it violates the following Content Security Policy directive: "script-src 'none'"
|
||||
```
|
||||
|
||||
**Lösungen:**
|
||||
1. Stellen Sie sicher, dass die `worker-src` Direktive korrekt konfiguriert ist:
|
||||
```
|
||||
worker-src 'self' blob:;
|
||||
```
|
||||
2. Erstellen Sie die erforderlichen Service Worker Dateien im richtigen Verzeichnis
|
||||
3. Verwenden Sie einen einheitlichen Ansatz zur Service Worker Registrierung, vorzugsweise im offline-app.js:
|
||||
```javascript
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/static/js/sw.js')
|
||||
.then(function(registration) {
|
||||
console.log('Service Worker registriert:', registration.scope);
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.log('Service Worker Registration fehlgeschlagen:', error);
|
||||
// Fallback
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Icon 404 Fehler
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
Failed to load resource: the server responded with a status of 404 (NOT FOUND)
|
||||
Error while trying to use the following icon from the Manifest: URL (Download error or resource isn't a valid image)
|
||||
```
|
||||
|
||||
**Lösungen:**
|
||||
1. Erstellen Sie die fehlenden Icon-Dateien im Verzeichnis `static/icons/`
|
||||
2. Aktualisieren Sie das Web App Manifest, um nur verfügbare Icons zu referenzieren:
|
||||
```json
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icons/mercedes-logo.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
]
|
||||
```
|
||||
3. Verwenden Sie Tools wie Lighthouse, um fehlende PWA-Ressourcen zu identifizieren
|
||||
|
||||
## JavaScript-Referenzfehler
|
||||
|
||||
### Problem: Nicht definierte Funktionen in admin-panel.js
|
||||
Fehlermeldungen wie "Uncaught ReferenceError: loadPrinters is not defined" können auftreten, wenn Funktionen in der JavaScript-Datei verwendet, aber nicht definiert werden.
|
||||
|
||||
**Lösung:**
|
||||
- Implementiere die fehlenden Funktionen (loadPrinters, loadSchedulerStatus, loadSystemStats, loadLogs, loadUsers, etc.)
|
||||
- Stelle sicher, dass alle aufgerufenen Funktionen im richtigen Scope definiert sind
|
||||
- Überprüfe die Reihenfolge der Funktionsdefinitionen und -aufrufe
|
||||
|
||||
### Problem: Funktionen in anderen Dateien werden nicht gefunden
|
||||
Fehler wie "Uncaught ReferenceError: refreshJobs is not defined" in jobs.html oder "exportStats is not defined" in stats.html.
|
||||
|
||||
**Lösung:**
|
||||
- Stelle sicher, dass die Funktionen in der jeweiligen HTML-Datei oder in einer eingebundenen JavaScript-Datei definiert sind
|
||||
- Überprüfe, ob die JavaScript-Dateien korrekt in der HTML-Datei eingebunden sind
|
||||
- Nutze konsistente Namenskonventionen für Funktionen
|
||||
|
||||
## Service Worker Fehler
|
||||
|
||||
### Problem: Cache-API kann mit chrome-extension URLs nicht umgehen
|
||||
Fehler: "Uncaught (in promise) TypeError: Failed to execute 'put' on 'Cache': Request scheme 'chrome-extension' is unsupported"
|
||||
|
||||
**Lösung:**
|
||||
- Überprüfe die URL-Protokolle bevor du Cache-Operationen durchführst
|
||||
- Füge eine spezielle Behandlung für chrome-extension-Protokolle hinzu
|
||||
- Verwende try-catch-Blöcke, um Fehler bei der Cache-Handhabung abzufangen
|
||||
|
||||
```javascript
|
||||
if (url.protocol === 'chrome-extension:') {
|
||||
// Spezielle Behandlung für chrome-extension URLs
|
||||
try {
|
||||
return await fetch(request);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch from chrome-extension:', error);
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Fehler beim Zugriff auf chrome-extension',
|
||||
offline: true
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API-Endpunkt Fehler
|
||||
|
||||
### Problem: API-Ressourcen werden nicht gefunden (404)
|
||||
Fehlermeldungen wie "Failed to load resource: the server responded with a status of 404 (NOT FOUND)"
|
||||
|
||||
**Lösung:**
|
||||
- Überprüfe die API-Routen im Backend
|
||||
- Stelle sicher, dass die API-Endpunkte korrekt implementiert sind
|
||||
- Überprüfe die Anfrage-URLs in den Frontend-Skripten
|
||||
- Füge Fehlerbehandlung für fehlende Ressourcen hinzu
|
||||
|
||||
## Frontend-Rendering Fehler
|
||||
|
||||
### Problem: Fehler beim Laden der Admin-Statistiken
|
||||
"Error loading admin: admin-panel.js:484 Fehler beim Laden der Admin-Statistiken"
|
||||
|
||||
**Lösung:**
|
||||
- Implementiere ordnungsgemäße Fehlerbehandlung in API-Anfragen
|
||||
- Zeige benutzerfreundliche Fehlermeldungen an
|
||||
- Füge Fallback-Werte für fehlende Daten hinzu
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const response = await fetch('/api/admin/stats');
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Laden der Admin-Statistiken');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Daten verarbeiten
|
||||
} catch (error) {
|
||||
console.error('Error loading admin stats:', error);
|
||||
showNotification('Fehler beim Laden der Admin-Statistiken', 'error');
|
||||
}
|
||||
```
|
||||
|
||||
## Icon/Ressourcen Fehler
|
||||
|
||||
### Problem: Icon-Dateien werden nicht gefunden
|
||||
"Error while trying to use the following icon from the Manifest: http://127.0.0.1:5000/static/icons/icon-144x144.png"
|
||||
|
||||
**Lösung:**
|
||||
- Überprüfe, ob die Icon-Dateien im richtigen Verzeichnis vorhanden sind
|
||||
- Stelle sicher, dass die Pfade in der Manifest-Datei korrekt sind
|
||||
- Füge Fallback-Icons für den Fall hinzu, dass die primären Icons nicht geladen werden können
|
||||
|
||||
## Allgemeine Fehlerbehebung
|
||||
|
||||
1. **Browser-Konsole prüfen**: Die meisten JavaScript-Fehler werden in der Browser-Konsole angezeigt (F12 oder Rechtsklick -> Untersuchen -> Konsole)
|
||||
2. **Netzwerk-Tab prüfen**: Im Netzwerk-Tab des Browsers können fehlgeschlagene API-Anfragen identifiziert werden
|
||||
3. **Codestruktur überprüfen**: Stelle sicher, dass alle Funktionen in der richtigen Reihenfolge definiert werden
|
||||
4. **Fehlerbehandlung verbessern**: Implementiere try-catch-Blöcke für alle asynchronen Funktionen
|
||||
5. **Browser-Cache leeren**: Manchmal können Probleme durch veraltete gecachte Dateien verursacht werden
|
||||
|
||||
## 404 Fehler (Seite nicht gefunden)
|
||||
|
||||
### Fehlende Routen oder API-Endpunkte
|
||||
|
||||
**Problem:**
|
||||
Die Anwendung zeigt 404-Fehler für bestimmte Routen wie `/privacy`, `/terms`, `/my/jobs`, `/api/user/export` oder `/api/user/profile`.
|
||||
|
||||
**Ursachen:**
|
||||
- Die Route wurde nicht korrekt in `app.py` oder dem entsprechenden Blueprint registriert
|
||||
- Bei API-Endpunkten: Die alte API-Route wurde verwendet, aber die Anwendung nutzt jetzt eine neue Route
|
||||
- Möglicherweise wurden die Templates erstellt, aber die Routen dafür fehlen
|
||||
|
||||
**Lösungen:**
|
||||
1. **Für öffentliche Seiten** (`/privacy`, `/terms`):
|
||||
- Überprüfen Sie, ob die Routen in `app.py` definiert sind
|
||||
- Stellen Sie sicher, dass der `@login_required` Decorator entfernt wurde, falls diese Seiten öffentlich sein sollen
|
||||
- Prüfen Sie, ob die entsprechenden Template-Dateien unter `templates/` existieren
|
||||
|
||||
2. **Für Benutzer-spezifische Seiten** (`/my/jobs`):
|
||||
- Fügen Sie eine Route hinzu, die zur Jobs-Seite mit vorgefiltertem Benutzer-Parameter weiterleitet
|
||||
|
||||
3. **Für API-Weiterleitungen** (`/api/user/export`, `/api/user/profile`):
|
||||
- Erstellen Sie Weiterleitungsrouten, die zu den neuen Blueprint-Routen führen
|
||||
- Beispiel: `/api/user/export` sollte zu `/user/export` weiterleiten
|
||||
|
||||
4. **Debugging von Routen:**
|
||||
- Mit `flask routes` können Sie alle registrierten Routen auflisten
|
||||
- Überprüfen Sie die Log-Dateien auf fehlgeschlagene Routenzugriffe
|
||||
- Im Debug-Modus starten: `FLASK_DEBUG=True python3.11 app.py`
|
||||
|
||||
5. **Blueprint-Registrierung prüfen:**
|
||||
- Stellen Sie sicher, dass alle Blueprints korrekt in `app.py` registriert sind:
|
||||
```python
|
||||
from blueprints.user import user_bp
|
||||
app.register_blueprint(user_bp)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Dieses Dokument wird kontinuierlich aktualisiert, wenn neue Fehler auftreten und Lösungen gefunden werden.
|
@ -1,173 +0,0 @@
|
||||
# MYP Platform - Entwicklungs-Roadmap
|
||||
|
||||
Dieses Dokument beschreibt die geplanten Entwicklungsschritte und zukünftigen Features für das MYP 3D-Drucker Reservierungssystem.
|
||||
|
||||
## Aktuelle Version: 1.1
|
||||
|
||||
Die aktuelle Version umfasst die Grundfunktionalitäten:
|
||||
|
||||
- Benutzerauthentifizierung und -verwaltung
|
||||
- Druckerverwaltung
|
||||
- Job-Scheduling und -Überwachung
|
||||
- Smart Plug Integration
|
||||
- **✅ Vollständiges UI-Komponenten-System mit Tailwind CSS**
|
||||
- **✅ Template-Helper für einfache UI-Entwicklung**
|
||||
- **✅ JavaScript-Utilities für interaktive Komponenten**
|
||||
- **✅ Dark Mode Support**
|
||||
- **✅ Responsive Design**
|
||||
- **✅ Umfassende UI-Dokumentation**
|
||||
|
||||
## Kürzlich Abgeschlossen (Version 1.2) ✅
|
||||
|
||||
### Sicherheits-Features ✅
|
||||
|
||||
- ✅ **Rate Limiting**: Schutz vor API-Missbrauch und DDoS-Attacken
|
||||
- ✅ **Content Security Policy (CSP)**: Schutz vor XSS-Angriffen
|
||||
- ✅ **Erweiterte Security Headers**: Comprehensive security headers für alle Responses
|
||||
- ✅ **Verdächtige Aktivitäts-Erkennung**: Automatische Erkennung von SQL-Injection und anderen Bedrohungen
|
||||
- ✅ **Client-Fingerprinting**: Erweiterte Sicherheit durch Client-Identifikation
|
||||
|
||||
### Erweiterte Berechtigungen ✅
|
||||
|
||||
- ✅ **Granulare Berechtigungen**: 7 detaillierte Rollen (Guest bis Super Admin)
|
||||
- ✅ **Ressourcen-spezifische Zugriffskontrolle**: Job-, Drucker- und Benutzer-spezifische Berechtigungen
|
||||
- ✅ **Temporäre Berechtigungen**: Zeitlich begrenzte Berechtigungsüberschreibungen
|
||||
- ✅ **Permission Caching**: Performance-optimierte Berechtigungsprüfung
|
||||
- ✅ **Template-Integration**: Template-Helper für berechtigungsbasierte UI-Anzeige
|
||||
|
||||
### Erweiterte UI-Komponenten ✅
|
||||
|
||||
- ✅ **Progress-Bars**: Animierte, konfigurabr progress indicators mit verschiedenen Styles
|
||||
- ✅ **Advanced File-Upload**: Drag & Drop, Preview, Chunk-Upload, Validierung
|
||||
- ✅ **DatePicker**: Deutscher Kalender mit Validierung und Custom Events
|
||||
- ✅ **Auto-Initialisierung**: Data-Attribute-basierte Komponenten-Initialisierung
|
||||
|
||||
### Analytics & Statistiken ✅
|
||||
|
||||
- ✅ **Umfassende Analytics-Engine**: Drucker-, Job- und Benutzer-Statistiken
|
||||
- ✅ **KPI-Dashboard**: Key Performance Indicators mit Trend-Analyse
|
||||
- ✅ **Report-Generierung**: Verschiedene Report-Typen und Zeiträume
|
||||
- ✅ **Interaktive Charts**: Chart.js-basierte Visualisierungen
|
||||
- ✅ **Export-Funktionalität**: JSON, CSV, PDF, Excel-Export (Framework bereit)
|
||||
|
||||
## Geplante Features
|
||||
|
||||
### Version 1.3 (Kurzfristig)
|
||||
|
||||
- [ ] **E-Mail-Benachrichtigungen**: Bei Job-Status-Änderungen und System-Events
|
||||
- [ ] **Erweiterte Formular-Validierung**: Client- und serverseitige Validierung mit UI-Feedback
|
||||
- [ ] **Multi-Format-Export**: Vollständige PDF- und Excel-Report-Generierung
|
||||
- [ ] **Zwei-Faktor-Authentifizierung**: TOTP-basierte 2FA-Implementierung
|
||||
|
||||
### Version 1.3 (Mittelfristig)
|
||||
|
||||
- [ ] Druckerprofile mit spezifischen Eigenschaften (Druckvolumen, Materialien, etc.)
|
||||
- [ ] Materialverwaltung und -tracking
|
||||
- [ ] Verbessertes Dashboard mit Echtzeit-Updates
|
||||
- [ ] **HTMX-Integration**: Für bessere Interaktivität ohne JavaScript-Framework
|
||||
- [ ] **Drag & Drop**: Für Job-Reihenfolge und Datei-Uploads
|
||||
- [ ] **Erweiterte Tabellen**: Sortierung, Filterung, Pagination
|
||||
|
||||
### Version 2.0 (Langfristig)
|
||||
|
||||
- [ ] OctoPrint Integration für direkte Druckersteuerung
|
||||
- [ ] Mobile App mit Push-Benachrichtigungen
|
||||
- [ ] Wartungsplanung und -tracking
|
||||
- [ ] Multi-Standort-Unterstützung
|
||||
- [ ] **Progressive Web App (PWA)**: Offline-Funktionalität und App-Installation
|
||||
- [ ] **Erweiterte Themes**: Anpassbare Farbschemata und Layouts
|
||||
|
||||
## Technische Verbesserungen
|
||||
|
||||
### Backend
|
||||
|
||||
- [ ] Refactoring für verbesserte Modularität
|
||||
- [ ] REST API Dokumentation mit Swagger/OpenAPI
|
||||
- [ ] Verbesserte Testabdeckung
|
||||
- [ ] Migration zu SQLAlchemy 2.0
|
||||
- [ ] **WebSocket-Integration**: Für Echtzeit-Updates
|
||||
|
||||
### Frontend
|
||||
|
||||
- [X] ~~Optimierung der Benutzeroberfläche~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~UI-Komponenten-System~~ ✅ **Abgeschlossen**
|
||||
- [ ] **HTMX-Integration**: Für bessere Interaktivität ohne komplexe JavaScript-Frameworks
|
||||
- [ ] **Progressive Web App (PWA)**: Funktionalität für App-ähnliche Erfahrung
|
||||
- [ ] **Barrierefreiheit**: Nach WCAG-Richtlinien
|
||||
- [ ] **Performance-Optimierung**: Lazy Loading, Code Splitting
|
||||
|
||||
### CSS/Styling
|
||||
|
||||
- [X] ~~Tailwind CSS Integration~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~PostCSS Build-Pipeline~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~Dark Mode Support~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~Responsive Design~~ ✅ **Abgeschlossen**
|
||||
- [ ] **CSS-Optimierung**: Purging ungenutzter Styles, Critical CSS
|
||||
- [ ] **Animation-System**: Micro-Interactions und Übergänge
|
||||
|
||||
## Leistung und Skalierung
|
||||
|
||||
- [ ] Optimierung der Datenbankabfragen
|
||||
- [ ] Caching-Strategie implementieren
|
||||
- [ ] Asynchrone Verarbeitung für zeitintensive Aufgaben
|
||||
- [ ] Docker-Container für einfache Bereitstellung
|
||||
- [ ] **CDN-Integration**: Für statische Assets
|
||||
- [ ] **Service Worker**: Für Offline-Funktionalität
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- [ ] Security Audit durchführen
|
||||
- [ ] Implementierung von CSRF-Schutz
|
||||
- [ ] Rate Limiting für API-Endpunkte
|
||||
- [ ] Zwei-Faktor-Authentifizierung
|
||||
- [ ] **Content Security Policy (CSP)**: Schutz vor XSS-Angriffen
|
||||
|
||||
## Entwickler-Erfahrung
|
||||
|
||||
### Dokumentation ✅
|
||||
|
||||
- [X] ~~UI-Komponenten-Dokumentation~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~Tailwind CSS Setup-Guide~~ ✅ **Abgeschlossen**
|
||||
- [ ] API-Dokumentation mit Swagger
|
||||
- [ ] Entwickler-Handbuch
|
||||
- [ ] Deployment-Guide
|
||||
|
||||
### Tooling
|
||||
|
||||
- [X] ~~PostCSS Build-System~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~NPM Scripts für Development~~ ✅ **Abgeschlossen**
|
||||
- [ ] **Hot Reload**: Für CSS und Templates
|
||||
- [ ] **Linting**: ESLint, Prettier, Flake8
|
||||
- [ ] **Testing**: Unit Tests, Integration Tests, E2E Tests
|
||||
|
||||
## Community und Beiträge
|
||||
|
||||
Wir freuen uns über Beiträge und Feedback zu dieser Roadmap. Wenn Sie Vorschläge haben oder an der Entwicklung teilnehmen möchten, erstellen Sie bitte einen Issue oder Pull Request im Repository.
|
||||
|
||||
### Aktuelle Prioritäten für Beiträge
|
||||
|
||||
1. **Testing**: Unit Tests für UI-Komponenten
|
||||
2. **Accessibility**: WCAG-konforme Verbesserungen
|
||||
3. **Performance**: Optimierung der CSS-Größe
|
||||
4. **Dokumentation**: Übersetzungen und Beispiele
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.1 (Dezember 2024)
|
||||
|
||||
- ✅ Vollständiges UI-Komponenten-System implementiert
|
||||
- ✅ Template-Helper für alle gängigen UI-Elemente
|
||||
- ✅ JavaScript-Utilities für interaktive Funktionen
|
||||
- ✅ PostCSS Build-Pipeline mit Tailwind CSS
|
||||
- ✅ Umfassende Dokumentation erstellt
|
||||
- ✅ Demo-Seite für alle Komponenten
|
||||
|
||||
### Version 1.0 (Juni 2023)
|
||||
|
||||
- ✅ Grundfunktionalitäten implementiert
|
||||
- ✅ Basis-UI mit Tailwind CSS
|
||||
- ✅ Dark Mode Support
|
||||
|
||||
---
|
||||
|
||||
*Zuletzt aktualisiert: Dezember 2024*
|
@ -1,57 +0,0 @@
|
||||
# MYP Platform - Python Dependencies
|
||||
# Basierend auf tatsächlich verwendeten Imports in app.py
|
||||
# Automatisch generiert am: 2025-05-29 19:41:49
|
||||
# Installiere mit: pip install -r requirements.txt
|
||||
|
||||
# ===== CORE FLASK FRAMEWORK =====
|
||||
# Direkt in app.py verwendet
|
||||
Flask==3.1.1
|
||||
Flask-Login==0.6.3
|
||||
Flask-WTF==1.2.1
|
||||
|
||||
# ===== DATENBANK =====
|
||||
# SQLAlchemy für Datenbankoperationen (models.py, app.py)
|
||||
SQLAlchemy==2.0.36
|
||||
|
||||
# ===== SICHERHEIT UND AUTHENTIFIZIERUNG =====
|
||||
# Werkzeug für Passwort-Hashing und Utilities (app.py)
|
||||
bcrypt==4.2.1
|
||||
cryptography==44.0.0
|
||||
Werkzeug==3.1.3
|
||||
|
||||
# ===== SMART PLUG STEUERUNG =====
|
||||
# PyP100 für TP-Link Tapo Smart Plugs (utils/job_scheduler.py)
|
||||
PyP100
|
||||
|
||||
# ===== RATE LIMITING UND CACHING =====
|
||||
# Redis für Rate Limiting (utils/rate_limiter.py) - optional
|
||||
redis==5.2.1
|
||||
|
||||
# ===== HTTP REQUESTS =====
|
||||
# Requests für HTTP-Anfragen (utils/queue_manager.py, utils/debug_drucker_erkennung.py)
|
||||
requests==2.32.3
|
||||
|
||||
# ===== TEMPLATE ENGINE =====
|
||||
# Jinja2 und MarkupSafe (automatisch mit Flask installiert, aber explizit für utils/template_helpers.py)
|
||||
MarkupSafe==3.0.2
|
||||
|
||||
# ===== SYSTEM MONITORING =====
|
||||
# psutil für System-Monitoring (utils/debug_utils.py, utils/debug_cli.py)
|
||||
psutil==6.1.1
|
||||
|
||||
# ===== ZUSÄTZLICHE CORE ABHÄNGIGKEITEN =====
|
||||
# Click für CLI-Kommandos (automatisch mit Flask)
|
||||
# Keine Core-Requirements
|
||||
|
||||
# ===== WINDOWS-SPEZIFISCHE ABHÄNGIGKEITEN =====
|
||||
# Nur für Windows-Systeme erforderlich
|
||||
# Keine Windows-Requirements
|
||||
|
||||
# ===== OPTIONAL: ENTWICKLUNG UND TESTING =====
|
||||
# Nur für Entwicklungsumgebung
|
||||
pytest==8.3.4; extra == "dev"
|
||||
pytest-cov==6.0.0; extra == "dev"
|
||||
|
||||
# ===== OPTIONAL: PRODUKTIONS-SERVER =====
|
||||
# Gunicorn für Produktionsumgebung
|
||||
gunicorn==23.0.0; extra == "prod"
|
@ -1,994 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ===================================================================
|
||||
# MYP Druckerverwaltung - Raspberry Pi Schnellstart Optimierung
|
||||
# Optimiert automatischen Start ohne Benutzeranmeldung
|
||||
# Für bereits installierte Systeme
|
||||
# ===================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# =========================== KONFIGURATION ===========================
|
||||
KIOSK_USER="kiosk"
|
||||
APP_USER="myp"
|
||||
APP_DIR="/opt/myp-druckerverwaltung"
|
||||
INSTALL_LOG="/var/log/myp-schnellstart.log"
|
||||
|
||||
# Farben für Ausgabe
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
NC='\033[0m'
|
||||
|
||||
# ========================== LOGGING-SYSTEM ==========================
|
||||
log() {
|
||||
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}" | tee -a "$INSTALL_LOG"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}[FEHLER] $1${NC}" | tee -a "$INSTALL_LOG"
|
||||
exit 1
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}[WARNUNG] $1${NC}" | tee -a "$INSTALL_LOG"
|
||||
}
|
||||
|
||||
info() {
|
||||
echo -e "${BLUE}[INFO] $1${NC}" | tee -a "$INSTALL_LOG"
|
||||
}
|
||||
|
||||
progress() {
|
||||
echo -e "${PURPLE}[FORTSCHRITT] $1${NC}" | tee -a "$INSTALL_LOG"
|
||||
}
|
||||
|
||||
# ========================== SYSTEM-CHECKS ==========================
|
||||
check_root() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
error "Dieses Skript muss als Root ausgeführt werden: sudo $0"
|
||||
fi
|
||||
}
|
||||
|
||||
check_system() {
|
||||
log "=== RASPBERRY PI SCHNELLSTART-OPTIMIERUNG ==="
|
||||
|
||||
# Prüfe ob Kiosk-Benutzer existiert
|
||||
if ! id "$KIOSK_USER" &>/dev/null; then
|
||||
# Kiosk-Benutzer erstellen falls nicht vorhanden
|
||||
progress "Erstelle Kiosk-Benutzer: $KIOSK_USER"
|
||||
if ! useradd -m -s /bin/bash "$KIOSK_USER" 2>/dev/null; then
|
||||
adduser --disabled-password --gecos "" "$KIOSK_USER" || error "Kann Kiosk-Benutzer nicht erstellen"
|
||||
fi
|
||||
# Kiosk-Benutzer zu Audio/Video-Gruppen hinzufügen
|
||||
usermod -aG audio,video,input "$KIOSK_USER" 2>/dev/null || true
|
||||
info "Kiosk-Benutzer erstellt"
|
||||
else
|
||||
info "Kiosk-Benutzer existiert bereits"
|
||||
fi
|
||||
|
||||
# Prüfe ob App-Verzeichnis existiert
|
||||
if [ ! -d "$APP_DIR" ]; then
|
||||
# App-Verzeichnis erstellen falls nicht vorhanden
|
||||
progress "Erstelle App-Verzeichnis: $APP_DIR"
|
||||
mkdir -p "$APP_DIR"
|
||||
chown -R "$APP_USER:$APP_USER" "$APP_DIR" 2>/dev/null || true
|
||||
info "App-Verzeichnis erstellt"
|
||||
else
|
||||
info "App-Verzeichnis existiert bereits"
|
||||
fi
|
||||
|
||||
# Prüfe ob App-Benutzer existiert
|
||||
if ! id "$APP_USER" &>/dev/null; then
|
||||
# App-Benutzer erstellen falls nicht vorhanden
|
||||
progress "Erstelle App-Benutzer: $APP_USER"
|
||||
if ! useradd -m -s /bin/bash "$APP_USER" 2>/dev/null; then
|
||||
adduser --disabled-password --gecos "" "$APP_USER" || error "Kann App-Benutzer nicht erstellen"
|
||||
fi
|
||||
usermod -aG sudo "$APP_USER" 2>/dev/null || true
|
||||
info "App-Benutzer erstellt"
|
||||
else
|
||||
info "App-Benutzer existiert bereits"
|
||||
fi
|
||||
|
||||
info "System-Checks erfolgreich"
|
||||
}
|
||||
|
||||
# ========================== ESSENZIELLE PAKETE INSTALLIEREN ==========================
|
||||
install_essential_packages() {
|
||||
log "=== ESSENZIELLE PAKETE INSTALLIEREN ==="
|
||||
|
||||
progress "Aktualisiere Paketlisten..."
|
||||
apt-get update -y || warning "APT Update teilweise fehlgeschlagen"
|
||||
|
||||
progress "Installiere essenzielle Pakete..."
|
||||
apt-get install -y \
|
||||
curl wget git unzip \
|
||||
python3 python3-pip python3-dev \
|
||||
build-essential libssl-dev libffi-dev \
|
||||
sqlite3 nginx supervisor \
|
||||
xorg xinit openbox \
|
||||
xserver-xorg-video-all \
|
||||
x11-xserver-utils xdotool unclutter \
|
||||
lightdm lightdm-gtk-greeter \
|
||||
pulseaudio alsa-utils \
|
||||
fonts-liberation fonts-dejavu \
|
||||
ca-certificates apt-transport-https \
|
||||
|| warning "Paket-Installation teilweise fehlgeschlagen"
|
||||
|
||||
log "✅ Essenzielle Pakete installiert"
|
||||
}
|
||||
|
||||
# ========================== BOOT-OPTIMIERUNG ==========================
|
||||
optimize_boot() {
|
||||
log "=== BOOT-OPTIMIERUNG ==="
|
||||
|
||||
progress "Optimiere Raspberry Pi Boot-Parameter..."
|
||||
|
||||
# Raspberry Pi Boot-Konfiguration
|
||||
if [ -f "/boot/config.txt" ]; then
|
||||
# GPU Memory für bessere Performance
|
||||
if ! grep -q "gpu_mem=" /boot/config.txt; then
|
||||
echo "gpu_mem=128" >> /boot/config.txt
|
||||
info "GPU Memory auf 128MB gesetzt"
|
||||
fi
|
||||
|
||||
# Disable Boot-Splash für schnelleren Start
|
||||
if ! grep -q "disable_splash=" /boot/config.txt; then
|
||||
echo "disable_splash=1" >> /boot/config.txt
|
||||
info "Boot-Splash deaktiviert"
|
||||
fi
|
||||
|
||||
# Boot-Delay reduzieren
|
||||
if ! grep -q "boot_delay=" /boot/config.txt; then
|
||||
echo "boot_delay=0" >> /boot/config.txt
|
||||
info "Boot-Delay auf 0 gesetzt"
|
||||
fi
|
||||
|
||||
# HDMI-Hotplug für bessere Display-Erkennung
|
||||
if ! grep -q "hdmi_force_hotplug=" /boot/config.txt; then
|
||||
echo "hdmi_force_hotplug=1" >> /boot/config.txt
|
||||
info "HDMI-Hotplug aktiviert"
|
||||
fi
|
||||
|
||||
# Disable Rainbow-Splash
|
||||
if ! grep -q "disable_overscan=" /boot/config.txt; then
|
||||
echo "disable_overscan=1" >> /boot/config.txt
|
||||
info "Overscan deaktiviert"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Kernel-Parameter optimieren
|
||||
if [ -f "/boot/cmdline.txt" ]; then
|
||||
# Backup erstellen
|
||||
cp /boot/cmdline.txt /boot/cmdline.txt.backup
|
||||
|
||||
# Entferne Boot-Splash und optimiere
|
||||
sed -i 's/splash//g' /boot/cmdline.txt
|
||||
|
||||
# Füge Performance-Parameter hinzu
|
||||
if ! grep -q "quiet" /boot/cmdline.txt; then
|
||||
sed -i 's/$/ quiet/' /boot/cmdline.txt
|
||||
fi
|
||||
if ! grep -q "loglevel=3" /boot/cmdline.txt; then
|
||||
sed -i 's/$/ loglevel=3/' /boot/cmdline.txt
|
||||
fi
|
||||
if ! grep -q "logo.nologo" /boot/cmdline.txt; then
|
||||
sed -i 's/$/ logo.nologo/' /boot/cmdline.txt
|
||||
fi
|
||||
if ! grep -q "vt.global_cursor_default=0" /boot/cmdline.txt; then
|
||||
sed -i 's/$/ vt.global_cursor_default=0/' /boot/cmdline.txt
|
||||
fi
|
||||
|
||||
info "Kernel-Parameter optimiert"
|
||||
fi
|
||||
|
||||
log "✅ Boot-Optimierung abgeschlossen"
|
||||
}
|
||||
|
||||
# ========================== AUTOLOGIN VERSTÄRKEN ==========================
|
||||
strengthen_autologin() {
|
||||
log "=== AUTOLOGIN-VERSTÄRKUNG ==="
|
||||
|
||||
progress "Verstärke automatischen Login..."
|
||||
|
||||
# Sicherstellen dass graphical.target Standard ist
|
||||
systemctl set-default graphical.target
|
||||
info "Graphical.target als Standard gesetzt"
|
||||
|
||||
# Getty Auto-Login verstärken
|
||||
mkdir -p "/etc/systemd/system/getty@tty1.service.d"
|
||||
cat > "/etc/systemd/system/getty@tty1.service.d/autologin.conf" << EOF
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin $KIOSK_USER --noclear %I \$TERM
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
EOF
|
||||
|
||||
# Getty Service aktivieren
|
||||
systemctl enable getty@tty1.service
|
||||
info "Getty Auto-Login konfiguriert"
|
||||
|
||||
# LightDM Auto-Login verstärken
|
||||
if [ -f "/etc/lightdm/lightdm.conf" ]; then
|
||||
# Backup erstellen
|
||||
cp /etc/lightdm/lightdm.conf /etc/lightdm/lightdm.conf.backup
|
||||
|
||||
# Neue Konfiguration
|
||||
cat > "/etc/lightdm/lightdm.conf" << EOF
|
||||
[Seat:*]
|
||||
# Automatischer Login für Kiosk-Benutzer
|
||||
autologin-user=$KIOSK_USER
|
||||
autologin-user-timeout=0
|
||||
autologin-session=openbox
|
||||
user-session=openbox
|
||||
session-wrapper=/etc/X11/Xsession
|
||||
greeter-session=lightdm-gtk-greeter
|
||||
allow-guest=false
|
||||
# Kein Benutzer-Wechsel möglich
|
||||
greeter-hide-users=true
|
||||
greeter-show-manual-login=false
|
||||
# Automatischer Start ohne Verzögerung
|
||||
autologin-in-background=false
|
||||
# Session-Setup
|
||||
session-setup-script=/usr/share/lightdm/setup-kiosk-session.sh
|
||||
|
||||
[SeatDefaults]
|
||||
# Zusätzliche Sicherheitseinstellungen
|
||||
autologin-user=$KIOSK_USER
|
||||
autologin-user-timeout=0
|
||||
autologin-session=openbox
|
||||
greeter-hide-users=true
|
||||
greeter-show-manual-login=false
|
||||
allow-user-switching=false
|
||||
EOF
|
||||
info "LightDM Auto-Login verstärkt"
|
||||
fi
|
||||
|
||||
# LightDM Service-Override
|
||||
mkdir -p "/etc/systemd/system/lightdm.service.d"
|
||||
cat > "/etc/systemd/system/lightdm.service.d/autologin-override.conf" << EOF
|
||||
[Unit]
|
||||
After=multi-user.target network.target myp-druckerverwaltung.service
|
||||
Wants=myp-druckerverwaltung.service
|
||||
|
||||
[Service]
|
||||
# Automatischer Restart bei Fehlern
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
# Umgebungsvariablen für Kiosk
|
||||
Environment=DISPLAY=:0
|
||||
Environment=KIOSK_MODE=1
|
||||
# Verzögerung für Backend-Start
|
||||
ExecStartPre=/bin/bash -c 'for i in {1..30}; do if curl -s http://localhost:5000 >/dev/null 2>&1; then break; fi; sleep 2; done'
|
||||
EOF
|
||||
|
||||
systemctl enable lightdm.service
|
||||
info "LightDM Service-Override konfiguriert"
|
||||
|
||||
log "✅ Autologin-Verstärkung abgeschlossen"
|
||||
}
|
||||
|
||||
# ========================== KIOSK-BENUTZER OPTIMIERUNG ==========================
|
||||
optimize_kiosk_user() {
|
||||
log "=== KIOSK-BENUTZER OPTIMIERUNG ==="
|
||||
|
||||
KIOSK_HOME="/home/$KIOSK_USER"
|
||||
|
||||
progress "Optimiere Kiosk-Benutzer Autostart..."
|
||||
|
||||
# Verstärkte .bashrc
|
||||
cat >> "$KIOSK_HOME/.bashrc" << 'EOF'
|
||||
|
||||
# ===== VERSTÄRKTER KIOSK AUTOSTART =====
|
||||
if [ -z "$SSH_CLIENT" ] && [ -z "$SSH_TTY" ] && [ -z "$KIOSK_STARTED" ]; then
|
||||
export KIOSK_STARTED=1
|
||||
|
||||
# Logge Autostart-Versuch
|
||||
echo "$(date): Bashrc Autostart-Versuch auf $(tty)" >> /var/log/kiosk-autostart.log
|
||||
|
||||
# Prüfe ob wir auf tty1 sind und X noch nicht läuft
|
||||
if [ "$(tty)" = "/dev/tty1" ] && [ -z "$DISPLAY" ]; then
|
||||
echo "$(date): Starte X-Session automatisch via bashrc" >> /var/log/kiosk-autostart.log
|
||||
exec startx
|
||||
fi
|
||||
|
||||
# Falls X läuft aber Kiosk-App nicht, starte sie
|
||||
if [ -n "$DISPLAY" ] && ! pgrep -f "chromium.*kiosk" > /dev/null; then
|
||||
echo "$(date): Starte Kiosk-Anwendung via bashrc" >> /var/log/kiosk-autostart.log
|
||||
exec $HOME/start-kiosk.sh
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
|
||||
# Verstärkte .profile
|
||||
cat >> "$KIOSK_HOME/.profile" << 'EOF'
|
||||
|
||||
# ===== VERSTÄRKTER KIOSK AUTOSTART (PROFILE) =====
|
||||
if [ -z "$SSH_CLIENT" ] && [ -z "$SSH_TTY" ] && [ -z "$KIOSK_STARTED" ]; then
|
||||
export KIOSK_STARTED=1
|
||||
|
||||
# Logge Profile-Autostart
|
||||
echo "$(date): Profile Autostart-Versuch auf $(tty)" >> /var/log/kiosk-autostart.log
|
||||
|
||||
# Starte X-Session falls nicht vorhanden
|
||||
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
|
||||
echo "$(date): Starte X-Session via profile" >> /var/log/kiosk-autostart.log
|
||||
exec startx
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
|
||||
# Optimierte .xinitrc
|
||||
cat > "$KIOSK_HOME/.xinitrc" << EOF
|
||||
#!/bin/bash
|
||||
# Optimierte Xinit-Konfiguration für Kiosk-Modus
|
||||
|
||||
# Logge X-Start
|
||||
echo "\$(date): X-Session gestartet via xinitrc" >> /var/log/kiosk-autostart.log
|
||||
|
||||
# Export Display
|
||||
export DISPLAY=:0
|
||||
|
||||
# Session-Setup
|
||||
xset s off
|
||||
xset s noblank
|
||||
xset s noexpose
|
||||
xset -dpms
|
||||
|
||||
# Verstecke Mauszeiger
|
||||
unclutter -idle 0.5 -root &
|
||||
|
||||
# Warte kurz auf System-Stabilisierung
|
||||
sleep 3
|
||||
|
||||
# Starte Openbox
|
||||
exec openbox-session
|
||||
EOF
|
||||
|
||||
chmod +x "$KIOSK_HOME/.xinitrc"
|
||||
|
||||
# Desktop Autostart verstärken
|
||||
mkdir -p "$KIOSK_HOME/.config/autostart"
|
||||
cat > "$KIOSK_HOME/.config/autostart/kiosk-app.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=MYP Kiosk Application
|
||||
Comment=Startet die MYP Kiosk-Anwendung automatisch
|
||||
Exec=$KIOSK_HOME/start-kiosk.sh
|
||||
Hidden=false
|
||||
NoDisplay=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
StartupNotify=false
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
# Berechtigungen setzen
|
||||
chown -R "$KIOSK_USER:$KIOSK_USER" "$KIOSK_HOME/.config"
|
||||
chown "$KIOSK_USER:$KIOSK_USER" "$KIOSK_HOME/.bashrc"
|
||||
chown "$KIOSK_USER:$KIOSK_USER" "$KIOSK_HOME/.profile"
|
||||
chown "$KIOSK_USER:$KIOSK_USER" "$KIOSK_HOME/.xinitrc"
|
||||
|
||||
info "Kiosk-Benutzer Autostart optimiert"
|
||||
|
||||
log "✅ Kiosk-Benutzer Optimierung abgeschlossen"
|
||||
}
|
||||
|
||||
# ========================== WATCHDOG VERSTÄRKEN ==========================
|
||||
strengthen_watchdog() {
|
||||
log "=== WATCHDOG-VERSTÄRKUNG ==="
|
||||
|
||||
progress "Verstärke Kiosk-Überwachung..."
|
||||
|
||||
# Verstärkter Kiosk-Watchdog Service
|
||||
cat > "/etc/systemd/system/kiosk-watchdog-enhanced.service" << EOF
|
||||
[Unit]
|
||||
Description=Enhanced Kiosk Watchdog Service
|
||||
After=multi-user.target lightdm.service
|
||||
Wants=lightdm.service myp-druckerverwaltung.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/bin/bash -c '
|
||||
while true; do
|
||||
# Prüfe Backend-Service
|
||||
if ! systemctl is-active --quiet myp-druckerverwaltung; then
|
||||
echo "\$(date): Backend-Service nicht aktiv - starte neu" >> /var/log/kiosk-watchdog-enhanced.log
|
||||
systemctl start myp-druckerverwaltung
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Prüfe Backend-Erreichbarkeit
|
||||
if ! curl -s http://localhost:5000 >/dev/null 2>&1; then
|
||||
echo "\$(date): Backend nicht erreichbar - starte Service neu" >> /var/log/kiosk-watchdog-enhanced.log
|
||||
systemctl restart myp-druckerverwaltung
|
||||
sleep 10
|
||||
fi
|
||||
|
||||
# Prüfe LightDM
|
||||
if ! systemctl is-active --quiet lightdm; then
|
||||
echo "\$(date): LightDM nicht aktiv - starte neu" >> /var/log/kiosk-watchdog-enhanced.log
|
||||
systemctl start lightdm
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Prüfe Kiosk-Benutzer Session
|
||||
if ! pgrep -u $KIOSK_USER > /dev/null; then
|
||||
echo "\$(date): Kiosk-Benutzer nicht angemeldet - starte LightDM neu" >> /var/log/kiosk-watchdog-enhanced.log
|
||||
systemctl restart lightdm
|
||||
sleep 10
|
||||
fi
|
||||
|
||||
# Prüfe Chromium Kiosk-Prozess
|
||||
if ! pgrep -u $KIOSK_USER -f "chromium.*kiosk" > /dev/null; then
|
||||
echo "\$(date): Chromium-Kiosk nicht gefunden - starte Kiosk-Session neu" >> /var/log/kiosk-watchdog-enhanced.log
|
||||
# Versuche Kiosk-Neustart als Kiosk-Benutzer
|
||||
sudo -u $KIOSK_USER DISPLAY=:0 /home/$KIOSK_USER/start-kiosk.sh &
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Prüfe X-Server
|
||||
if ! pgrep -f "X.*:0" > /dev/null; then
|
||||
echo "\$(date): X-Server nicht gefunden - starte LightDM neu" >> /var/log/kiosk-watchdog-enhanced.log
|
||||
systemctl restart lightdm
|
||||
sleep 10
|
||||
fi
|
||||
|
||||
sleep 20
|
||||
done
|
||||
'
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Alten Watchdog deaktivieren und neuen aktivieren
|
||||
systemctl disable kiosk-watchdog.service 2>/dev/null || true
|
||||
systemctl enable kiosk-watchdog-enhanced.service
|
||||
|
||||
# Cron-Watchdog verstärken
|
||||
cat > "/etc/cron.d/kiosk-watchdog-enhanced" << EOF
|
||||
# Verstärkter Kiosk-Watchdog: Prüft alle 2 Minuten
|
||||
*/2 * * * * $KIOSK_USER /bin/bash -c 'if ! pgrep -f "chromium.*kiosk" > /dev/null; then echo "\$(date): Cron-Watchdog startet Kiosk neu" >> /var/log/kiosk-cron-watchdog.log; DISPLAY=:0 $HOME/start-kiosk.sh & fi'
|
||||
|
||||
# System-Watchdog: Prüft Services alle 5 Minuten
|
||||
*/5 * * * * root /bin/bash -c 'if ! systemctl is-active --quiet lightdm; then echo "\$(date): Cron startet LightDM neu" >> /var/log/system-cron-watchdog.log; systemctl start lightdm; fi'
|
||||
EOF
|
||||
|
||||
# RC.Local verstärken
|
||||
cat > "/etc/rc.local" << EOF
|
||||
#!/bin/bash
|
||||
# Verstärkter rc.local - Kiosk-Fallback
|
||||
|
||||
# Logge Start
|
||||
echo "\$(date): rc.local gestartet" >> /var/log/kiosk-fallback.log
|
||||
|
||||
# Warte auf System-Initialisierung
|
||||
sleep 20
|
||||
|
||||
# Starte Backend-Service falls nicht läuft
|
||||
if ! systemctl is-active --quiet myp-druckerverwaltung; then
|
||||
echo "\$(date): Starte Backend-Service" >> /var/log/kiosk-fallback.log
|
||||
systemctl start myp-druckerverwaltung
|
||||
sleep 10
|
||||
fi
|
||||
|
||||
# Warte auf Backend-Verfügbarkeit
|
||||
for i in {1..30}; do
|
||||
if curl -s http://localhost:5000 >/dev/null 2>&1; then
|
||||
echo "\$(date): Backend verfügbar nach \$i Versuchen" >> /var/log/kiosk-fallback.log
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Starte LightDM falls nicht läuft
|
||||
if ! systemctl is-active --quiet lightdm; then
|
||||
echo "\$(date): Starte LightDM" >> /var/log/kiosk-fallback.log
|
||||
systemctl start lightdm
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Prüfe nach 30 Sekunden ob Kiosk-Benutzer angemeldet ist
|
||||
sleep 30
|
||||
if ! pgrep -u $KIOSK_USER > /dev/null; then
|
||||
echo "\$(date): Kiosk-Benutzer nicht angemeldet - starte LightDM neu" >> /var/log/kiosk-fallback.log
|
||||
systemctl restart lightdm
|
||||
fi
|
||||
|
||||
echo "\$(date): rc.local Kiosk-Fallback abgeschlossen" >> /var/log/kiosk-fallback.log
|
||||
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
chmod +x "/etc/rc.local"
|
||||
|
||||
info "Watchdog-Services verstärkt"
|
||||
|
||||
log "✅ Watchdog-Verstärkung abgeschlossen"
|
||||
}
|
||||
|
||||
# ========================== SYSTEM-SERVICES OPTIMIEREN ==========================
|
||||
optimize_services() {
|
||||
log "=== SERVICE-OPTIMIERUNG ==="
|
||||
|
||||
progress "Optimiere System-Services für schnelleren Start..."
|
||||
|
||||
# Deaktiviere unnötige Services
|
||||
DISABLE_SERVICES=(
|
||||
"bluetooth"
|
||||
"hciuart"
|
||||
"triggerhappy"
|
||||
"avahi-daemon"
|
||||
"cups"
|
||||
"cups-browsed"
|
||||
"ModemManager"
|
||||
"wpa_supplicant"
|
||||
)
|
||||
|
||||
for service in "${DISABLE_SERVICES[@]}"; do
|
||||
if systemctl is-enabled --quiet "$service" 2>/dev/null; then
|
||||
systemctl disable "$service" 2>/dev/null || true
|
||||
info "Service '$service' deaktiviert"
|
||||
fi
|
||||
done
|
||||
|
||||
# Optimiere wichtige Services
|
||||
systemctl enable myp-druckerverwaltung
|
||||
systemctl enable lightdm
|
||||
systemctl enable kiosk-watchdog-enhanced
|
||||
|
||||
# Systemd-Daemon neu laden
|
||||
systemctl daemon-reload
|
||||
|
||||
info "Services optimiert"
|
||||
|
||||
log "✅ Service-Optimierung abgeschlossen"
|
||||
}
|
||||
|
||||
# ========================== SYSTEM-PARAMETER OPTIMIEREN ==========================
|
||||
optimize_system_parameters() {
|
||||
log "=== SYSTEM-PARAMETER OPTIMIERUNG ==="
|
||||
|
||||
progress "Optimiere System-Parameter..."
|
||||
|
||||
# Systemd-Logind für Kiosk optimieren
|
||||
mkdir -p "/etc/systemd/logind.conf.d"
|
||||
cat > "/etc/systemd/logind.conf.d/kiosk.conf" << EOF
|
||||
[Login]
|
||||
# Verhindere dass System bei Inaktivität heruntergefahren wird
|
||||
IdleAction=ignore
|
||||
IdleActionSec=infinity
|
||||
|
||||
# Verhindere Suspend/Hibernate
|
||||
HandlePowerKey=ignore
|
||||
HandleSuspendKey=ignore
|
||||
HandleHibernateKey=ignore
|
||||
HandleLidSwitch=ignore
|
||||
|
||||
# Session-Einstellungen für Kiosk
|
||||
KillUserProcesses=no
|
||||
UserStopDelaySec=10
|
||||
|
||||
# Automatic VT allocation
|
||||
ReserveVT=1
|
||||
EOF
|
||||
|
||||
# Kernel-Parameter für bessere Performance
|
||||
cat > "/etc/sysctl.d/99-kiosk-performance.conf" << EOF
|
||||
# Kiosk-Performance Optimierungen
|
||||
vm.swappiness=10
|
||||
vm.dirty_ratio=15
|
||||
vm.dirty_background_ratio=5
|
||||
kernel.sched_autogroup_enabled=0
|
||||
EOF
|
||||
|
||||
# Tmpfs für bessere Performance
|
||||
if ! grep -q "tmpfs.*tmp" /etc/fstab; then
|
||||
echo "tmpfs /tmp tmpfs defaults,noatime,nosuid,size=100m 0 0" >> /etc/fstab
|
||||
info "Tmpfs für /tmp konfiguriert"
|
||||
fi
|
||||
|
||||
info "System-Parameter optimiert"
|
||||
|
||||
log "✅ System-Parameter Optimierung abgeschlossen"
|
||||
}
|
||||
|
||||
# ========================== WARTUNGSTOOLS ERSTELLEN ==========================
|
||||
create_maintenance_tools() {
|
||||
log "=== WARTUNGSTOOLS ERSTELLEN ==="
|
||||
|
||||
progress "Erstelle Wartungs-Skript..."
|
||||
|
||||
# Wartungsskript
|
||||
cat > "/usr/local/bin/myp-maintenance" << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo "Starte alle MYP-Services..."
|
||||
systemctl start myp-druckerverwaltung
|
||||
systemctl start nginx
|
||||
systemctl start lightdm
|
||||
echo "Services gestartet."
|
||||
;;
|
||||
stop)
|
||||
echo "Stoppe alle MYP-Services..."
|
||||
systemctl stop lightdm
|
||||
systemctl stop nginx
|
||||
systemctl stop myp-druckerverwaltung
|
||||
echo "Services gestoppt."
|
||||
;;
|
||||
restart)
|
||||
echo "Starte alle MYP-Services neu..."
|
||||
systemctl restart myp-druckerverwaltung
|
||||
sleep 3
|
||||
systemctl restart nginx
|
||||
systemctl restart lightdm
|
||||
echo "Services neugestartet."
|
||||
;;
|
||||
status)
|
||||
echo "=== MYP SYSTEM STATUS ==="
|
||||
echo
|
||||
echo "📱 Anwendung:"
|
||||
systemctl status myp-druckerverwaltung --no-pager -l
|
||||
echo
|
||||
echo "🌐 Nginx Proxy:"
|
||||
systemctl status nginx --no-pager -l
|
||||
echo
|
||||
echo "🖥️ Display Manager:"
|
||||
systemctl status lightdm --no-pager -l
|
||||
echo
|
||||
echo "👤 Kiosk-Benutzer-Sessions:"
|
||||
who | grep kiosk || echo "Kein Kiosk-Benutzer angemeldet"
|
||||
echo
|
||||
echo "🌐 Anwendung erreichbar:"
|
||||
if curl -s http://localhost:5000 > /dev/null; then
|
||||
echo "✅ http://localhost:5000 erreichbar"
|
||||
else
|
||||
echo "❌ http://localhost:5000 NICHT erreichbar"
|
||||
fi
|
||||
;;
|
||||
logs)
|
||||
echo "=== ANWENDUNGS-LOGS (Strg+C zum Beenden) ==="
|
||||
journalctl -u myp-druckerverwaltung -f
|
||||
;;
|
||||
kiosk-logs)
|
||||
echo "=== KIOSK-LOGS (Strg+C zum Beenden) ==="
|
||||
echo "LightDM-Logs:"
|
||||
journalctl -u lightdm -f &
|
||||
echo "Session-Logs:"
|
||||
tail -f /var/log/kiosk-session.log 2>/dev/null &
|
||||
wait
|
||||
;;
|
||||
exit-kiosk)
|
||||
echo "🔐 KIOSK-MODUS BEENDEN"
|
||||
echo "WARNUNG: Stoppt den Kiosk und aktiviert Wartungsmodus!"
|
||||
echo "Passwort erforderlich für Sicherheit."
|
||||
read -s -p "Kiosk-Passwort: " password
|
||||
echo
|
||||
if [ "$password" = "744563017196A" ]; then
|
||||
echo "✅ Passwort korrekt - beende Kiosk-Modus..."
|
||||
systemctl stop lightdm
|
||||
systemctl enable ssh
|
||||
systemctl start ssh
|
||||
echo "🔧 Wartungsmodus aktiviert:"
|
||||
echo " • Kiosk gestoppt"
|
||||
echo " • SSH aktiviert"
|
||||
echo " • Console verfügbar"
|
||||
echo "Kiosk-Neustart mit: myp-maintenance start"
|
||||
else
|
||||
echo "❌ Falsches Passwort! Kiosk bleibt aktiv."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
enable-ssh)
|
||||
echo "Aktiviere SSH für Wartung..."
|
||||
systemctl enable ssh
|
||||
systemctl start ssh
|
||||
echo "✅ SSH aktiviert für Remote-Wartung"
|
||||
echo "SSH-Status: $(systemctl is-active ssh)"
|
||||
echo "IP-Adresse: $(hostname -I | awk '{print $1}')"
|
||||
;;
|
||||
disable-ssh)
|
||||
echo "Deaktiviere SSH für Sicherheit..."
|
||||
systemctl stop ssh
|
||||
systemctl disable ssh
|
||||
echo "✅ SSH deaktiviert"
|
||||
;;
|
||||
check-health)
|
||||
echo "=== SYSTEM-GESUNDHEITSCHECK ==="
|
||||
echo
|
||||
# Services-Check
|
||||
echo "📋 Service-Status:"
|
||||
for service in myp-druckerverwaltung nginx lightdm; do
|
||||
if systemctl is-active --quiet $service; then
|
||||
echo " ✅ $service: aktiv"
|
||||
else
|
||||
echo " ❌ $service: INAKTIV"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
# Netzwerk-Check
|
||||
echo "🌐 Netzwerk-Status:"
|
||||
if curl -s http://localhost:5000 > /dev/null; then
|
||||
echo " ✅ Anwendung erreichbar"
|
||||
else
|
||||
echo " ❌ Anwendung NICHT erreichbar"
|
||||
fi
|
||||
|
||||
echo
|
||||
# Kiosk-Check
|
||||
echo "🖥️ Kiosk-Status:"
|
||||
if pgrep -u kiosk > /dev/null; then
|
||||
echo " ✅ Kiosk-Benutzer angemeldet"
|
||||
else
|
||||
echo " ❌ Kiosk-Benutzer NICHT angemeldet"
|
||||
fi
|
||||
|
||||
if pgrep -f "chromium.*kiosk" > /dev/null; then
|
||||
echo " ✅ Chromium-Kiosk läuft"
|
||||
else
|
||||
echo " ❌ Chromium-Kiosk läuft NICHT"
|
||||
fi
|
||||
echo
|
||||
;;
|
||||
*)
|
||||
echo "MYP Druckerverwaltung - Wartungstool"
|
||||
echo
|
||||
echo "VERWENDUNG: $0 BEFEHL"
|
||||
echo
|
||||
echo "SERVICE-MANAGEMENT:"
|
||||
echo " start Alle Services starten"
|
||||
echo " stop Alle Services stoppen"
|
||||
echo " restart Alle Services neustarten"
|
||||
echo " status Detaillierter Status aller Services"
|
||||
echo
|
||||
echo "LOGS & MONITORING:"
|
||||
echo " logs Live Anwendungs-Logs anzeigen"
|
||||
echo " kiosk-logs Live Kiosk-Logs anzeigen"
|
||||
echo " check-health System-Gesundheitscheck"
|
||||
echo
|
||||
echo "KIOSK-KONTROLLE:"
|
||||
echo " exit-kiosk Kiosk beenden (Passwort: 744563017196A)"
|
||||
echo " enable-ssh SSH für Remote-Wartung aktivieren"
|
||||
echo " disable-ssh SSH wieder deaktivieren"
|
||||
echo
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
|
||||
chmod +x "/usr/local/bin/myp-maintenance"
|
||||
|
||||
# Kiosk-Starter-Skript
|
||||
progress "Erstelle Kiosk-Starter-Skript..."
|
||||
|
||||
KIOSK_HOME="/home/$KIOSK_USER"
|
||||
|
||||
cat > "$KIOSK_HOME/start-kiosk.sh" << EOF
|
||||
#!/bin/bash
|
||||
# MYP Kiosk-Starter
|
||||
|
||||
export DISPLAY=:0
|
||||
|
||||
# Logging für Debugging
|
||||
exec > >(tee -a /var/log/kiosk-session.log) 2>&1
|
||||
echo "\$(date): Kiosk-Session gestartet für Benutzer $KIOSK_USER"
|
||||
|
||||
# Bildschirmschoner deaktivieren
|
||||
xset s off
|
||||
xset s noblank
|
||||
xset s noexpose
|
||||
xset -dpms
|
||||
|
||||
# Mauszeiger verstecken
|
||||
unclutter -idle 0.5 -root &
|
||||
|
||||
# Warte auf Anwendung
|
||||
echo "Warte auf MYP-Anwendung..."
|
||||
WAIT_COUNT=0
|
||||
while ! curl -s http://localhost:5000 > /dev/null; do
|
||||
echo "Warte auf MYP-Anwendung... (\$WAIT_COUNT/30)"
|
||||
sleep 2
|
||||
WAIT_COUNT=\$((WAIT_COUNT + 1))
|
||||
if [ \$WAIT_COUNT -gt 30 ]; then
|
||||
echo "FEHLER: MYP-Anwendung nach 60s nicht erreichbar!"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Starte Chromium im Kiosk-Modus
|
||||
if command -v chromium &> /dev/null; then
|
||||
CHROMIUM_BIN="chromium"
|
||||
elif command -v chromium-browser &> /dev/null; then
|
||||
CHROMIUM_BIN="chromium-browser"
|
||||
else
|
||||
echo "Chromium nicht gefunden! Versuche alternativ Firefox..."
|
||||
if command -v firefox &> /dev/null; then
|
||||
firefox --kiosk http://localhost:5000
|
||||
exit 0
|
||||
else
|
||||
echo "Kein unterstützter Browser gefunden!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Starte \$CHROMIUM_BIN im Kiosk-Modus..."
|
||||
|
||||
\$CHROMIUM_BIN --kiosk --no-sandbox --disable-infobars --disable-session-crashed-bubble http://localhost:5000
|
||||
|
||||
echo "\$(date): Kiosk-Session beendet"
|
||||
EOF
|
||||
|
||||
chmod +x "$KIOSK_HOME/start-kiosk.sh"
|
||||
chown "$KIOSK_USER:$KIOSK_USER" "$KIOSK_HOME/start-kiosk.sh"
|
||||
|
||||
# Erstelle leere Log-Dateien
|
||||
touch /var/log/kiosk-session.log
|
||||
touch /var/log/kiosk-watchdog.log
|
||||
touch /var/log/kiosk-autostart.log
|
||||
touch /var/log/kiosk-fallback.log
|
||||
chmod 666 /var/log/kiosk-session.log
|
||||
chmod 666 /var/log/kiosk-watchdog.log
|
||||
chmod 666 /var/log/kiosk-autostart.log
|
||||
chmod 666 /var/log/kiosk-fallback.log
|
||||
|
||||
log "✅ Wartungstools erstellt"
|
||||
}
|
||||
|
||||
# ========================== SERVICE-DATEIEN ERSTELLEN ==========================
|
||||
create_service_files() {
|
||||
log "=== SERVICE-DATEIEN ERSTELLEN ==="
|
||||
|
||||
progress "Erstelle myp-druckerverwaltung.service..."
|
||||
|
||||
# Service-Datei für die Hauptanwendung
|
||||
cat > "/etc/systemd/system/myp-druckerverwaltung.service" << EOF
|
||||
[Unit]
|
||||
Description=MYP Druckerverwaltung Flask Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$APP_USER
|
||||
Group=$APP_USER
|
||||
WorkingDirectory=$APP_DIR
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
Environment=PYTHONPATH=$APP_DIR
|
||||
ExecStart=/usr/bin/python3 $APP_DIR/app.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Erstelle eine einfache app.py falls keine vorhanden ist
|
||||
if [ ! -f "$APP_DIR/app.py" ]; then
|
||||
progress "Erstelle einfache app.py als Platzhalter..."
|
||||
mkdir -p "$APP_DIR"
|
||||
cat > "$APP_DIR/app.py" << 'EOF'
|
||||
#!/usr/bin/python3
|
||||
# Einfache Flask-Anwendung als Platzhalter
|
||||
|
||||
from flask import Flask, render_template_string
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
return render_template_string("""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MYP Druckerverwaltung</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background: #f5f5f5; }
|
||||
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
|
||||
h1 { color: #333; }
|
||||
.box { background: white; border-radius: 5px; padding: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>MYP Druckerverwaltung</h1>
|
||||
<div class="box">
|
||||
<h2>System erfolgreich gestartet</h2>
|
||||
<p>Die MYP Druckerverwaltung läuft im Kiosk-Modus.</p>
|
||||
<p>Sie können diese Anwendung nun durch Ihre eigentliche Anwendung ersetzen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000)
|
||||
EOF
|
||||
chmod +x "$APP_DIR/app.py"
|
||||
chown "$APP_USER:$APP_USER" "$APP_DIR/app.py"
|
||||
|
||||
# Installiere Flask falls nicht vorhanden
|
||||
if ! python3 -c "import flask" &>/dev/null; then
|
||||
progress "Installiere Flask..."
|
||||
pip3 install flask --break-system-packages || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Erstelle Templates-Verzeichnis falls nicht vorhanden
|
||||
if [ ! -d "$APP_DIR/templates" ]; then
|
||||
mkdir -p "$APP_DIR/templates"
|
||||
chown "$APP_USER:$APP_USER" "$APP_DIR/templates"
|
||||
fi
|
||||
|
||||
# Erstelle Static-Verzeichnis falls nicht vorhanden
|
||||
if [ ! -d "$APP_DIR/static" ]; then
|
||||
mkdir -p "$APP_DIR/static"
|
||||
chown "$APP_USER:$APP_USER" "$APP_DIR/static"
|
||||
fi
|
||||
|
||||
# Systemd neu laden
|
||||
systemctl daemon-reload
|
||||
|
||||
log "✅ Service-Dateien erstellt"
|
||||
}
|
||||
|
||||
# ========================== HAUPTFUNKTION ==========================
|
||||
main() {
|
||||
log "=== RASPBERRY PI SCHNELLSTART-OPTIMIERUNG GESTARTET ==="
|
||||
|
||||
check_root
|
||||
check_system
|
||||
install_essential_packages
|
||||
create_service_files
|
||||
optimize_boot
|
||||
strengthen_autologin
|
||||
optimize_kiosk_user
|
||||
create_maintenance_tools
|
||||
strengthen_watchdog
|
||||
optimize_services
|
||||
optimize_system_parameters
|
||||
|
||||
log "=== OPTIMIERUNG ABGESCHLOSSEN ==="
|
||||
log ""
|
||||
log "🎉 RASPBERRY PI SCHNELLSTART-OPTIMIERUNG ERFOLGREICH!"
|
||||
log ""
|
||||
log "📋 ZUSAMMENFASSUNG:"
|
||||
log " ✅ Service-Dateien erstellt"
|
||||
log " ✅ Boot-Parameter optimiert"
|
||||
log " ✅ Autologin verstärkt"
|
||||
log " ✅ Kiosk-Benutzer optimiert"
|
||||
log " ✅ Wartungstools erstellt"
|
||||
log " ✅ Watchdog-Services verstärkt"
|
||||
log " ✅ System-Services optimiert"
|
||||
log " ✅ System-Parameter optimiert"
|
||||
log ""
|
||||
log "🔄 NEUSTART ERFORDERLICH:"
|
||||
log " sudo reboot"
|
||||
log ""
|
||||
log "📊 NACH DEM NEUSTART:"
|
||||
log " - System startet automatisch ohne Anmeldung"
|
||||
log " - Kiosk-Modus wird automatisch gestartet"
|
||||
log " - Web-UI ist sofort verfügbar"
|
||||
log " - Mehrfache Überwachung aktiv"
|
||||
log ""
|
||||
log "🔧 WARTUNG:"
|
||||
log " sudo myp-maintenance status # System-Status prüfen"
|
||||
log " sudo myp-maintenance logs # Logs anzeigen"
|
||||
log " sudo myp-maintenance restart # Services neustarten"
|
||||
log ""
|
||||
|
||||
warning "WICHTIG: Führen Sie jetzt 'sudo reboot' aus, um die Optimierungen zu aktivieren!"
|
||||
}
|
||||
|
||||
# Skript ausführen
|
||||
main "$@"
|
@ -1,396 +0,0 @@
|
||||
"""
|
||||
Queue Manager für die Verwaltung von Druckjobs in Warteschlangen.
|
||||
Überwacht offline Drucker und aktiviert Jobs automatisch.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import subprocess
|
||||
import os
|
||||
import requests
|
||||
import signal
|
||||
import atexit
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from contextlib import contextmanager
|
||||
|
||||
from models import get_db_session, Job, Printer, User, Notification
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
# Windows-spezifische Imports
|
||||
if os.name == 'nt':
|
||||
try:
|
||||
from utils.windows_fixes import get_windows_thread_manager
|
||||
except ImportError:
|
||||
get_windows_thread_manager = None
|
||||
else:
|
||||
get_windows_thread_manager = None
|
||||
|
||||
# Logger für Queue-Manager
|
||||
queue_logger = get_logger("queue_manager")
|
||||
|
||||
def check_printer_status(ip_address: str, timeout: int = 5) -> Tuple[str, bool]:
|
||||
"""
|
||||
Vereinfachte Drucker-Status-Prüfung für den Queue Manager.
|
||||
|
||||
Args:
|
||||
ip_address: IP-Adresse der Drucker-Steckdose
|
||||
timeout: Timeout in Sekunden (Standard: 5)
|
||||
|
||||
Returns:
|
||||
Tuple[str, bool]: (Status, Aktiv) - Status ist "online" oder "offline", Aktiv ist True/False
|
||||
"""
|
||||
if not ip_address or ip_address.strip() == "":
|
||||
return "offline", False
|
||||
|
||||
try:
|
||||
# Ping-Test um Erreichbarkeit zu prüfen
|
||||
if os.name == 'nt': # Windows
|
||||
cmd = ['ping', '-n', '1', '-w', str(timeout * 1000), ip_address.strip()]
|
||||
else: # Unix/Linux/macOS
|
||||
cmd = ['ping', '-c', '1', '-W', str(timeout), ip_address.strip()]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout + 1,
|
||||
encoding='utf-8',
|
||||
errors='replace'
|
||||
)
|
||||
|
||||
# Wenn Ping erfolgreich ist, als online betrachten
|
||||
if result.returncode == 0:
|
||||
queue_logger.debug(f"✅ Drucker {ip_address} ist erreichbar (Ping erfolgreich)")
|
||||
return "online", True
|
||||
else:
|
||||
queue_logger.debug(f"❌ Drucker {ip_address} nicht erreichbar (Ping fehlgeschlagen)")
|
||||
return "offline", False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
queue_logger.warning(f"⏱️ Ping-Timeout für Drucker {ip_address} nach {timeout} Sekunden")
|
||||
return "offline", False
|
||||
except Exception as e:
|
||||
queue_logger.error(f"❌ Fehler beim Status-Check für Drucker {ip_address}: {str(e)}")
|
||||
return "offline", False
|
||||
|
||||
class PrinterQueueManager:
|
||||
"""
|
||||
Verwaltet die Warteschlangen für offline Drucker und überwacht deren Status.
|
||||
Verbesserte Version mit ordnungsgemäßem Thread-Management für Windows.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_running = False
|
||||
self.monitor_thread = None
|
||||
self.shutdown_event = threading.Event() # Sauberes Shutdown-Signal
|
||||
self.check_interval = 120 # 2 Minuten zwischen Status-Checks
|
||||
self.last_status_cache = {} # Cache für letzten bekannten Status
|
||||
self.notification_cooldown = {} # Verhindert Spam-Benachrichtigungen
|
||||
self._lock = threading.Lock() # Thread-Sicherheit
|
||||
|
||||
# Windows-spezifische Signal-Handler registrieren
|
||||
if os.name == 'nt':
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
|
||||
def _signal_handler(self, signum, frame):
|
||||
"""Signal-Handler für ordnungsgemäßes Shutdown."""
|
||||
queue_logger.warning(f"🛑 Signal {signum} empfangen - stoppe Queue Manager...")
|
||||
self.stop()
|
||||
|
||||
def start(self):
|
||||
"""Startet den Queue-Manager mit verbessertem Thread-Management."""
|
||||
with self._lock:
|
||||
if not self.is_running:
|
||||
self.is_running = True
|
||||
self.shutdown_event.clear()
|
||||
self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=False)
|
||||
self.monitor_thread.name = "PrinterQueueMonitor"
|
||||
|
||||
# Windows Thread-Manager verwenden falls verfügbar
|
||||
if os.name == 'nt' and get_windows_thread_manager:
|
||||
try:
|
||||
thread_manager = get_windows_thread_manager()
|
||||
thread_manager.register_thread(self.monitor_thread)
|
||||
thread_manager.register_cleanup_function(self.stop)
|
||||
queue_logger.debug("✅ Queue Manager bei Windows Thread-Manager registriert")
|
||||
except Exception as e:
|
||||
queue_logger.warning(f"⚠️ Windows Thread-Manager nicht verfügbar: {str(e)}")
|
||||
|
||||
self.monitor_thread.start()
|
||||
queue_logger.info("✅ Printer Queue Manager erfolgreich gestartet")
|
||||
|
||||
def stop(self):
|
||||
"""Stoppt den Queue-Manager ordnungsgemäß."""
|
||||
with self._lock:
|
||||
if self.is_running:
|
||||
queue_logger.info("🔄 Beende Queue Manager...")
|
||||
self.is_running = False
|
||||
self.shutdown_event.set()
|
||||
|
||||
if self.monitor_thread and self.monitor_thread.is_alive():
|
||||
queue_logger.debug("⏳ Warte auf Thread-Beendigung...")
|
||||
self.monitor_thread.join(timeout=10)
|
||||
|
||||
if self.monitor_thread.is_alive():
|
||||
queue_logger.warning("⚠️ Thread konnte nicht ordnungsgemäß beendet werden")
|
||||
else:
|
||||
queue_logger.info("✅ Monitor-Thread erfolgreich beendet")
|
||||
|
||||
self.monitor_thread = None
|
||||
queue_logger.info("❌ Printer Queue Manager gestoppt")
|
||||
|
||||
def _monitor_loop(self):
|
||||
"""Hauptschleife für die Überwachung der Drucker mit verbessertem Shutdown-Handling."""
|
||||
queue_logger.info(f"🔄 Queue-Überwachung gestartet (Intervall: {self.check_interval} Sekunden)")
|
||||
|
||||
while self.is_running and not self.shutdown_event.is_set():
|
||||
try:
|
||||
self._check_waiting_jobs()
|
||||
|
||||
# Verwende Event.wait() statt time.sleep() für unterbrechbares Warten
|
||||
if self.shutdown_event.wait(timeout=self.check_interval):
|
||||
# Shutdown-Signal erhalten
|
||||
queue_logger.info("🛑 Shutdown-Signal empfangen - beende Monitor-Loop")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
queue_logger.error(f"❌ Fehler in Monitor-Schleife: {str(e)}")
|
||||
# Kürzere Wartezeit bei Fehlern, aber auch unterbrechbar
|
||||
if self.shutdown_event.wait(timeout=30):
|
||||
break
|
||||
|
||||
queue_logger.info("🔚 Monitor-Loop beendet")
|
||||
|
||||
def _check_waiting_jobs(self):
|
||||
"""Überprüft alle wartenden Jobs und aktiviert sie bei verfügbaren Druckern."""
|
||||
if self.shutdown_event.is_set():
|
||||
return
|
||||
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
# Alle wartenden Jobs abrufen
|
||||
waiting_jobs = db_session.query(Job).filter(
|
||||
Job.status == "waiting_for_printer"
|
||||
).all()
|
||||
|
||||
if not waiting_jobs:
|
||||
return
|
||||
|
||||
queue_logger.info(f"🔍 Überprüfe {len(waiting_jobs)} wartende Jobs...")
|
||||
|
||||
activated_jobs = []
|
||||
|
||||
for job in waiting_jobs:
|
||||
# Shutdown-Check zwischen Jobs
|
||||
if self.shutdown_event.is_set():
|
||||
break
|
||||
|
||||
# Drucker-Status prüfen
|
||||
printer = db_session.get(Printer, job.printer_id)
|
||||
if not printer:
|
||||
continue
|
||||
|
||||
# Status-Check mit Cache-Optimierung
|
||||
printer_key = f"printer_{printer.id}"
|
||||
current_status = None
|
||||
|
||||
try:
|
||||
if printer.plug_ip:
|
||||
status, active = check_printer_status(printer.plug_ip, timeout=5)
|
||||
current_status = "online" if (status == "online" and active) else "offline"
|
||||
else:
|
||||
current_status = "offline"
|
||||
|
||||
except Exception as e:
|
||||
queue_logger.warning(f"⚠️ Status-Check für Drucker {printer.name} fehlgeschlagen: {str(e)}")
|
||||
current_status = "offline"
|
||||
|
||||
# Prüfen, ob Drucker online geworden ist
|
||||
last_status = self.last_status_cache.get(printer_key, "offline")
|
||||
self.last_status_cache[printer_key] = current_status
|
||||
|
||||
if current_status == "online" and last_status == "offline":
|
||||
# Drucker ist online geworden!
|
||||
queue_logger.info(f"🟢 Drucker {printer.name} ist ONLINE geworden - aktiviere wartende Jobs")
|
||||
|
||||
# Job aktivieren
|
||||
job.status = "scheduled"
|
||||
printer.status = "available"
|
||||
printer.active = True
|
||||
printer.last_checked = datetime.now()
|
||||
|
||||
activated_jobs.append({
|
||||
"job": job,
|
||||
"printer": printer
|
||||
})
|
||||
|
||||
elif current_status == "online":
|
||||
# Drucker ist bereits online, Job kann aktiviert werden
|
||||
job.status = "scheduled"
|
||||
printer.status = "available"
|
||||
printer.active = True
|
||||
printer.last_checked = datetime.now()
|
||||
|
||||
activated_jobs.append({
|
||||
"job": job,
|
||||
"printer": printer
|
||||
})
|
||||
|
||||
else:
|
||||
# Drucker bleibt offline
|
||||
printer.status = "offline"
|
||||
printer.active = False
|
||||
printer.last_checked = datetime.now()
|
||||
|
||||
# Speichere alle Änderungen
|
||||
if activated_jobs:
|
||||
db_session.commit()
|
||||
queue_logger.info(f"✅ {len(activated_jobs)} Jobs erfolgreich aktiviert")
|
||||
|
||||
# Benachrichtigungen versenden (nur wenn nicht im Shutdown)
|
||||
if not self.shutdown_event.is_set():
|
||||
for item in activated_jobs:
|
||||
self._send_job_activation_notification(item["job"], item["printer"])
|
||||
else:
|
||||
# Auch offline-Status speichern
|
||||
db_session.commit()
|
||||
|
||||
except Exception as e:
|
||||
db_session.rollback()
|
||||
queue_logger.error(f"❌ Fehler beim Überprüfen wartender Jobs: {str(e)}")
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
def _send_job_activation_notification(self, job: Job, printer: Printer):
|
||||
"""Sendet eine Benachrichtigung, wenn ein Job aktiviert wird."""
|
||||
if self.shutdown_event.is_set():
|
||||
return
|
||||
|
||||
try:
|
||||
# Cooldown prüfen (keine Spam-Benachrichtigungen)
|
||||
cooldown_key = f"job_{job.id}_activated"
|
||||
now = datetime.now()
|
||||
|
||||
if cooldown_key in self.notification_cooldown:
|
||||
last_notification = self.notification_cooldown[cooldown_key]
|
||||
if (now - last_notification).total_seconds() < 300: # 5 Minuten Cooldown
|
||||
return
|
||||
|
||||
self.notification_cooldown[cooldown_key] = now
|
||||
|
||||
# Benachrichtigung erstellen
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
user = db_session.get(User, job.user_id)
|
||||
if not user:
|
||||
return
|
||||
|
||||
notification = Notification(
|
||||
user_id=user.id,
|
||||
type="job_activated",
|
||||
payload={
|
||||
"job_id": job.id,
|
||||
"job_name": job.name,
|
||||
"printer_id": printer.id,
|
||||
"printer_name": printer.name,
|
||||
"start_time": job.start_at.isoformat() if job.start_at else None,
|
||||
"message": f"🎉 Gute Nachrichten! Drucker '{printer.name}' ist online. Ihr Job '{job.name}' wurde aktiviert und startet bald."
|
||||
}
|
||||
)
|
||||
|
||||
db_session.add(notification)
|
||||
db_session.commit()
|
||||
|
||||
queue_logger.info(f"📧 Benachrichtigung für User {user.name} gesendet: Job {job.name} aktiviert")
|
||||
|
||||
except Exception as e:
|
||||
db_session.rollback()
|
||||
queue_logger.error(f"❌ Fehler beim Erstellen der Benachrichtigung: {str(e)}")
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
except Exception as e:
|
||||
queue_logger.error(f"❌ Fehler beim Senden der Aktivierungs-Benachrichtigung: {str(e)}")
|
||||
|
||||
def get_queue_status(self) -> Dict:
|
||||
"""Gibt den aktuellen Status der Warteschlangen zurück."""
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
# Wartende Jobs zählen
|
||||
waiting_jobs = db_session.query(Job).filter(
|
||||
Job.status == "waiting_for_printer"
|
||||
).count()
|
||||
|
||||
# Offline Drucker mit wartenden Jobs
|
||||
offline_printers_with_queue = db_session.query(Printer).join(Job).filter(
|
||||
Printer.status == "offline",
|
||||
Job.status == "waiting_for_printer"
|
||||
).distinct().count()
|
||||
|
||||
# Online Drucker
|
||||
online_printers = db_session.query(Printer).filter(
|
||||
Printer.status == "available"
|
||||
).count()
|
||||
|
||||
total_printers = db_session.query(Printer).count()
|
||||
|
||||
return {
|
||||
"waiting_jobs": waiting_jobs,
|
||||
"offline_printers_with_queue": offline_printers_with_queue,
|
||||
"online_printers": online_printers,
|
||||
"total_printers": total_printers,
|
||||
"queue_manager_running": self.is_running,
|
||||
"last_check": datetime.now().isoformat(),
|
||||
"check_interval_seconds": self.check_interval
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
queue_logger.error(f"❌ Fehler beim Abrufen des Queue-Status: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"queue_manager_running": self.is_running
|
||||
}
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
def is_healthy(self) -> bool:
|
||||
"""Prüft, ob der Queue Manager ordnungsgemäß läuft."""
|
||||
return (self.is_running and
|
||||
self.monitor_thread is not None and
|
||||
self.monitor_thread.is_alive() and
|
||||
not self.shutdown_event.is_set())
|
||||
|
||||
# Globale Instanz des Queue-Managers
|
||||
_queue_manager_instance = None
|
||||
_queue_manager_lock = threading.Lock()
|
||||
|
||||
def get_queue_manager() -> PrinterQueueManager:
|
||||
"""Gibt die globale Instanz des Queue-Managers zurück."""
|
||||
global _queue_manager_instance
|
||||
with _queue_manager_lock:
|
||||
if _queue_manager_instance is None:
|
||||
_queue_manager_instance = PrinterQueueManager()
|
||||
return _queue_manager_instance
|
||||
|
||||
def start_queue_manager():
|
||||
"""Startet den globalen Queue-Manager."""
|
||||
manager = get_queue_manager()
|
||||
manager.start()
|
||||
return manager
|
||||
|
||||
def stop_queue_manager():
|
||||
"""Stoppt den globalen Queue-Manager."""
|
||||
global _queue_manager_instance
|
||||
with _queue_manager_lock:
|
||||
if _queue_manager_instance:
|
||||
_queue_manager_instance.stop()
|
||||
_queue_manager_instance = None
|
||||
|
||||
# Automatisches Cleanup bei Prozess-Ende registrieren
|
||||
atexit.register(stop_queue_manager)
|
@ -1,353 +0,0 @@
|
||||
"""
|
||||
Windows-spezifische Fixes für Thread- und Socket-Probleme
|
||||
Behebt bekannte Issues mit Flask Auto-Reload auf Windows.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import threading
|
||||
import time
|
||||
import atexit
|
||||
from typing import List, Callable
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
# Logger für Windows-Fixes
|
||||
windows_logger = get_logger("windows_fixes")
|
||||
|
||||
# Globale Flags um doppelte Anwendung zu verhindern
|
||||
_windows_fixes_applied = False
|
||||
_socket_patches_applied = False
|
||||
|
||||
class WindowsThreadManager:
|
||||
"""
|
||||
Verwaltet Threads und deren ordnungsgemäße Beendigung auf Windows.
|
||||
Behebt Socket-Fehler beim Flask Auto-Reload.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.managed_threads: List[threading.Thread] = []
|
||||
self.cleanup_functions: List[Callable] = []
|
||||
self.shutdown_event = threading.Event()
|
||||
self._lock = threading.Lock()
|
||||
self._is_shutting_down = False
|
||||
|
||||
# Signal-Handler nur auf Windows registrieren
|
||||
if os.name == 'nt':
|
||||
self._register_signal_handlers()
|
||||
|
||||
def _register_signal_handlers(self):
|
||||
"""Registriert Windows-spezifische Signal-Handler."""
|
||||
try:
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
# Windows-spezifisches SIGBREAK
|
||||
if hasattr(signal, 'SIGBREAK'):
|
||||
signal.signal(signal.SIGBREAK, self._signal_handler)
|
||||
windows_logger.debug("✅ Windows Signal-Handler registriert")
|
||||
except Exception as e:
|
||||
windows_logger.warning(f"⚠️ Signal-Handler konnten nicht registriert werden: {str(e)}")
|
||||
|
||||
def _signal_handler(self, sig, frame):
|
||||
"""Signal-Handler für ordnungsgemäßes Shutdown."""
|
||||
if not self._is_shutting_down:
|
||||
windows_logger.warning(f"🛑 Windows Signal {sig} empfangen - initiiere Shutdown")
|
||||
self.shutdown_all()
|
||||
|
||||
def register_thread(self, thread: threading.Thread):
|
||||
"""Registriert einen Thread für ordnungsgemäße Beendigung."""
|
||||
with self._lock:
|
||||
if thread not in self.managed_threads:
|
||||
self.managed_threads.append(thread)
|
||||
windows_logger.debug(f"📝 Thread {thread.name} registriert")
|
||||
|
||||
def register_cleanup_function(self, func: Callable):
|
||||
"""Registriert eine Cleanup-Funktion."""
|
||||
with self._lock:
|
||||
if func not in self.cleanup_functions:
|
||||
self.cleanup_functions.append(func)
|
||||
windows_logger.debug(f"📝 Cleanup-Funktion registriert")
|
||||
|
||||
def shutdown_all(self):
|
||||
"""Beendet alle verwalteten Threads und führt Cleanup durch."""
|
||||
if self._is_shutting_down:
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
self._is_shutting_down = True
|
||||
windows_logger.info("🔄 Starte Windows Thread-Shutdown...")
|
||||
|
||||
# Shutdown-Event setzen
|
||||
self.shutdown_event.set()
|
||||
|
||||
# Cleanup-Funktionen ausführen
|
||||
for func in self.cleanup_functions:
|
||||
try:
|
||||
windows_logger.debug(f"🧹 Führe Cleanup-Funktion aus: {func.__name__}")
|
||||
func()
|
||||
except Exception as e:
|
||||
windows_logger.error(f"❌ Fehler bei Cleanup-Funktion {func.__name__}: {str(e)}")
|
||||
|
||||
# Threads beenden
|
||||
active_threads = [t for t in self.managed_threads if t.is_alive()]
|
||||
if active_threads:
|
||||
windows_logger.info(f"⏳ Warte auf {len(active_threads)} aktive Threads...")
|
||||
|
||||
for thread in active_threads:
|
||||
try:
|
||||
windows_logger.debug(f"🔄 Beende Thread: {thread.name}")
|
||||
thread.join(timeout=5)
|
||||
|
||||
if thread.is_alive():
|
||||
windows_logger.warning(f"⚠️ Thread {thread.name} konnte nicht ordnungsgemäß beendet werden")
|
||||
else:
|
||||
windows_logger.debug(f"✅ Thread {thread.name} erfolgreich beendet")
|
||||
except Exception as e:
|
||||
windows_logger.error(f"❌ Fehler beim Beenden von Thread {thread.name}: {str(e)}")
|
||||
|
||||
windows_logger.info("✅ Windows Thread-Shutdown abgeschlossen")
|
||||
|
||||
# Globale Instanz
|
||||
_windows_thread_manager = None
|
||||
|
||||
def get_windows_thread_manager() -> WindowsThreadManager:
|
||||
"""Gibt die globale Instanz des Windows Thread-Managers zurück."""
|
||||
global _windows_thread_manager
|
||||
if _windows_thread_manager is None:
|
||||
_windows_thread_manager = WindowsThreadManager()
|
||||
return _windows_thread_manager
|
||||
|
||||
def fix_windows_socket_issues():
|
||||
"""
|
||||
Anwendung von Windows-spezifischen Socket-Fixes.
|
||||
Vereinfachte, sichere Version ohne Monkey-Patching.
|
||||
"""
|
||||
global _socket_patches_applied
|
||||
|
||||
if os.name != 'nt':
|
||||
return
|
||||
|
||||
if _socket_patches_applied:
|
||||
windows_logger.debug("⏭️ Socket-Patches bereits angewendet")
|
||||
return
|
||||
|
||||
try:
|
||||
# SICHERERE Alternative: Nur TCP Socket-Optionen setzen ohne Monkey-Patching
|
||||
import socket
|
||||
|
||||
# Erweitere die Socket-Klasse mit einer Hilfsmethode
|
||||
if not hasattr(socket.socket, 'windows_bind_with_reuse'):
|
||||
|
||||
def windows_bind_with_reuse(self, address):
|
||||
"""Windows-optimierte bind-Methode mit SO_REUSEADDR."""
|
||||
try:
|
||||
# SO_REUSEADDR aktivieren
|
||||
self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
windows_logger.debug(f"SO_REUSEADDR aktiviert für Socket {address}")
|
||||
except Exception as e:
|
||||
windows_logger.debug(f"SO_REUSEADDR konnte nicht gesetzt werden: {str(e)}")
|
||||
|
||||
# Standard-bind ausführen
|
||||
return self.bind(address)
|
||||
|
||||
# Füge die Hilfsmethode hinzu ohne die ursprüngliche bind-Methode zu überschreiben
|
||||
socket.socket.windows_bind_with_reuse = windows_bind_with_reuse
|
||||
|
||||
# Setze globale Socket-Optionen für bessere Windows-Kompatibilität
|
||||
socket.setdefaulttimeout(30) # 30 Sekunden Standard-Timeout
|
||||
|
||||
_socket_patches_applied = True
|
||||
windows_logger.debug("✅ Windows Socket-Optimierungen angewendet (sicher)")
|
||||
|
||||
except Exception as e:
|
||||
windows_logger.warning(f"⚠️ Socket-Optimierungen konnten nicht angewendet werden: {str(e)}")
|
||||
|
||||
def apply_safe_socket_options():
|
||||
"""
|
||||
Wendet sichere Socket-Optionen für Windows an ohne Monkey-Patching.
|
||||
"""
|
||||
if os.name != 'nt':
|
||||
return
|
||||
|
||||
try:
|
||||
import socket
|
||||
|
||||
# Sichere Socket-Defaults für Windows
|
||||
if hasattr(socket, 'TCP_NODELAY'):
|
||||
# TCP_NODELAY als Standard aktivieren für bessere Performance
|
||||
pass # Wird pro Socket gesetzt, nicht global
|
||||
|
||||
windows_logger.debug("✅ Sichere Socket-Optionen angewendet")
|
||||
|
||||
except Exception as e:
|
||||
windows_logger.debug(f"Socket-Optionen konnten nicht gesetzt werden: {str(e)}")
|
||||
|
||||
def setup_windows_environment():
|
||||
"""
|
||||
Richtet die Windows-Umgebung für bessere Flask-Kompatibilität ein.
|
||||
"""
|
||||
if os.name != 'nt':
|
||||
return
|
||||
|
||||
try:
|
||||
# Umgebungsvariablen für bessere Windows-Kompatibilität
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
os.environ['PYTHONUTF8'] = '1'
|
||||
|
||||
windows_logger.debug("✅ Windows-Umgebung optimiert")
|
||||
|
||||
except Exception as e:
|
||||
windows_logger.warning(f"⚠️ Windows-Umgebung konnte nicht optimiert werden: {str(e)}")
|
||||
|
||||
def is_flask_reloader_process() -> bool:
|
||||
"""
|
||||
Prüft, ob der aktuelle Prozess der Flask-Reloader-Prozess ist.
|
||||
"""
|
||||
return os.environ.get('WERKZEUG_RUN_MAIN') != 'true'
|
||||
|
||||
def apply_all_windows_fixes():
|
||||
"""Wendet alle Windows-spezifischen Fixes an."""
|
||||
global _windows_fixes_applied
|
||||
|
||||
if _windows_fixes_applied:
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info("🔧 Wende Windows-spezifische Fixes an...")
|
||||
|
||||
# 1. Encoding-Fixes
|
||||
apply_encoding_fixes()
|
||||
|
||||
# 2. Threading-Fixes
|
||||
apply_threading_fixes()
|
||||
|
||||
# 3. Signal-Handler-Fixes
|
||||
apply_signal_fixes()
|
||||
|
||||
# 4. Subprocess-Patch für UTF-8 Encoding
|
||||
patch_subprocess()
|
||||
|
||||
# 5. Globaler Subprocess-Patch für bereits importierte Module
|
||||
apply_global_subprocess_patch()
|
||||
|
||||
_windows_fixes_applied = True
|
||||
logger.info("✅ Alle Windows-Fixes erfolgreich angewendet")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Fehler beim Anwenden der Windows-Fixes: {str(e)}")
|
||||
raise e
|
||||
|
||||
# Automatisch Windows-Fixes beim Import anwenden (nur einmal)
|
||||
if os.name == 'nt' and not _windows_fixes_applied:
|
||||
# Sehr früher subprocess-Patch für sofortige Wirkung
|
||||
try:
|
||||
import subprocess
|
||||
if not hasattr(subprocess, '_early_patched'):
|
||||
patch_subprocess()
|
||||
subprocess._early_patched = True
|
||||
logger.info("✅ Früher subprocess-Patch beim Import angewendet")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Früher subprocess-Patch fehlgeschlagen: {str(e)}")
|
||||
|
||||
apply_all_windows_fixes()
|
||||
|
||||
# ===== SICHERE SUBPROCESS-WRAPPER =====
|
||||
|
||||
def safe_subprocess_run(*args, **kwargs):
|
||||
"""
|
||||
Sicherer subprocess.run Wrapper für Windows mit UTF-8 Encoding.
|
||||
Verhindert charmap-Fehler durch explizite Encoding-Einstellungen.
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
# Standard-Encoding für Windows setzen
|
||||
if 'encoding' not in kwargs and kwargs.get('text', False):
|
||||
kwargs['encoding'] = 'utf-8'
|
||||
kwargs['errors'] = 'replace'
|
||||
|
||||
# Timeout-Standard setzen falls nicht vorhanden
|
||||
if 'timeout' not in kwargs:
|
||||
kwargs['timeout'] = 30
|
||||
|
||||
try:
|
||||
return subprocess.run(*args, **kwargs)
|
||||
except subprocess.TimeoutExpired as e:
|
||||
logger.warning(f"Subprocess-Timeout nach {kwargs.get('timeout', 30)}s: {' '.join(args[0]) if args and isinstance(args[0], list) else str(args)}")
|
||||
raise e
|
||||
except UnicodeDecodeError as e:
|
||||
logger.error(f"Unicode-Decode-Fehler in subprocess: {str(e)}")
|
||||
# Fallback ohne text=True
|
||||
kwargs_fallback = kwargs.copy()
|
||||
kwargs_fallback.pop('text', None)
|
||||
kwargs_fallback.pop('encoding', None)
|
||||
kwargs_fallback.pop('errors', None)
|
||||
return subprocess.run(*args, **kwargs_fallback)
|
||||
except Exception as e:
|
||||
logger.error(f"Subprocess-Fehler: {str(e)}")
|
||||
raise e
|
||||
|
||||
# ===== SUBPROCESS-MONKEY-PATCH =====
|
||||
|
||||
def patch_subprocess():
|
||||
"""
|
||||
Patcht subprocess.run und subprocess.Popen um automatisch sichere Encoding-Einstellungen zu verwenden.
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
# Original-Funktionen speichern
|
||||
if not hasattr(subprocess, '_original_run'):
|
||||
subprocess._original_run = subprocess.run
|
||||
subprocess._original_popen = subprocess.Popen
|
||||
|
||||
def patched_run(*args, **kwargs):
|
||||
# Automatisch UTF-8 Encoding für text=True setzen
|
||||
if kwargs.get('text', False) and 'encoding' not in kwargs:
|
||||
kwargs['encoding'] = 'utf-8'
|
||||
kwargs['errors'] = 'replace'
|
||||
|
||||
return subprocess._original_run(*args, **kwargs)
|
||||
|
||||
def patched_popen(*args, **kwargs):
|
||||
# Automatisch UTF-8 Encoding für text=True setzen
|
||||
if kwargs.get('text', False) and 'encoding' not in kwargs:
|
||||
kwargs['encoding'] = 'utf-8'
|
||||
kwargs['errors'] = 'replace'
|
||||
|
||||
# Auch für universal_newlines (ältere Python-Versionen)
|
||||
if kwargs.get('universal_newlines', False) and 'encoding' not in kwargs:
|
||||
kwargs['encoding'] = 'utf-8'
|
||||
kwargs['errors'] = 'replace'
|
||||
|
||||
return subprocess._original_popen(*args, **kwargs)
|
||||
|
||||
subprocess.run = patched_run
|
||||
subprocess.Popen = patched_popen
|
||||
logger.info("✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen)")
|
||||
|
||||
# ===== GLOBALER SUBPROCESS-PATCH =====
|
||||
|
||||
def apply_global_subprocess_patch():
|
||||
"""
|
||||
Wendet den subprocess-Patch global an, auch für bereits importierte Module.
|
||||
"""
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
# Patch subprocess direkt
|
||||
patch_subprocess()
|
||||
|
||||
# Patch auch in bereits importierten Modulen
|
||||
for module_name, module in sys.modules.items():
|
||||
if hasattr(module, 'subprocess') and module.subprocess is subprocess:
|
||||
# Modul verwendet subprocess - patch es
|
||||
module.subprocess = subprocess
|
||||
logger.debug(f"✅ Subprocess in Modul {module_name} gepatcht")
|
||||
|
||||
logger.info("✅ Globaler subprocess-Patch angewendet")
|
||||
|
||||
# ===== EXPORT SAFE SUBPROCESS =====
|
||||
|
||||
# Sichere subprocess-Funktion exportieren
|
||||
__all__.append('safe_subprocess_run')
|
||||
__all__.append('patch_subprocess')
|
||||
__all__.append('apply_global_subprocess_patch')
|
@ -1,550 +0,0 @@
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy import and_, or_, func
|
||||
|
||||
from models import Job, Printer, User, UserPermission, get_cached_session
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
calendar_blueprint = Blueprint('calendar', __name__)
|
||||
logger = get_logger("calendar")
|
||||
|
||||
def can_edit_events(user):
|
||||
"""Prüft, ob ein Benutzer Kalendereinträge bearbeiten darf."""
|
||||
if user.is_admin:
|
||||
return True
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user.id).first()
|
||||
if not permission:
|
||||
return False
|
||||
return permission.can_approve_jobs
|
||||
|
||||
def get_smart_printer_assignment(start_date, end_date, priority="normal", db_session=None):
|
||||
"""
|
||||
Intelligente Druckerzuweisung basierend auf verschiedenen Faktoren.
|
||||
|
||||
Args:
|
||||
start_date: Startzeit des Jobs
|
||||
end_date: Endzeit des Jobs
|
||||
priority: Prioritätsstufe ('urgent', 'high', 'normal', 'low')
|
||||
db_session: Datenbankverbindung
|
||||
|
||||
Returns:
|
||||
printer_id: ID des empfohlenen Druckers oder None
|
||||
"""
|
||||
if not db_session:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Verfügbare Drucker ermitteln
|
||||
available_printers = db_session.query(Printer).filter(
|
||||
Printer.active == True
|
||||
).all()
|
||||
|
||||
if not available_printers:
|
||||
logger.warning("Keine aktiven Drucker für automatische Zuweisung gefunden")
|
||||
return None
|
||||
|
||||
printer_scores = []
|
||||
|
||||
for printer in available_printers:
|
||||
score = 0
|
||||
|
||||
# 1. Verfügbarkeit prüfen - Jobs im gleichen Zeitraum
|
||||
conflicting_jobs = db_session.query(Job).filter(
|
||||
Job.printer_id == printer.id,
|
||||
Job.status.in_(["scheduled", "running"]),
|
||||
or_(
|
||||
and_(Job.start_at >= start_date, Job.start_at < end_date),
|
||||
and_(Job.end_at > start_date, Job.end_at <= end_date),
|
||||
and_(Job.start_at <= start_date, Job.end_at >= end_date)
|
||||
)
|
||||
).count()
|
||||
|
||||
if conflicting_jobs > 0:
|
||||
continue # Drucker ist nicht verfügbar
|
||||
|
||||
score += 100 # Grundpunkte für Verfügbarkeit
|
||||
|
||||
# 2. Auslastung in den letzten 24 Stunden bewerten
|
||||
last_24h = datetime.now() - timedelta(hours=24)
|
||||
recent_jobs = db_session.query(Job).filter(
|
||||
Job.printer_id == printer.id,
|
||||
Job.start_at >= last_24h,
|
||||
Job.status.in_(["scheduled", "running", "finished"])
|
||||
).count()
|
||||
|
||||
# Weniger Auslastung = höhere Punktzahl
|
||||
score += max(0, 50 - (recent_jobs * 10))
|
||||
|
||||
# 3. Prioritätsbasierte Zuweisung
|
||||
if priority == "urgent":
|
||||
# Für dringende Jobs: Express-Drucker bevorzugen
|
||||
if "express" in printer.name.lower() or "schnell" in printer.name.lower():
|
||||
score += 30
|
||||
elif priority == "high":
|
||||
# Für hohe Priorität: Weniger belastete Drucker
|
||||
if recent_jobs <= 2:
|
||||
score += 20
|
||||
|
||||
# 4. Zeitfenster-basierte Zuweisung
|
||||
start_hour = start_date.hour
|
||||
if start_hour >= 18 or start_hour <= 6: # Nachtschicht
|
||||
if "nacht" in printer.name.lower() or printer.location and "c" in printer.location.lower():
|
||||
score += 25
|
||||
elif start_hour >= 8 and start_hour <= 17: # Tagschicht
|
||||
if "tag" in printer.name.lower() or printer.location and "a" in printer.location.lower():
|
||||
score += 15
|
||||
|
||||
# 5. Standort-basierte Bewertung (Round-Robin ähnlich)
|
||||
if printer.location:
|
||||
location_penalty = hash(printer.location) % 10 # Verteilung basierend auf Standort
|
||||
score += location_penalty
|
||||
|
||||
# 6. Druckerdauer-Eignung
|
||||
job_duration_hours = (end_date - start_date).total_seconds() / 3600
|
||||
if job_duration_hours > 8: # Lange Jobs
|
||||
if "langzeit" in printer.name.lower() or "marathon" in printer.name.lower():
|
||||
score += 20
|
||||
elif job_duration_hours <= 2: # Kurze Jobs
|
||||
if "express" in printer.name.lower() or "schnell" in printer.name.lower():
|
||||
score += 15
|
||||
|
||||
# 7. Wartungszyklen berücksichtigen
|
||||
# Neuere Drucker (falls last_maintenance_date verfügbar) bevorzugen
|
||||
# TODO: Implementierung abhängig von Printer-Model-Erweiterungen
|
||||
|
||||
printer_scores.append({
|
||||
'printer': printer,
|
||||
'score': score,
|
||||
'conflicts': conflicting_jobs,
|
||||
'recent_load': recent_jobs
|
||||
})
|
||||
|
||||
# Nach Punktzahl sortieren und besten Drucker auswählen
|
||||
if not printer_scores:
|
||||
logger.warning("Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden")
|
||||
return None
|
||||
|
||||
printer_scores.sort(key=lambda x: x['score'], reverse=True)
|
||||
best_printer = printer_scores[0]
|
||||
|
||||
logger.info(f"Automatische Druckerzuweisung: {best_printer['printer'].name} "
|
||||
f"(Score: {best_printer['score']}, Load: {best_printer['recent_load']})")
|
||||
|
||||
return best_printer['printer'].id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei automatischer Druckerzuweisung: {str(e)}")
|
||||
return None
|
||||
|
||||
@calendar_blueprint.route('/calendar', methods=['GET'])
|
||||
@login_required
|
||||
def calendar_view():
|
||||
"""Kalender-Ansicht anzeigen."""
|
||||
can_edit = can_edit_events(current_user)
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
printers = db_session.query(Printer).filter_by(active=True).all()
|
||||
|
||||
return render_template('calendar.html',
|
||||
printers=printers,
|
||||
can_edit=can_edit)
|
||||
|
||||
@calendar_blueprint.route('/api/calendar', methods=['GET'])
|
||||
@login_required
|
||||
def api_get_calendar_events():
|
||||
"""Kalendereinträge als JSON für FullCalendar zurückgeben."""
|
||||
try:
|
||||
# Datumsbereich aus Anfrage
|
||||
start_str = request.args.get('from')
|
||||
end_str = request.args.get('to')
|
||||
|
||||
if not start_str or not end_str:
|
||||
# Standardmäßig eine Woche anzeigen
|
||||
start_date = datetime.now()
|
||||
end_date = start_date + timedelta(days=7)
|
||||
else:
|
||||
try:
|
||||
start_date = datetime.fromisoformat(start_str)
|
||||
end_date = datetime.fromisoformat(end_str)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
|
||||
# Optional: Filter nach Druckern
|
||||
printer_id = request.args.get('printer_id')
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Jobs im angegebenen Zeitraum abfragen
|
||||
query = db_session.query(Job).filter(
|
||||
or_(
|
||||
# Jobs, die im Zeitraum beginnen
|
||||
and_(Job.start_at >= start_date, Job.start_at <= end_date),
|
||||
# Jobs, die im Zeitraum enden
|
||||
and_(Job.end_at >= start_date, Job.end_at <= end_date),
|
||||
# Jobs, die den Zeitraum komplett umfassen
|
||||
and_(Job.start_at <= start_date, Job.end_at >= end_date)
|
||||
)
|
||||
)
|
||||
|
||||
if printer_id:
|
||||
query = query.filter(Job.printer_id == printer_id)
|
||||
|
||||
jobs = query.all()
|
||||
|
||||
# Jobs in FullCalendar-Event-Format umwandeln
|
||||
events = []
|
||||
for job in jobs:
|
||||
# Farbe basierend auf Status bestimmen
|
||||
color = "#6B7280" # Grau für pending
|
||||
if job.status == "running":
|
||||
color = "#3B82F6" # Blau für running
|
||||
elif job.status == "finished":
|
||||
color = "#10B981" # Grün für finished
|
||||
elif job.status == "scheduled":
|
||||
color = "#10B981" # Grün für approved
|
||||
elif job.status == "cancelled" or job.status == "failed":
|
||||
color = "#EF4444" # Rot für abgebrochen/fehlgeschlagen
|
||||
|
||||
# Benutzerinformationen laden
|
||||
user = db_session.query(User).filter_by(id=job.user_id).first()
|
||||
user_name = user.name if user else "Unbekannt"
|
||||
|
||||
# Druckerinformationen laden
|
||||
printer = db_session.query(Printer).filter_by(id=job.printer_id).first()
|
||||
printer_name = printer.name if printer else "Unbekannt"
|
||||
|
||||
event = {
|
||||
"id": job.id,
|
||||
"title": job.name,
|
||||
"start": job.start_at.isoformat(),
|
||||
"end": job.end_at.isoformat(),
|
||||
"color": color,
|
||||
"extendedProps": {
|
||||
"status": job.status,
|
||||
"description": job.description,
|
||||
"userName": user_name,
|
||||
"printerId": job.printer_id,
|
||||
"printerName": printer_name
|
||||
}
|
||||
}
|
||||
|
||||
events.append(event)
|
||||
|
||||
return jsonify(events)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Kalendereinträge: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/event', methods=['POST'])
|
||||
@login_required
|
||||
def api_create_calendar_event():
|
||||
"""Neuen Kalendereintrag (Job) erstellen."""
|
||||
# Nur Admins und Benutzer mit can_approve_jobs dürfen Einträge erstellen
|
||||
if not can_edit_events(current_user):
|
||||
return jsonify({"error": "Keine Berechtigung zum Erstellen von Kalendereinträgen"}), 403
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
# Pflichtfelder prüfen
|
||||
title = data.get('title')
|
||||
start = data.get('start')
|
||||
end = data.get('end')
|
||||
printer_id = data.get('printerId') # Jetzt optional
|
||||
priority = data.get('priority', 'normal')
|
||||
|
||||
if not all([title, start, end]):
|
||||
return jsonify({"error": "Titel, Start und Ende sind erforderlich"}), 400
|
||||
|
||||
# Datumsfelder konvertieren
|
||||
try:
|
||||
start_date = datetime.fromisoformat(start)
|
||||
end_date = datetime.fromisoformat(end)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
|
||||
# Dauer in Minuten berechnen
|
||||
duration_minutes = int((end_date - start_date).total_seconds() / 60)
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Intelligente Druckerzuweisung falls kein Drucker angegeben
|
||||
if not printer_id:
|
||||
logger.info(f"Automatische Druckerzuweisung wird verwendet für Job '{title}'")
|
||||
printer_id = get_smart_printer_assignment(
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
priority=priority,
|
||||
db_session=db_session
|
||||
)
|
||||
|
||||
if not printer_id:
|
||||
return jsonify({
|
||||
"error": "Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden. "
|
||||
"Bitte wählen Sie einen spezifischen Drucker oder einen anderen Zeitraum."
|
||||
}), 409
|
||||
|
||||
# Drucker prüfen/validieren
|
||||
printer = db_session.query(Printer).filter_by(id=printer_id).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Drucker nicht gefunden"}), 404
|
||||
|
||||
if not printer.active:
|
||||
return jsonify({"error": f"Drucker '{printer.name}' ist nicht aktiv"}), 400
|
||||
|
||||
# Nochmals Verfügbarkeit prüfen bei automatischer Zuweisung
|
||||
if not data.get('printerId'): # War automatische Zuweisung
|
||||
conflicting_jobs = db_session.query(Job).filter(
|
||||
Job.printer_id == printer_id,
|
||||
Job.status.in_(["scheduled", "running"]),
|
||||
or_(
|
||||
and_(Job.start_at >= start_date, Job.start_at < end_date),
|
||||
and_(Job.end_at > start_date, Job.end_at <= end_date),
|
||||
and_(Job.start_at <= start_date, Job.end_at >= end_date)
|
||||
)
|
||||
).first()
|
||||
|
||||
if conflicting_jobs:
|
||||
return jsonify({
|
||||
"error": f"Automatisch zugewiesener Drucker '{printer.name}' ist nicht mehr verfügbar. "
|
||||
"Bitte wählen Sie einen anderen Zeitraum oder spezifischen Drucker."
|
||||
}), 409
|
||||
|
||||
# Neuen Job erstellen
|
||||
job = Job(
|
||||
name=title,
|
||||
description=data.get('description', ''),
|
||||
user_id=current_user.id,
|
||||
printer_id=printer_id,
|
||||
start_at=start_date,
|
||||
end_at=end_date,
|
||||
status="scheduled",
|
||||
duration_minutes=duration_minutes,
|
||||
owner_id=current_user.id
|
||||
)
|
||||
|
||||
db_session.add(job)
|
||||
db_session.commit()
|
||||
|
||||
assignment_type = "automatisch" if not data.get('printerId') else "manuell"
|
||||
logger.info(f"Neuer Kalendereintrag erstellt: ID {job.id}, Name: {title}, "
|
||||
f"Drucker: {printer.name} ({assignment_type} zugewiesen)")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"id": job.id,
|
||||
"title": job.name,
|
||||
"start": job.start_at.isoformat(),
|
||||
"end": job.end_at.isoformat(),
|
||||
"status": job.status,
|
||||
"printer": {
|
||||
"id": printer.id,
|
||||
"name": printer.name,
|
||||
"location": printer.location,
|
||||
"assignment_type": assignment_type
|
||||
},
|
||||
"message": f"Auftrag erfolgreich erstellt und {assignment_type} dem Drucker '{printer.name}' zugewiesen."
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen des Kalendereintrags: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/event/<int:event_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def api_update_calendar_event(event_id):
|
||||
"""Kalendereintrag (Job) aktualisieren."""
|
||||
# Nur Admins und Benutzer mit can_approve_jobs dürfen Einträge bearbeiten
|
||||
if not can_edit_events(current_user):
|
||||
return jsonify({"error": "Keine Berechtigung zum Bearbeiten von Kalendereinträgen"}), 403
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
job = db_session.query(Job).filter_by(id=event_id).first()
|
||||
if not job:
|
||||
return jsonify({"error": "Kalendereintrag nicht gefunden"}), 404
|
||||
|
||||
# Felder aktualisieren, die im Request enthalten sind
|
||||
if 'title' in data:
|
||||
job.name = data['title']
|
||||
|
||||
if 'description' in data:
|
||||
job.description = data['description']
|
||||
|
||||
if 'start' in data and 'end' in data:
|
||||
try:
|
||||
start_date = datetime.fromisoformat(data['start'])
|
||||
end_date = datetime.fromisoformat(data['end'])
|
||||
|
||||
job.start_at = start_date
|
||||
job.end_at = end_date
|
||||
job.duration_minutes = int((end_date - start_date).total_seconds() / 60)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
|
||||
if 'printerId' in data:
|
||||
printer = db_session.query(Printer).filter_by(id=data['printerId']).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Drucker nicht gefunden"}), 404
|
||||
job.printer_id = data['printerId']
|
||||
|
||||
if 'status' in data:
|
||||
# Status nur ändern, wenn er gültig ist
|
||||
valid_statuses = ["scheduled", "running", "finished", "cancelled"]
|
||||
if data['status'] in valid_statuses:
|
||||
job.status = data['status']
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Kalendereintrag aktualisiert: ID {job.id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"id": job.id,
|
||||
"title": job.name,
|
||||
"start": job.start_at.isoformat(),
|
||||
"end": job.end_at.isoformat(),
|
||||
"status": job.status
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktualisieren des Kalendereintrags: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/event/<int:event_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def api_delete_calendar_event(event_id):
|
||||
"""Kalendereintrag (Job) löschen."""
|
||||
# Nur Admins und Benutzer mit can_approve_jobs dürfen Einträge löschen
|
||||
if not can_edit_events(current_user):
|
||||
return jsonify({"error": "Keine Berechtigung zum Löschen von Kalendereinträgen"}), 403
|
||||
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
job = db_session.query(Job).filter_by(id=event_id).first()
|
||||
if not job:
|
||||
return jsonify({"error": "Kalendereintrag nicht gefunden"}), 404
|
||||
|
||||
db_session.delete(job)
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Kalendereintrag gelöscht: ID {event_id}")
|
||||
|
||||
return jsonify({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Löschen des Kalendereintrags: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/smart-recommendation', methods=['POST'])
|
||||
@login_required
|
||||
def api_get_smart_recommendation():
|
||||
"""Intelligente Druckerempfehlung für gegebenes Zeitfenster abrufen."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
start = data.get('start')
|
||||
end = data.get('end')
|
||||
priority = data.get('priority', 'normal')
|
||||
|
||||
if not all([start, end]):
|
||||
return jsonify({"error": "Start und Ende sind erforderlich"}), 400
|
||||
|
||||
# Datumsfelder konvertieren
|
||||
try:
|
||||
start_date = datetime.fromisoformat(start)
|
||||
end_date = datetime.fromisoformat(end)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Empfohlenen Drucker ermitteln
|
||||
recommended_printer_id = get_smart_printer_assignment(
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
priority=priority,
|
||||
db_session=db_session
|
||||
)
|
||||
|
||||
if not recommended_printer_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden."
|
||||
})
|
||||
|
||||
# Drucker-Details abrufen
|
||||
printer = db_session.query(Printer).filter_by(id=recommended_printer_id).first()
|
||||
if not printer:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "Empfohlener Drucker nicht mehr verfügbar."
|
||||
})
|
||||
|
||||
# Zusätzliche Statistiken für die Empfehlung berechnen
|
||||
last_24h = datetime.now() - timedelta(hours=24)
|
||||
recent_jobs_count = db_session.query(Job).filter(
|
||||
Job.printer_id == printer.id,
|
||||
Job.start_at >= last_24h,
|
||||
Job.status.in_(["scheduled", "running", "finished"])
|
||||
).count()
|
||||
|
||||
# Verfügbarkeit als Prozentsatz berechnen
|
||||
total_time_slots = 24 # Stunden pro Tag
|
||||
availability_percent = max(0, 100 - (recent_jobs_count * 4)) # Grobe Schätzung
|
||||
|
||||
# Auslastung berechnen
|
||||
utilization_percent = min(100, recent_jobs_count * 8) # Grobe Schätzung
|
||||
|
||||
# Eignung basierend auf Priorität und Zeitfenster bestimmen
|
||||
suitability = "Gut"
|
||||
if priority == "urgent" and ("express" in printer.name.lower() or "schnell" in printer.name.lower()):
|
||||
suitability = "Perfekt"
|
||||
elif priority == "high" and recent_jobs_count <= 2:
|
||||
suitability = "Ausgezeichnet"
|
||||
elif recent_jobs_count == 0:
|
||||
suitability = "Optimal"
|
||||
|
||||
# Begründung generieren
|
||||
reason = f"Optimale Verfügbarkeit und geringe Auslastung im gewählten Zeitraum"
|
||||
|
||||
job_duration_hours = (end_date - start_date).total_seconds() / 3600
|
||||
start_hour = start_date.hour
|
||||
|
||||
if priority == "urgent":
|
||||
reason = "Schnellster verfügbarer Drucker für dringende Aufträge"
|
||||
elif start_hour >= 18 or start_hour <= 6:
|
||||
reason = "Speziell für Nachtschichten optimiert"
|
||||
elif job_duration_hours > 8:
|
||||
reason = "Zuverlässig für lange Druckaufträge"
|
||||
elif job_duration_hours <= 2:
|
||||
reason = "Optimal für schnelle Druckaufträge"
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"recommendation": {
|
||||
"printer_id": printer.id,
|
||||
"printer_name": f"{printer.name}",
|
||||
"location": printer.location or "Haupthalle",
|
||||
"reason": reason,
|
||||
"availability": f"{availability_percent}%",
|
||||
"utilization": f"{utilization_percent}%",
|
||||
"suitability": suitability,
|
||||
"recent_jobs": recent_jobs_count,
|
||||
"priority_optimized": priority in ["urgent", "high"] and suitability in ["Perfekt", "Ausgezeichnet"]
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der intelligenten Empfehlung: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
@ -1,855 +0,0 @@
|
||||
import json
|
||||
import secrets
|
||||
import bcrypt
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort, session, flash
|
||||
from flask_login import current_user, login_required
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from wtforms import StringField, TextAreaField, IntegerField, SelectField
|
||||
from wtforms.validators import DataRequired, Email, Optional, NumberRange
|
||||
from functools import wraps
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from models import GuestRequest, Job, Printer, User, UserPermission, Notification, get_cached_session
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
guest_blueprint = Blueprint('guest', __name__)
|
||||
logger = get_logger("guest")
|
||||
|
||||
# Flask-WTF Formular für Gastanfragen
|
||||
class GuestRequestForm(FlaskForm):
|
||||
name = StringField('Vollständiger Name', validators=[DataRequired()])
|
||||
email = StringField('E-Mail-Adresse', validators=[DataRequired(), Email()])
|
||||
printer_id = SelectField('Drucker auswählen', coerce=int, validators=[Optional()])
|
||||
duration_min = IntegerField('Geschätzte Dauer (Minuten)',
|
||||
validators=[DataRequired(), NumberRange(min=1, max=1440)],
|
||||
default=60)
|
||||
reason = TextAreaField('Projektbeschreibung', validators=[Optional()])
|
||||
file = FileField('3D-Datei hochladen',
|
||||
validators=[Optional(), FileAllowed(['stl', 'obj', '3mf', 'amf', 'gcode'],
|
||||
'3D-Dateien sind erlaubt!')])
|
||||
|
||||
# Hilfsfunktionen
|
||||
def can_approve_jobs(user_id):
|
||||
"""Prüft, ob ein Benutzer Anfragen genehmigen darf."""
|
||||
with get_cached_session() as db_session:
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
if not permission:
|
||||
return False
|
||||
return permission.can_approve_jobs
|
||||
|
||||
def approver_required(f):
|
||||
"""Decorator zur Prüfung der Genehmigungsberechtigung."""
|
||||
@wraps(f)
|
||||
@login_required
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not can_approve_jobs(current_user.id):
|
||||
abort(403, "Keine Berechtigung zum Genehmigen von Anfragen")
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
# Gast-Routen
|
||||
@guest_blueprint.route('/request', methods=['GET', 'POST'])
|
||||
def guest_request_form():
|
||||
"""Formular für Gastanfragen anzeigen und verarbeiten."""
|
||||
with get_cached_session() as db_session:
|
||||
# Aktive Drucker für SelectField laden
|
||||
printers = db_session.query(Printer).filter_by(active=True).all()
|
||||
|
||||
# Formular erstellen
|
||||
form = GuestRequestForm()
|
||||
|
||||
# Drucker-Optionen für SelectField setzen
|
||||
printer_choices = [(0, 'Keinen spezifischen Drucker auswählen')]
|
||||
printer_choices.extend([(p.id, f"{p.name} ({p.location or 'Kein Standort'})") for p in printers])
|
||||
form.printer_id.choices = printer_choices
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
# Daten aus dem Formular extrahieren
|
||||
name = form.name.data
|
||||
email = form.email.data
|
||||
reason = form.reason.data
|
||||
duration_min = form.duration_min.data
|
||||
printer_id = form.printer_id.data if form.printer_id.data != 0 else None
|
||||
|
||||
# IP-Adresse erfassen
|
||||
author_ip = request.remote_addr
|
||||
|
||||
# Drucker validieren, falls angegeben
|
||||
if printer_id:
|
||||
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
|
||||
if not printer:
|
||||
flash("Ungültiger Drucker ausgewählt.", "error")
|
||||
return render_template('guest_request.html', form=form, printers=printers)
|
||||
|
||||
# Neue Anfrage erstellen
|
||||
guest_request = GuestRequest(
|
||||
name=name,
|
||||
email=email,
|
||||
reason=reason,
|
||||
duration_min=duration_min,
|
||||
printer_id=printer_id,
|
||||
author_ip=author_ip
|
||||
)
|
||||
|
||||
db_session.add(guest_request)
|
||||
db_session.commit()
|
||||
|
||||
# Benachrichtigung für Genehmiger erstellen
|
||||
Notification.create_for_approvers(
|
||||
notification_type="guest_request",
|
||||
payload={
|
||||
"request_id": guest_request.id,
|
||||
"name": guest_request.name,
|
||||
"created_at": guest_request.created_at.isoformat(),
|
||||
"status": guest_request.status
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Neue Gastanfrage erstellt: ID {guest_request.id}, Name: {name}")
|
||||
flash("Ihr Antrag wurde erfolgreich eingereicht!", "success")
|
||||
|
||||
# Weiterleitung zur Status-Seite
|
||||
return redirect(url_for('guest.guest_request_status', request_id=guest_request.id))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen der Gastanfrage: {str(e)}")
|
||||
flash("Fehler beim Verarbeiten Ihres Antrags. Bitte versuchen Sie es erneut.", "error")
|
||||
|
||||
# Drucker-Liste von der Session trennen für Template-Verwendung
|
||||
db_session.expunge_all()
|
||||
|
||||
return render_template('guest_request.html', form=form, printers=printers)
|
||||
|
||||
@guest_blueprint.route('/start-job', methods=['GET'])
|
||||
def guest_start_job_form():
|
||||
"""Code-Eingabe-Formular für Gäste anzeigen."""
|
||||
return render_template('guest_start_job.html')
|
||||
|
||||
@guest_blueprint.route('/job/<int:job_id>/status', methods=['GET'])
|
||||
def guest_job_status(job_id):
|
||||
"""Job-Status-Seite für Gäste anzeigen."""
|
||||
with get_cached_session() as db_session:
|
||||
# Job mit eager loading des printer-Relationships laden
|
||||
job = db_session.query(Job).options(
|
||||
joinedload(Job.printer),
|
||||
joinedload(Job.user)
|
||||
).filter_by(id=job_id).first()
|
||||
|
||||
if not job:
|
||||
abort(404, "Job nicht gefunden")
|
||||
|
||||
# Zugehörige Gastanfrage finden
|
||||
guest_request = db_session.query(GuestRequest).filter_by(job_id=job_id).first()
|
||||
|
||||
# Objekte explizit von der Session trennen, um sie außerhalb verwenden zu können
|
||||
db_session.expunge(job)
|
||||
if guest_request:
|
||||
db_session.expunge(guest_request)
|
||||
|
||||
return render_template('guest_job_status.html',
|
||||
job=job,
|
||||
guest_request=guest_request)
|
||||
|
||||
@guest_blueprint.route('/requests/overview', methods=['GET'])
|
||||
def guest_requests_overview():
|
||||
"""Öffentliche Übersicht aller Druckanträge mit zensierten persönlichen Daten."""
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
# Alle Gastanfragen mit eager loading des printer-Relationships laden
|
||||
guest_requests = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer)
|
||||
).order_by(desc(GuestRequest.created_at)).all()
|
||||
|
||||
# Daten für Gäste aufbereiten (persönliche Daten zensieren)
|
||||
public_requests = []
|
||||
for req in guest_requests:
|
||||
# Name zensieren: Nur ersten Buchstaben und letzten Buchstaben anzeigen
|
||||
censored_name = "***"
|
||||
if req.name and len(req.name) > 0:
|
||||
if len(req.name) == 1:
|
||||
censored_name = req.name[0] + "***"
|
||||
elif len(req.name) == 2:
|
||||
censored_name = req.name[0] + "***" + req.name[-1]
|
||||
else:
|
||||
censored_name = req.name[0] + "***" + req.name[-1]
|
||||
|
||||
# E-Mail zensieren
|
||||
censored_email = "***@***.***"
|
||||
if req.email and "@" in req.email:
|
||||
email_parts = req.email.split("@")
|
||||
if len(email_parts[0]) > 2:
|
||||
censored_email = email_parts[0][:2] + "***@" + email_parts[1]
|
||||
else:
|
||||
censored_email = "***@" + email_parts[1]
|
||||
|
||||
# Grund zensieren (nur erste 20 Zeichen anzeigen)
|
||||
censored_reason = "***"
|
||||
if req.reason and len(req.reason) > 20:
|
||||
censored_reason = req.reason[:20] + "..."
|
||||
elif req.reason:
|
||||
censored_reason = req.reason
|
||||
|
||||
public_requests.append({
|
||||
"id": req.id,
|
||||
"name": censored_name,
|
||||
"email": censored_email,
|
||||
"reason": censored_reason,
|
||||
"duration_min": req.duration_min,
|
||||
"created_at": req.created_at,
|
||||
"status": req.status,
|
||||
"printer": req.printer.to_dict() if req.printer else None
|
||||
})
|
||||
|
||||
# Objekte explizit von der Session trennen
|
||||
db_session.expunge_all()
|
||||
|
||||
return render_template('guest_requests_overview.html', requests=public_requests)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der öffentlichen Gastanfragen: {str(e)}")
|
||||
return render_template('guest_requests_overview.html', requests=[], error="Fehler beim Laden der Anfragen")
|
||||
|
||||
@guest_blueprint.route('/request/<int:request_id>', methods=['GET'])
|
||||
def guest_request_status(request_id):
|
||||
"""Status einer Gastanfrage anzeigen."""
|
||||
with get_cached_session() as db_session:
|
||||
# Guest Request mit eager loading des printer-Relationships laden
|
||||
guest_request = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer)
|
||||
).filter_by(id=request_id).first()
|
||||
|
||||
if not guest_request:
|
||||
abort(404, "Anfrage nicht gefunden")
|
||||
|
||||
# OTP-Code nur anzeigen, wenn Anfrage genehmigt wurde
|
||||
otp_code = None
|
||||
show_start_link = False
|
||||
|
||||
if guest_request.status == "approved":
|
||||
if not guest_request.otp_code:
|
||||
# OTP generieren falls noch nicht vorhanden
|
||||
otp_code = guest_request.generate_otp()
|
||||
db_session.commit()
|
||||
else:
|
||||
# OTP existiert bereits - prüfen ob noch nicht verwendet
|
||||
show_start_link = guest_request.otp_used_at is None
|
||||
|
||||
# Zugehörigen Job laden, falls vorhanden
|
||||
job = None
|
||||
if guest_request.job_id:
|
||||
job = db_session.query(Job).filter_by(id=guest_request.job_id).first()
|
||||
|
||||
# Objekte explizit von der Session trennen, um sie außerhalb verwenden zu können
|
||||
db_session.expunge(guest_request)
|
||||
if job:
|
||||
db_session.expunge(job)
|
||||
|
||||
return render_template('guest_status.html',
|
||||
request=guest_request,
|
||||
job=job,
|
||||
otp_code=otp_code,
|
||||
show_start_link=show_start_link)
|
||||
|
||||
# API-Endpunkte
|
||||
@guest_blueprint.route('/api/guest/requests', methods=['POST'])
|
||||
def api_create_guest_request():
|
||||
"""Neue Gastanfrage erstellen."""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
# Pflichtfelder prüfen
|
||||
name = data.get('name')
|
||||
if not name:
|
||||
return jsonify({"error": "Name ist erforderlich"}), 400
|
||||
|
||||
# Optionale Felder
|
||||
email = data.get('email')
|
||||
reason = data.get('reason')
|
||||
duration_min = data.get('duration_min', 60) # Standard: 1 Stunde
|
||||
printer_id = data.get('printer_id')
|
||||
|
||||
# IP-Adresse erfassen
|
||||
author_ip = request.remote_addr
|
||||
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
# Drucker prüfen
|
||||
if printer_id:
|
||||
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
|
||||
|
||||
# Neue Anfrage erstellen
|
||||
guest_request = GuestRequest(
|
||||
name=name,
|
||||
email=email,
|
||||
reason=reason,
|
||||
duration_min=duration_min,
|
||||
printer_id=printer_id,
|
||||
author_ip=author_ip
|
||||
)
|
||||
|
||||
db_session.add(guest_request)
|
||||
db_session.commit()
|
||||
|
||||
# Benachrichtigung für Genehmiger erstellen
|
||||
Notification.create_for_approvers(
|
||||
notification_type="guest_request",
|
||||
payload={
|
||||
"request_id": guest_request.id,
|
||||
"name": guest_request.name,
|
||||
"created_at": guest_request.created_at.isoformat(),
|
||||
"status": guest_request.status
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Neue Gastanfrage erstellt: ID {guest_request.id}, Name: {name}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"request_id": guest_request.id,
|
||||
"status": guest_request.status,
|
||||
"redirect_url": url_for('guest.guest_request_status', request_id=guest_request.id)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/guest/start-job', methods=['POST'])
|
||||
def api_start_job_with_code():
|
||||
"""Job mit OTP-Code starten."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'code' not in data:
|
||||
return jsonify({"error": "Code ist erforderlich"}), 400
|
||||
|
||||
code = data['code'].strip().upper()
|
||||
if len(code) != 6:
|
||||
return jsonify({"error": "Code muss 6 Zeichen lang sein"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Alle genehmigten Gastanfragen mit OTP-Codes finden
|
||||
guest_requests = db_session.query(GuestRequest).filter(
|
||||
GuestRequest.status == "approved",
|
||||
GuestRequest.otp_code.isnot(None),
|
||||
GuestRequest.otp_used_at.is_(None) # Noch nicht verwendet
|
||||
).all()
|
||||
|
||||
matching_request = None
|
||||
for req in guest_requests:
|
||||
# Code validieren
|
||||
if req.verify_otp(code):
|
||||
matching_request = req
|
||||
break
|
||||
|
||||
if not matching_request:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Ungültiger oder bereits verwendeter Code"
|
||||
}), 400
|
||||
|
||||
# Prüfen ob zugehöriger Job existiert
|
||||
if not matching_request.job_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Kein zugehöriger Job gefunden"
|
||||
}), 400
|
||||
|
||||
job = db_session.query(Job).options(
|
||||
joinedload(Job.printer)
|
||||
).filter_by(id=matching_request.job_id).first()
|
||||
|
||||
if not job:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Job nicht gefunden"
|
||||
}), 400
|
||||
|
||||
# Prüfen ob Job noch startbar ist
|
||||
if job.status not in ["scheduled", "waiting_for_printer"]:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Job kann im Status '{job.status}' nicht gestartet werden"
|
||||
}), 400
|
||||
|
||||
# Job starten
|
||||
now = datetime.now()
|
||||
job.status = "running"
|
||||
job.start_at = now
|
||||
job.end_at = now + timedelta(minutes=matching_request.duration_min)
|
||||
job.actual_start_time = now
|
||||
|
||||
# OTP als verwendet markieren
|
||||
matching_request.otp_used_at = now
|
||||
|
||||
# Drucker einschalten (falls implementiert)
|
||||
if job.printer and job.printer.plug_ip:
|
||||
try:
|
||||
from utils.job_scheduler import toggle_plug
|
||||
toggle_plug(job.printer_id, True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Einschalten des Druckers: {str(e)}")
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Job {job.id} mit OTP-Code gestartet für Gastanfrage {matching_request.id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"job_id": job.id,
|
||||
"job_name": job.name,
|
||||
"start_time": job.start_at.strftime("%H:%M"),
|
||||
"end_time": job.end_at.strftime("%H:%M"),
|
||||
"duration_minutes": matching_request.duration_min,
|
||||
"printer_name": job.printer.name if job.printer else "Unbekannt",
|
||||
"message": f"Job '{job.name}' erfolgreich gestartet"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Starten des Jobs mit Code: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Fehler beim Starten des Jobs"
|
||||
}), 500
|
||||
|
||||
@guest_blueprint.route('/api/guest/requests/<int:request_id>', methods=['GET'])
|
||||
def api_get_guest_request(request_id):
|
||||
"""Status einer Gastanfrage abrufen."""
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
||||
|
||||
# OTP wird nie über die API zurückgegeben
|
||||
response_data = guest_request.to_dict()
|
||||
response_data.pop("otp_code", None)
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/guest/job/<int:job_id>/status', methods=['GET'])
|
||||
def api_get_guest_job_status(job_id):
|
||||
"""Job-Status für Gäste abrufen."""
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
# Job mit Drucker-Information laden
|
||||
job = db_session.query(Job).options(
|
||||
joinedload(Job.printer)
|
||||
).filter_by(id=job_id).first()
|
||||
|
||||
if not job:
|
||||
return jsonify({"error": "Job nicht gefunden"}), 404
|
||||
|
||||
# Zugehörige Gastanfrage prüfen
|
||||
guest_request = db_session.query(GuestRequest).filter_by(job_id=job_id).first()
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Kein Gastjob"}), 403
|
||||
|
||||
# Aktuelle Zeit für Berechnungen
|
||||
now = datetime.now()
|
||||
|
||||
# Restzeit berechnen
|
||||
remaining_minutes = 0
|
||||
if job.status == "running" and job.end_at:
|
||||
remaining_seconds = (job.end_at - now).total_seconds()
|
||||
remaining_minutes = max(0, int(remaining_seconds / 60))
|
||||
|
||||
# Fortschritt berechnen
|
||||
progress_percent = 0
|
||||
if job.status == "running" and job.start_at and job.end_at:
|
||||
total_duration = (job.end_at - job.start_at).total_seconds()
|
||||
elapsed_duration = (now - job.start_at).total_seconds()
|
||||
progress_percent = min(100, max(0, int((elapsed_duration / total_duration) * 100)))
|
||||
elif job.status in ["completed", "finished"]:
|
||||
progress_percent = 100
|
||||
|
||||
job_data = {
|
||||
"id": job.id,
|
||||
"name": job.name,
|
||||
"status": job.status,
|
||||
"start_at": job.start_at.isoformat() if job.start_at else None,
|
||||
"end_at": job.end_at.isoformat() if job.end_at else None,
|
||||
"duration_minutes": job.duration_minutes,
|
||||
"remaining_minutes": remaining_minutes,
|
||||
"progress_percent": progress_percent,
|
||||
"printer": {
|
||||
"id": job.printer.id,
|
||||
"name": job.printer.name,
|
||||
"location": job.printer.location
|
||||
} if job.printer else None,
|
||||
"guest_request": {
|
||||
"id": guest_request.id,
|
||||
"name": guest_request.name,
|
||||
"created_at": guest_request.created_at.isoformat()
|
||||
},
|
||||
"is_active": job.status in ["scheduled", "running"],
|
||||
"is_completed": job.status in ["completed", "finished"],
|
||||
"is_failed": job.status in ["failed", "cancelled"]
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"job": job_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen des Job-Status: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/notifications', methods=['GET'])
|
||||
@login_required
|
||||
def api_get_notifications():
|
||||
"""Benachrichtigungen für den aktuellen Benutzer abrufen."""
|
||||
try:
|
||||
# Zeitstempel für Filter (nur neue Benachrichtigungen)
|
||||
since = request.args.get('since')
|
||||
if since:
|
||||
try:
|
||||
since_date = datetime.fromisoformat(since)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
else:
|
||||
since_date = None
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
query = db_session.query(Notification).filter_by(
|
||||
user_id=current_user.id,
|
||||
read=False
|
||||
)
|
||||
|
||||
if since_date:
|
||||
query = query.filter(Notification.created_at > since_date)
|
||||
|
||||
notifications = query.order_by(desc(Notification.created_at)).all()
|
||||
|
||||
return jsonify({
|
||||
"count": len(notifications),
|
||||
"notifications": [n.to_dict() for n in notifications]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Benachrichtigungen: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/notifications/<int:notification_id>/read', methods=['POST'])
|
||||
@login_required
|
||||
def api_mark_notification_read(notification_id):
|
||||
"""Benachrichtigung als gelesen markieren."""
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
notification = db_session.query(Notification).filter_by(
|
||||
id=notification_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not notification:
|
||||
return jsonify({"error": "Benachrichtigung nicht gefunden"}), 404
|
||||
|
||||
notification.read = True
|
||||
db_session.commit()
|
||||
|
||||
return jsonify({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Markieren der Benachrichtigung als gelesen: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/admin/requests', methods=['GET'])
|
||||
@approver_required
|
||||
def api_get_all_requests():
|
||||
"""Alle Gastanfragen für Admins abrufen."""
|
||||
try:
|
||||
# Filter-Parameter
|
||||
status_filter = request.args.get('status', 'all') # all, pending, approved, denied
|
||||
limit = int(request.args.get('limit', 50))
|
||||
offset = int(request.args.get('offset', 0))
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Query mit eager loading
|
||||
query = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer),
|
||||
joinedload(GuestRequest.job),
|
||||
joinedload(GuestRequest.processed_by_user)
|
||||
)
|
||||
|
||||
# Status-Filter anwenden
|
||||
if status_filter != 'all':
|
||||
query = query.filter(GuestRequest.status == status_filter)
|
||||
|
||||
# Sortierung: Pending zuerst, dann nach Erstellungsdatum
|
||||
query = query.order_by(
|
||||
desc(GuestRequest.status == 'pending'),
|
||||
desc(GuestRequest.created_at)
|
||||
)
|
||||
|
||||
# Pagination
|
||||
total_count = query.count()
|
||||
requests = query.offset(offset).limit(limit).all()
|
||||
|
||||
# Daten für Admin aufbereiten
|
||||
admin_requests = []
|
||||
for req in requests:
|
||||
request_data = req.to_dict()
|
||||
|
||||
# Zusätzliche Admin-Informationen
|
||||
request_data.update({
|
||||
"can_be_processed": req.status == "pending",
|
||||
"is_overdue": (
|
||||
req.status == "approved" and
|
||||
req.job and
|
||||
req.job.end_at < datetime.now()
|
||||
) if req.job else False,
|
||||
"time_since_creation": (datetime.now() - req.created_at).total_seconds() / 3600 if req.created_at else 0 # Stunden
|
||||
})
|
||||
|
||||
admin_requests.append(request_data)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"requests": admin_requests,
|
||||
"pagination": {
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"has_more": (offset + limit) < total_count
|
||||
},
|
||||
"stats": {
|
||||
"total": total_count,
|
||||
"pending": db_session.query(GuestRequest).filter_by(status='pending').count(),
|
||||
"approved": db_session.query(GuestRequest).filter_by(status='approved').count(),
|
||||
"denied": db_session.query(GuestRequest).filter_by(status='denied').count()
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Admin-Gastanfragen: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/admin/requests/<int:request_id>', methods=['GET'])
|
||||
@approver_required
|
||||
def api_get_request_details(request_id):
|
||||
"""Detaillierte Informationen zu einer Gastanfrage für Admins abrufen."""
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
guest_request = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer),
|
||||
joinedload(GuestRequest.job),
|
||||
joinedload(GuestRequest.processed_by_user)
|
||||
).filter_by(id=request_id).first()
|
||||
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
||||
|
||||
# Vollständige Admin-Informationen
|
||||
request_data = guest_request.to_dict()
|
||||
|
||||
# Verfügbare Drucker für Zuweisung
|
||||
available_printers = db_session.query(Printer).filter_by(active=True).all()
|
||||
request_data["available_printers"] = [p.to_dict() for p in available_printers]
|
||||
|
||||
# Job-Historie falls vorhanden
|
||||
if guest_request.job:
|
||||
job_data = guest_request.job.to_dict()
|
||||
job_data["is_active"] = guest_request.job.status in ["scheduled", "running"]
|
||||
job_data["is_overdue"] = guest_request.job.end_at < datetime.now() if guest_request.job.end_at else False
|
||||
request_data["job_details"] = job_data
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"request": request_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Gastanfrage-Details: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/admin/requests/<int:request_id>/update', methods=['PUT'])
|
||||
@approver_required
|
||||
def api_update_request(request_id):
|
||||
"""Gastanfrage aktualisieren (nur für Admins)."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
||||
|
||||
# Erlaubte Felder für Updates
|
||||
allowed_fields = ['printer_id', 'duration_min', 'approval_notes', 'rejection_reason']
|
||||
changes_made = False
|
||||
|
||||
for field in allowed_fields:
|
||||
if field in data:
|
||||
if field == 'printer_id' and data[field]:
|
||||
# Drucker validieren
|
||||
printer = db_session.query(Printer).filter_by(id=data[field], active=True).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
|
||||
|
||||
setattr(guest_request, field, data[field])
|
||||
changes_made = True
|
||||
|
||||
if changes_made:
|
||||
guest_request.processed_by = current_user.id
|
||||
guest_request.processed_at = datetime.now()
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Gastanfrage {request_id} aktualisiert von Admin {current_user.id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Anfrage erfolgreich aktualisiert",
|
||||
"updated_by": current_user.name,
|
||||
"updated_at": guest_request.processed_at.isoformat()
|
||||
})
|
||||
else:
|
||||
return jsonify({"error": "Keine Änderungen vorgenommen"}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktualisieren der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
# Admin-Routen
|
||||
@guest_blueprint.route('/admin/requests', methods=['GET'])
|
||||
@approver_required
|
||||
def admin_requests_management():
|
||||
"""Admin-Oberfläche für die Verwaltung von Gastanfragen."""
|
||||
return render_template('admin_guest_requests.html')
|
||||
|
||||
@guest_blueprint.route('/api/requests/<int:request_id>/approve', methods=['POST'])
|
||||
@approver_required
|
||||
def api_approve_request(request_id):
|
||||
"""Gastanfrage genehmigen."""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
approval_notes = data.get('notes', '')
|
||||
printer_id = data.get('printer_id') # Optional: Drucker zuweisen/ändern
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
||||
|
||||
if guest_request.status != "pending":
|
||||
return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400
|
||||
|
||||
# Drucker validieren, falls angegeben
|
||||
if printer_id:
|
||||
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
|
||||
guest_request.printer_id = printer_id
|
||||
|
||||
# Sicherstellen, dass ein Drucker zugewiesen ist
|
||||
if not guest_request.printer_id:
|
||||
return jsonify({"error": "Kein Drucker zugewiesen. Bitte wählen Sie einen Drucker aus."}), 400
|
||||
|
||||
# Anfrage genehmigen
|
||||
guest_request.status = "approved"
|
||||
guest_request.processed_by = current_user.id
|
||||
guest_request.processed_at = datetime.now()
|
||||
guest_request.approval_notes = approval_notes
|
||||
|
||||
# OTP generieren
|
||||
otp_plain = guest_request.generate_otp()
|
||||
|
||||
# Zugehörigen Job erstellen
|
||||
start_time = datetime.now() + timedelta(minutes=5) # Start in 5 Minuten
|
||||
end_time = start_time + timedelta(minutes=guest_request.duration_min)
|
||||
|
||||
# Admin-Benutzer als Eigentümer verwenden
|
||||
admin_user = db_session.query(User).filter_by(is_admin=True).first()
|
||||
if not admin_user:
|
||||
admin_user = current_user # Fallback auf aktuellen Benutzer
|
||||
|
||||
job = Job(
|
||||
name=f"Gastauftrag: {guest_request.name}",
|
||||
description=guest_request.reason or "Gastauftrag",
|
||||
user_id=admin_user.id,
|
||||
printer_id=guest_request.printer_id,
|
||||
start_at=start_time,
|
||||
end_at=end_time,
|
||||
status="scheduled",
|
||||
duration_minutes=guest_request.duration_min,
|
||||
owner_id=admin_user.id
|
||||
)
|
||||
|
||||
db_session.add(job)
|
||||
db_session.flush() # ID generieren
|
||||
|
||||
# Job-ID in Gastanfrage speichern
|
||||
guest_request.job_id = job.id
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Gastanfrage {request_id} genehmigt von Admin {current_user.id} ({current_user.username})")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"status": "approved",
|
||||
"job_id": job.id,
|
||||
"otp": otp_plain, # Nur in dieser Antwort wird der OTP-Klartext zurückgegeben
|
||||
"approved_by": current_user.username,
|
||||
"approved_at": guest_request.processed_at.isoformat(),
|
||||
"notes": approval_notes,
|
||||
"message": f"Anfrage genehmigt. Zugangscode: {otp_plain}"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Genehmigen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@guest_blueprint.route('/api/requests/<int:request_id>/deny', methods=['POST'])
|
||||
@approver_required
|
||||
def api_deny_request(request_id):
|
||||
"""Gastanfrage ablehnen."""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
rejection_reason = data.get('reason', '')
|
||||
|
||||
if not rejection_reason.strip():
|
||||
return jsonify({"error": "Ablehnungsgrund ist erforderlich"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
||||
if not guest_request:
|
||||
return jsonify({"error": "Anfrage nicht gefunden"}), 404
|
||||
|
||||
if guest_request.status != "pending":
|
||||
return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400
|
||||
|
||||
# Anfrage ablehnen
|
||||
guest_request.status = "denied"
|
||||
guest_request.processed_by = current_user.id
|
||||
guest_request.processed_at = datetime.now()
|
||||
guest_request.rejection_reason = rejection_reason
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Gastanfrage {request_id} abgelehnt von Admin {current_user.id} ({current_user.username}): {rejection_reason}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"status": "denied",
|
||||
"rejected_by": current_user.username,
|
||||
"rejected_at": guest_request.processed_at.isoformat(),
|
||||
"reason": rejection_reason,
|
||||
"message": "Anfrage wurde abgelehnt"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ablehnen der Gastanfrage: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
@ -1,181 +0,0 @@
|
||||
"""
|
||||
Drucker-Blueprint für MYP Platform
|
||||
Enthält alle Routen und Funktionen zur Druckerverwaltung, Statusüberwachung und Steuerung.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, request, jsonify, current_app, abort, Response
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
from werkzeug.exceptions import NotFound, BadRequest
|
||||
from sqlalchemy import func, desc, asc
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from typing import Dict, List, Tuple, Any, Optional
|
||||
|
||||
from models import Printer, User, Job, get_db_session
|
||||
from utils.logging_config import get_logger, measure_execution_time
|
||||
from utils.permissions import require_permission, Permission, check_permission
|
||||
from utils.printer_monitor import printer_monitor
|
||||
|
||||
# Logger initialisieren
|
||||
printers_logger = get_logger("printers")
|
||||
|
||||
# Blueprint erstellen
|
||||
printers_blueprint = Blueprint("printers", __name__, url_prefix="/api/printers")
|
||||
|
||||
@printers_blueprint.route("/monitor/live-status", methods=["GET"])
|
||||
@login_required
|
||||
@measure_execution_time(logger=printers_logger, task_name="API-Live-Drucker-Status-Abfrage")
|
||||
def get_live_printer_status():
|
||||
"""
|
||||
Liefert den aktuellen Live-Status aller Drucker.
|
||||
|
||||
Query-Parameter:
|
||||
- use_cache: ob Cache verwendet werden soll (default: true)
|
||||
|
||||
Returns:
|
||||
JSON mit Live-Status aller Drucker
|
||||
"""
|
||||
printers_logger.info(f"🔄 Live-Status-Abfrage von Benutzer {current_user.name} (ID: {current_user.id})")
|
||||
|
||||
# Parameter auslesen
|
||||
use_cache_param = request.args.get("use_cache", "true").lower()
|
||||
use_cache = use_cache_param == "true"
|
||||
|
||||
try:
|
||||
# Live-Status über den PrinterMonitor abrufen
|
||||
status_data = printer_monitor.get_live_printer_status(use_session_cache=use_cache)
|
||||
|
||||
# Zusammenfassung der Druckerstatus erstellen
|
||||
summary = printer_monitor.get_printer_summary()
|
||||
|
||||
# Antwort mit Status und Zusammenfassung
|
||||
response = {
|
||||
"success": True,
|
||||
"status": status_data,
|
||||
"summary": summary,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"cache_used": use_cache
|
||||
}
|
||||
|
||||
printers_logger.info(f"✅ Live-Status-Abfrage erfolgreich: {len(status_data)} Drucker")
|
||||
return jsonify(response)
|
||||
|
||||
except Exception as e:
|
||||
printers_logger.error(f"❌ Fehler bei Live-Status-Abfrage: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Fehler bei Abfrage des Druckerstatus",
|
||||
"message": str(e)
|
||||
}), 500
|
||||
|
||||
@printers_blueprint.route("/control/<int:printer_id>/power", methods=["POST"])
|
||||
@login_required
|
||||
@require_permission(Permission.CONTROL_PRINTER) # Verwende die bereits vorhandene Berechtigung
|
||||
@measure_execution_time(logger=printers_logger, task_name="API-Drucker-Stromversorgung-Steuerung")
|
||||
def control_printer_power(printer_id):
|
||||
"""
|
||||
Steuert die Stromversorgung eines Druckers (ein-/ausschalten).
|
||||
|
||||
Args:
|
||||
printer_id: ID des zu steuernden Druckers
|
||||
|
||||
JSON-Parameter:
|
||||
- action: "on" oder "off"
|
||||
|
||||
Returns:
|
||||
JSON mit Ergebnis der Steuerungsaktion
|
||||
"""
|
||||
printers_logger.info(f"🔌 Stromsteuerung für Drucker {printer_id} von Benutzer {current_user.name}")
|
||||
|
||||
# Parameter validieren
|
||||
data = request.get_json()
|
||||
if not data or "action" not in data:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Parameter 'action' fehlt"
|
||||
}), 400
|
||||
|
||||
action = data["action"]
|
||||
if action not in ["on", "off"]:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Ungültige Aktion. Erlaubt sind 'on' oder 'off'."
|
||||
}), 400
|
||||
|
||||
try:
|
||||
# Drucker aus Datenbank holen
|
||||
db_session = get_db_session()
|
||||
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
|
||||
|
||||
if not printer:
|
||||
db_session.close()
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Drucker mit ID {printer_id} nicht gefunden"
|
||||
}), 404
|
||||
|
||||
# Prüfen, ob Drucker eine Steckdose konfiguriert hat
|
||||
if not printer.plug_ip or not printer.plug_username or not printer.plug_password:
|
||||
db_session.close()
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Drucker {printer.name} hat keine Steckdose konfiguriert"
|
||||
}), 400
|
||||
|
||||
# Steckdose steuern
|
||||
from PyP100 import PyP110
|
||||
try:
|
||||
# TP-Link Tapo P110 Verbindung herstellen
|
||||
p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password)
|
||||
p110.handshake() # Authentifizierung
|
||||
p110.login() # Login
|
||||
|
||||
# Steckdose ein- oder ausschalten
|
||||
if action == "on":
|
||||
p110.turnOn()
|
||||
success = True
|
||||
message = "Steckdose erfolgreich eingeschaltet"
|
||||
printer.status = "starting" # Status aktualisieren
|
||||
else:
|
||||
p110.turnOff()
|
||||
success = True
|
||||
message = "Steckdose erfolgreich ausgeschaltet"
|
||||
printer.status = "offline" # Status aktualisieren
|
||||
|
||||
# Zeitpunkt der letzten Prüfung aktualisieren
|
||||
printer.last_checked = datetime.now()
|
||||
db_session.commit()
|
||||
|
||||
# Cache leeren, damit neue Status-Abfragen aktuell sind
|
||||
printer_monitor.clear_all_caches()
|
||||
|
||||
printers_logger.info(f"✅ {action.upper()}: Drucker {printer.name} erfolgreich {message}")
|
||||
|
||||
except Exception as e:
|
||||
printers_logger.error(f"❌ Fehler bei Steckdosensteuerung für {printer.name}: {str(e)}")
|
||||
db_session.close()
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Fehler bei Steckdosensteuerung: {str(e)}"
|
||||
}), 500
|
||||
|
||||
db_session.close()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": message,
|
||||
"printer_id": printer_id,
|
||||
"printer_name": printer.name,
|
||||
"action": action,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
printers_logger.error(f"❌ Allgemeiner Fehler bei Stromsteuerung: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Allgemeiner Fehler: {str(e)}"
|
||||
}), 500
|
@ -1,168 +0,0 @@
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from functools import wraps
|
||||
|
||||
from models import User, UserPermission, get_cached_session
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
users_blueprint = Blueprint('users', __name__)
|
||||
logger = get_logger("users")
|
||||
|
||||
def users_admin_required(f):
|
||||
"""Decorator zur Prüfung der Admin-Berechtigung für Users Blueprint."""
|
||||
@wraps(f)
|
||||
@login_required
|
||||
def users_decorated_function(*args, **kwargs):
|
||||
if not current_user.is_admin:
|
||||
abort(403, "Nur Administratoren haben Zugriff auf diese Seite")
|
||||
return f(*args, **kwargs)
|
||||
return users_decorated_function
|
||||
|
||||
@users_blueprint.route('/admin/users/<int:user_id>/permissions', methods=['GET'])
|
||||
@users_admin_required
|
||||
def admin_user_permissions(user_id):
|
||||
"""Benutzerberechtigungen anzeigen und bearbeiten."""
|
||||
with get_cached_session() as db_session:
|
||||
user = db_session.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
abort(404, "Benutzer nicht gefunden")
|
||||
|
||||
# Berechtigungen laden oder erstellen, falls nicht vorhanden
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
if not permission:
|
||||
permission = UserPermission(user_id=user_id)
|
||||
db_session.add(permission)
|
||||
db_session.commit()
|
||||
|
||||
return render_template('admin_user_permissions.html', user=user, permission=permission)
|
||||
|
||||
@users_blueprint.route('/api/users/<int:user_id>/permissions', methods=['GET'])
|
||||
@login_required
|
||||
def api_get_user_permissions(user_id):
|
||||
"""Benutzerberechtigungen als JSON zurückgeben."""
|
||||
# Nur Admins oder der Benutzer selbst darf seine Berechtigungen sehen
|
||||
if not current_user.is_admin and current_user.id != user_id:
|
||||
return jsonify({"error": "Keine Berechtigung"}), 403
|
||||
|
||||
try:
|
||||
with get_cached_session() as db_session:
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
|
||||
if not permission:
|
||||
# Falls keine Berechtigungen existieren, Standard-Werte zurückgeben
|
||||
return jsonify({
|
||||
"user_id": user_id,
|
||||
"can_start_jobs": False,
|
||||
"needs_approval": True,
|
||||
"can_approve_jobs": False
|
||||
})
|
||||
|
||||
return jsonify(permission.to_dict())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Benutzerberechtigungen: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@users_blueprint.route('/api/users/<int:user_id>/permissions', methods=['PUT'])
|
||||
@users_admin_required
|
||||
def api_update_user_permissions(user_id):
|
||||
"""Benutzerberechtigungen aktualisieren."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Benutzer prüfen
|
||||
user = db_session.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
return jsonify({"error": "Benutzer nicht gefunden"}), 404
|
||||
|
||||
# Berechtigungen laden oder erstellen
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
if not permission:
|
||||
permission = UserPermission(user_id=user_id)
|
||||
db_session.add(permission)
|
||||
|
||||
# Berechtigungen aktualisieren
|
||||
if 'can_start_jobs' in data:
|
||||
permission.can_start_jobs = bool(data['can_start_jobs'])
|
||||
|
||||
if 'needs_approval' in data:
|
||||
permission.needs_approval = bool(data['needs_approval'])
|
||||
|
||||
if 'can_approve_jobs' in data:
|
||||
permission.can_approve_jobs = bool(data['can_approve_jobs'])
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Berechtigungen für Benutzer {user_id} aktualisiert durch Admin {current_user.id}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"permissions": permission.to_dict()
|
||||
})
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Datenbankfehler beim Aktualisieren der Benutzerberechtigungen: {str(e)}")
|
||||
return jsonify({"error": "Datenbankfehler beim Verarbeiten der Anfrage"}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktualisieren der Benutzerberechtigungen: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@users_blueprint.route('/admin/users/<int:user_id>/permissions/update', methods=['POST'])
|
||||
@users_admin_required
|
||||
def admin_update_user_permissions(user_id):
|
||||
"""Benutzerberechtigungen über Formular aktualisieren."""
|
||||
try:
|
||||
# Formularfelder auslesen
|
||||
can_start_jobs = request.form.get('can_start_jobs') == 'on'
|
||||
needs_approval = request.form.get('needs_approval') == 'on'
|
||||
can_approve_jobs = request.form.get('can_approve_jobs') == 'on'
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Benutzer prüfen
|
||||
user = db_session.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
abort(404, "Benutzer nicht gefunden")
|
||||
|
||||
# Berechtigungen laden oder erstellen
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
if not permission:
|
||||
permission = UserPermission(user_id=user_id)
|
||||
db_session.add(permission)
|
||||
|
||||
# Berechtigungen aktualisieren
|
||||
permission.can_start_jobs = can_start_jobs
|
||||
permission.needs_approval = needs_approval
|
||||
permission.can_approve_jobs = can_approve_jobs
|
||||
|
||||
db_session.commit()
|
||||
|
||||
logger.info(f"Berechtigungen für Benutzer {user_id} aktualisiert durch Admin {current_user.id} (Formular)")
|
||||
|
||||
return redirect(url_for('users.admin_user_permissions', user_id=user_id))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktualisieren der Benutzerberechtigungen: {str(e)}")
|
||||
abort(500, "Fehler beim Verarbeiten der Anfrage")
|
||||
|
||||
# Erweiterung des bestehenden Benutzer-Bearbeitungsformulars
|
||||
@users_blueprint.route('/admin/users/<int:user_id>/edit/permissions', methods=['GET'])
|
||||
@users_admin_required
|
||||
def admin_edit_user_permissions_section(user_id):
|
||||
"""Rendert nur den Berechtigungsteil für das Benutzer-Edit-Formular."""
|
||||
with get_cached_session() as db_session:
|
||||
user = db_session.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
abort(404, "Benutzer nicht gefunden")
|
||||
|
||||
# Berechtigungen laden oder erstellen, falls nicht vorhanden
|
||||
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
|
||||
if not permission:
|
||||
permission = UserPermission(user_id=user_id)
|
||||
db_session.add(permission)
|
||||
db_session.commit()
|
||||
|
||||
return render_template('_user_permissions_form.html', user=user, permission=permission)
|
@ -1,74 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Configuration Package for MYP Platform
|
||||
======================================
|
||||
|
||||
This package contains all configuration modules for the Mercedes-Benz 3D Printing Platform.
|
||||
|
||||
Modules:
|
||||
- security: Security configuration and middleware
|
||||
- database: Database configuration and settings
|
||||
- logging: Logging configuration
|
||||
- app_config: Main application configuration
|
||||
"""
|
||||
|
||||
__version__ = "2.0.0"
|
||||
__author__ = "MYP Development Team"
|
||||
|
||||
# Import main configuration modules
|
||||
try:
|
||||
from .security import SecurityConfig, get_security_headers
|
||||
from .app_config import Config, DevelopmentConfig, ProductionConfig, TestingConfig
|
||||
except ImportError as e:
|
||||
print(f"Warning: Could not import configuration modules: {e}")
|
||||
# Fallback configurations
|
||||
SecurityConfig = None
|
||||
get_security_headers = None
|
||||
Config = None
|
||||
|
||||
# Export main configuration classes
|
||||
__all__ = [
|
||||
'SecurityConfig',
|
||||
'get_security_headers',
|
||||
'Config',
|
||||
'DevelopmentConfig',
|
||||
'ProductionConfig',
|
||||
'TestingConfig'
|
||||
]
|
||||
|
||||
def get_config(config_name='development'):
|
||||
"""
|
||||
Get configuration object based on environment name.
|
||||
|
||||
Args:
|
||||
config_name (str): Configuration environment name
|
||||
|
||||
Returns:
|
||||
Config: Configuration object
|
||||
"""
|
||||
configs = {
|
||||
'development': DevelopmentConfig,
|
||||
'production': ProductionConfig,
|
||||
'testing': TestingConfig
|
||||
}
|
||||
|
||||
return configs.get(config_name, DevelopmentConfig)
|
||||
|
||||
def validate_config(config_obj):
|
||||
"""
|
||||
Validate configuration object.
|
||||
|
||||
Args:
|
||||
config_obj: Configuration object to validate
|
||||
|
||||
Returns:
|
||||
bool: True if valid, False otherwise
|
||||
"""
|
||||
required_attrs = ['SECRET_KEY', 'DATABASE_URL']
|
||||
|
||||
for attr in required_attrs:
|
||||
if not hasattr(config_obj, attr):
|
||||
print(f"Missing required configuration: {attr}")
|
||||
return False
|
||||
|
||||
return True
|
@ -1,181 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Application Configuration Module for MYP Platform
|
||||
================================================
|
||||
|
||||
Flask configuration classes for different environments.
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
# Base configuration directory
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(BASE_DIR, '..', '..'))
|
||||
|
||||
class Config:
|
||||
"""Base configuration class with common settings."""
|
||||
|
||||
# Secret key for Flask sessions and CSRF protection
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production-744563017196A'
|
||||
|
||||
# Session configuration
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(hours=24)
|
||||
SESSION_COOKIE_SECURE = False # Set to True in production with HTTPS
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
|
||||
# Database configuration
|
||||
DATABASE_URL = os.environ.get('DATABASE_URL') or f'sqlite:///{os.path.join(PROJECT_ROOT, "data", "myp_platform.db")}'
|
||||
SQLALCHEMY_DATABASE_URI = DATABASE_URL
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
'pool_pre_ping': True,
|
||||
'pool_recycle': 300,
|
||||
}
|
||||
|
||||
# Upload configuration
|
||||
UPLOAD_FOLDER = os.path.join(PROJECT_ROOT, 'uploads')
|
||||
MAX_CONTENT_LENGTH = 500 * 1024 * 1024 # 500MB max file size
|
||||
ALLOWED_EXTENSIONS = {'gcode', 'stl', 'obj', '3mf', 'amf'}
|
||||
|
||||
# Security configuration
|
||||
WTF_CSRF_ENABLED = True
|
||||
WTF_CSRF_TIME_LIMIT = 3600 # 1 hour
|
||||
|
||||
# Mail configuration (optional)
|
||||
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
|
||||
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
|
||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||
|
||||
# Logging configuration
|
||||
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')
|
||||
LOG_FILE_MAX_BYTES = 10 * 1024 * 1024 # 10MB
|
||||
LOG_BACKUP_COUNT = 5
|
||||
|
||||
# Application-specific settings
|
||||
SCHEDULER_ENABLED = os.environ.get('SCHEDULER_ENABLED', 'true').lower() in ['true', 'on', '1']
|
||||
SCHEDULER_INTERVAL = int(os.environ.get('SCHEDULER_INTERVAL', '60')) # seconds
|
||||
|
||||
# SSL/HTTPS configuration
|
||||
SSL_ENABLED = os.environ.get('SSL_ENABLED', 'false').lower() in ['true', 'on', '1']
|
||||
SSL_CERT_PATH = os.environ.get('SSL_CERT_PATH')
|
||||
SSL_KEY_PATH = os.environ.get('SSL_KEY_PATH')
|
||||
|
||||
# Network configuration
|
||||
DEFAULT_PORT = int(os.environ.get('PORT', '5000'))
|
||||
DEFAULT_HOST = os.environ.get('HOST', '0.0.0.0')
|
||||
|
||||
@staticmethod
|
||||
def init_app(app):
|
||||
"""Initialize application with this configuration."""
|
||||
pass
|
||||
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
"""Development environment configuration."""
|
||||
|
||||
DEBUG = True
|
||||
TESTING = False
|
||||
|
||||
# More verbose logging in development
|
||||
LOG_LEVEL = 'DEBUG'
|
||||
|
||||
# Disable some security features for easier development
|
||||
SESSION_COOKIE_SECURE = False
|
||||
WTF_CSRF_ENABLED = False # Disable CSRF for easier API testing
|
||||
|
||||
@staticmethod
|
||||
def init_app(app):
|
||||
Config.init_app(app)
|
||||
|
||||
# Development-specific initialization
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
"""Testing environment configuration."""
|
||||
|
||||
TESTING = True
|
||||
DEBUG = True
|
||||
|
||||
# Use in-memory database for testing
|
||||
DATABASE_URL = 'sqlite:///:memory:'
|
||||
SQLALCHEMY_DATABASE_URI = DATABASE_URL
|
||||
|
||||
# Disable CSRF for testing
|
||||
WTF_CSRF_ENABLED = False
|
||||
|
||||
# Shorter session lifetime for testing
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(minutes=5)
|
||||
|
||||
@staticmethod
|
||||
def init_app(app):
|
||||
Config.init_app(app)
|
||||
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""Production environment configuration."""
|
||||
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
|
||||
# Strict security settings for production
|
||||
SESSION_COOKIE_SECURE = True # Requires HTTPS
|
||||
WTF_CSRF_ENABLED = True
|
||||
|
||||
# Production logging
|
||||
LOG_LEVEL = 'WARNING'
|
||||
|
||||
# SSL should be enabled in production
|
||||
SSL_ENABLED = True
|
||||
|
||||
@staticmethod
|
||||
def init_app(app):
|
||||
Config.init_app(app)
|
||||
|
||||
# Production-specific initialization
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
# Set up file logging for production
|
||||
log_dir = os.path.join(os.path.dirname(app.instance_path), 'logs')
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
|
||||
file_handler = RotatingFileHandler(
|
||||
os.path.join(log_dir, 'myp_platform.log'),
|
||||
maxBytes=Config.LOG_FILE_MAX_BYTES,
|
||||
backupCount=Config.LOG_BACKUP_COUNT
|
||||
)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
||||
))
|
||||
file_handler.setLevel(logging.WARNING)
|
||||
app.logger.addHandler(file_handler)
|
||||
app.logger.setLevel(logging.WARNING)
|
||||
|
||||
|
||||
# Configuration dictionary for easy access
|
||||
config = {
|
||||
'development': DevelopmentConfig,
|
||||
'testing': TestingConfig,
|
||||
'production': ProductionConfig,
|
||||
'default': DevelopmentConfig
|
||||
}
|
||||
|
||||
|
||||
def get_config_by_name(config_name):
|
||||
"""
|
||||
Get configuration class by name.
|
||||
|
||||
Args:
|
||||
config_name (str): Name of the configuration ('development', 'testing', 'production')
|
||||
|
||||
Returns:
|
||||
Config: Configuration class
|
||||
"""
|
||||
return config.get(config_name, config['default'])
|
@ -1,81 +0,0 @@
|
||||
"""
|
||||
Sicherheitskonfiguration für die MYP Platform
|
||||
"""
|
||||
|
||||
# Sicherheits-Headers für HTTP-Responses
|
||||
SECURITY_HEADERS = {
|
||||
'Content-Security-Policy': (
|
||||
"default-src 'self'; "
|
||||
"script-src 'self' 'unsafe-eval' 'unsafe-inline'; "
|
||||
"script-src-elem 'self' 'unsafe-inline'; "
|
||||
"style-src 'self' 'unsafe-inline'; "
|
||||
"font-src 'self'; "
|
||||
"img-src 'self' data:; "
|
||||
"connect-src 'self'; "
|
||||
"worker-src 'self' blob:; "
|
||||
"frame-src 'none'; "
|
||||
"object-src 'none'; "
|
||||
"base-uri 'self'; "
|
||||
"form-action 'self'; "
|
||||
"frame-ancestors 'none';"
|
||||
),
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
'X-XSS-Protection': '1; mode=block',
|
||||
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()'
|
||||
}
|
||||
|
||||
# Rate Limiting Konfiguration
|
||||
RATE_LIMITS = {
|
||||
'default': "200 per day, 50 per hour",
|
||||
'login': "5 per minute",
|
||||
'api': "100 per hour",
|
||||
'admin': "500 per hour"
|
||||
}
|
||||
|
||||
# Session-Sicherheit
|
||||
SESSION_CONFIG = {
|
||||
'SESSION_COOKIE_SECURE': False, # Für Offline-Betrieb auf False setzen
|
||||
'SESSION_COOKIE_HTTPONLY': True,
|
||||
'SESSION_COOKIE_SAMESITE': 'Lax',
|
||||
'PERMANENT_SESSION_LIFETIME': 3600 # 1 Stunde
|
||||
}
|
||||
|
||||
# CSRF-Schutz
|
||||
CSRF_CONFIG = {
|
||||
'CSRF_ENABLED': True,
|
||||
'CSRF_SESSION_KEY': 'csrf_token',
|
||||
'CSRF_TIME_LIMIT': 3600
|
||||
}
|
||||
|
||||
class SecurityConfig:
|
||||
"""Sicherheitskonfiguration für die Anwendung"""
|
||||
|
||||
def __init__(self):
|
||||
self.headers = SECURITY_HEADERS
|
||||
self.rate_limits = RATE_LIMITS
|
||||
self.session_config = SESSION_CONFIG
|
||||
self.csrf_config = CSRF_CONFIG
|
||||
|
||||
def get_headers(self):
|
||||
"""Gibt die Sicherheits-Headers zurück"""
|
||||
return self.headers
|
||||
|
||||
def get_rate_limits(self):
|
||||
"""Gibt die Rate-Limiting-Konfiguration zurück"""
|
||||
return self.rate_limits
|
||||
|
||||
def get_session_config(self):
|
||||
"""Gibt die Session-Konfiguration zurück"""
|
||||
return self.session_config
|
||||
|
||||
def get_csrf_config(self):
|
||||
"""Gibt die CSRF-Konfiguration zurück"""
|
||||
return self.csrf_config
|
||||
|
||||
|
||||
def get_security_headers():
|
||||
"""Gibt die Sicherheits-Headers zurück"""
|
||||
return SECURITY_HEADERS
|
@ -1,187 +0,0 @@
|
||||
import os
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
def get_env_variable(name: str, default: str = None) -> str:
|
||||
"""
|
||||
Holt eine Umgebungsvariable oder gibt den Standardwert zurück.
|
||||
|
||||
Args:
|
||||
name: Name der Umgebungsvariable
|
||||
default: Standardwert, falls die Variable nicht gesetzt ist
|
||||
|
||||
Returns:
|
||||
str: Wert der Umgebungsvariable oder Standardwert
|
||||
"""
|
||||
return os.environ.get(name, default)
|
||||
|
||||
# Hardcodierte Konfiguration
|
||||
SECRET_KEY = "7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F"
|
||||
|
||||
# Dynamische Pfade basierend auf dem aktuellen Arbeitsverzeichnis
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # backend/app
|
||||
PROJECT_ROOT = os.path.dirname(BASE_DIR) # backend
|
||||
DATABASE_PATH = os.path.join(BASE_DIR, "database", "myp.db")
|
||||
|
||||
# ===== SMART PLUG KONFIGURATION =====
|
||||
# TP-Link Tapo P110 Standardkonfiguration
|
||||
TAPO_USERNAME = "till.tomczak@mercedes-benz.com"
|
||||
TAPO_PASSWORD = "744563017196A"
|
||||
|
||||
# Automatische Steckdosen-Erkennung aktivieren
|
||||
TAPO_AUTO_DISCOVERY = True
|
||||
|
||||
# Standard-Steckdosen-IPs (diese können später in der Datenbank überschrieben werden)
|
||||
DEFAULT_TAPO_IPS = [
|
||||
"192.168.0.103", # Erreichbare Steckdose laut Test
|
||||
"192.168.0.104", # Erreichbare Steckdose laut Test
|
||||
"192.168.0.100",
|
||||
"192.168.0.101",
|
||||
"192.168.0.102",
|
||||
"192.168.0.105"
|
||||
]
|
||||
|
||||
# Timeout-Konfiguration für Tapo-Verbindungen
|
||||
TAPO_TIMEOUT = 10 # Sekunden
|
||||
TAPO_RETRY_COUNT = 3 # Anzahl Wiederholungsversuche
|
||||
|
||||
# Drucker-Konfiguration
|
||||
PRINTERS = {
|
||||
"Printer 1": {"ip": "192.168.0.100"},
|
||||
"Printer 2": {"ip": "192.168.0.101"},
|
||||
"Printer 3": {"ip": "192.168.0.102"},
|
||||
"Printer 4": {"ip": "192.168.0.103"},
|
||||
"Printer 5": {"ip": "192.168.0.104"},
|
||||
"Printer 6": {"ip": "192.168.0.106"}
|
||||
}
|
||||
|
||||
# Logging-Konfiguration
|
||||
LOG_DIR = os.path.join(BASE_DIR, "logs")
|
||||
LOG_SUBDIRS = ["app", "scheduler", "auth", "jobs", "printers", "errors"]
|
||||
LOG_LEVEL = "INFO"
|
||||
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
# Flask-Konfiguration
|
||||
FLASK_HOST = "0.0.0.0"
|
||||
FLASK_PORT = 443 # Geändert von 443 auf 8443 (nicht-privilegierter Port)
|
||||
FLASK_FALLBACK_PORT = 8080 # Geändert von 80 auf 8080 (nicht-privilegierter Port)
|
||||
FLASK_DEBUG = True
|
||||
SESSION_LIFETIME = timedelta(hours=2) # Reduziert von 7 Tagen auf 2 Stunden für bessere Sicherheit
|
||||
|
||||
# Upload-Konfiguration
|
||||
UPLOAD_FOLDER = os.path.join(BASE_DIR, "uploads")
|
||||
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'gcode', '3mf', 'stl'}
|
||||
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB Maximum-Dateigröße
|
||||
|
||||
# Umgebungskonfiguration
|
||||
ENVIRONMENT = get_env_variable("MYP_ENVIRONMENT", "development")
|
||||
|
||||
# SSL-Konfiguration
|
||||
SSL_ENABLED = get_env_variable("MYP_SSL_ENABLED", "True").lower() in ("true", "1", "yes")
|
||||
SSL_CERT_PATH = os.path.join(BASE_DIR, "certs", "myp.crt")
|
||||
SSL_KEY_PATH = os.path.join(BASE_DIR, "certs", "myp.key")
|
||||
SSL_HOSTNAME = get_env_variable("MYP_SSL_HOSTNAME", "localhost")
|
||||
|
||||
# Scheduler-Konfiguration
|
||||
SCHEDULER_INTERVAL = 60 # Sekunden
|
||||
SCHEDULER_ENABLED = True
|
||||
|
||||
# Datenbank-Konfiguration
|
||||
DB_ENGINE = f"sqlite:///{DATABASE_PATH}"
|
||||
|
||||
def get_log_file(category: str) -> str:
|
||||
"""
|
||||
Gibt den Pfad zur Log-Datei für eine bestimmte Kategorie zurück.
|
||||
|
||||
Args:
|
||||
category: Log-Kategorie (app, scheduler, auth, jobs, printers, errors)
|
||||
|
||||
Returns:
|
||||
str: Pfad zur Log-Datei
|
||||
"""
|
||||
if category not in LOG_SUBDIRS:
|
||||
category = "app"
|
||||
|
||||
return os.path.join(LOG_DIR, category, f"{category}.log")
|
||||
|
||||
def ensure_log_directories():
|
||||
"""Erstellt alle erforderlichen Log-Verzeichnisse."""
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
for subdir in LOG_SUBDIRS:
|
||||
os.makedirs(os.path.join(LOG_DIR, subdir), exist_ok=True)
|
||||
|
||||
def ensure_database_directory():
|
||||
"""Erstellt das Datenbank-Verzeichnis."""
|
||||
db_dir = os.path.dirname(DATABASE_PATH)
|
||||
if db_dir:
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
def ensure_ssl_directory():
|
||||
"""Erstellt das SSL-Verzeichnis, falls es nicht existiert."""
|
||||
ssl_dir = os.path.dirname(SSL_CERT_PATH)
|
||||
if ssl_dir and not os.path.exists(ssl_dir):
|
||||
os.makedirs(ssl_dir, exist_ok=True)
|
||||
|
||||
def ensure_upload_directory():
|
||||
"""Erstellt das Upload-Verzeichnis, falls es nicht existiert."""
|
||||
if not os.path.exists(UPLOAD_FOLDER):
|
||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||
|
||||
def get_ssl_context():
|
||||
"""
|
||||
Gibt den SSL-Kontext für Flask zurück, wenn SSL aktiviert ist.
|
||||
|
||||
Returns:
|
||||
tuple oder None: Tuple mit Zertifikat- und Schlüsselpfad, wenn SSL aktiviert ist, sonst None
|
||||
"""
|
||||
if not SSL_ENABLED:
|
||||
return None
|
||||
|
||||
# Wenn Zertifikate nicht existieren, diese automatisch erstellen
|
||||
if not os.path.exists(SSL_CERT_PATH) or not os.path.exists(SSL_KEY_PATH):
|
||||
ensure_ssl_directory()
|
||||
|
||||
# Im Entwicklungsmodus versuchen wir, einfache Zertifikate zu erstellen
|
||||
if FLASK_DEBUG:
|
||||
print("SSL-Zertifikate nicht gefunden. Erstelle einfache selbstsignierte Zertifikate...")
|
||||
try:
|
||||
# Einfache Zertifikate mit Python erstellen
|
||||
create_simple_ssl_cert()
|
||||
|
||||
# Prüfen, ob die Zertifikate erfolgreich erstellt wurden
|
||||
if not os.path.exists(SSL_CERT_PATH) or not os.path.exists(SSL_KEY_PATH):
|
||||
print("Konnte keine SSL-Zertifikate erstellen.")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Erstellen der SSL-Zertifikate: {e}")
|
||||
return None
|
||||
else:
|
||||
print("WARNUNG: SSL-Zertifikate nicht gefunden und Nicht-Debug-Modus. SSL wird deaktiviert.")
|
||||
return None
|
||||
|
||||
return (SSL_CERT_PATH, SSL_KEY_PATH)
|
||||
|
||||
def create_simple_ssl_cert():
|
||||
"""
|
||||
Erstellt ein Mercedes-Benz SSL-Zertifikat mit dem neuen SSL-Manager.
|
||||
"""
|
||||
try:
|
||||
# Verwende den neuen SSL-Manager
|
||||
from utils.ssl_manager import ssl_manager
|
||||
success = ssl_manager.generate_mercedes_certificate()
|
||||
|
||||
if success:
|
||||
print(f"Mercedes-Benz SSL-Zertifikat erfolgreich erstellt: {SSL_CERT_PATH}")
|
||||
return True
|
||||
else:
|
||||
print("Fehler beim Erstellen des Mercedes-Benz SSL-Zertifikats")
|
||||
return None
|
||||
|
||||
except ImportError as e:
|
||||
print(f"SSL-Manager nicht verfügbar: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Erstellen der SSL-Zertifikate: {e}")
|
||||
return None
|
@ -1,116 +0,0 @@
|
||||
import os
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
# Hardcodierte Konfiguration
|
||||
SECRET_KEY = "7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F"
|
||||
DATABASE_PATH = "database/myp.db"
|
||||
TAPO_USERNAME = "till.tomczak@mercedes-benz.com"
|
||||
TAPO_PASSWORD = "744563017196A"
|
||||
|
||||
# Drucker-Konfiguration
|
||||
PRINTERS = {
|
||||
"Printer 1": {"ip": "192.168.0.100"},
|
||||
"Printer 2": {"ip": "192.168.0.101"},
|
||||
"Printer 3": {"ip": "192.168.0.102"},
|
||||
"Printer 4": {"ip": "192.168.0.103"},
|
||||
"Printer 5": {"ip": "192.168.0.104"},
|
||||
"Printer 6": {"ip": "192.168.0.106"}
|
||||
}
|
||||
|
||||
# Logging-Konfiguration
|
||||
LOG_DIR = "logs"
|
||||
LOG_SUBDIRS = ["app", "scheduler", "auth", "jobs", "printers", "errors"]
|
||||
LOG_LEVEL = "INFO"
|
||||
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
# Flask-Konfiguration
|
||||
FLASK_HOST = "0.0.0.0"
|
||||
FLASK_PORT = 443
|
||||
FLASK_FALLBACK_PORT = 80
|
||||
FLASK_DEBUG = True
|
||||
SESSION_LIFETIME = timedelta(days=7)
|
||||
|
||||
# SSL-Konfiguration
|
||||
SSL_ENABLED = True
|
||||
SSL_CERT_PATH = "instance/ssl/myp.crt"
|
||||
SSL_KEY_PATH = "instance/ssl/myp.key"
|
||||
SSL_HOSTNAME = "raspberrypi"
|
||||
|
||||
# Scheduler-Konfiguration
|
||||
SCHEDULER_INTERVAL = 60 # Sekunden
|
||||
SCHEDULER_ENABLED = True
|
||||
|
||||
# Datenbank-Konfiguration
|
||||
DB_ENGINE = f"sqlite:///{DATABASE_PATH}"
|
||||
|
||||
def get_log_file(category: str) -> str:
|
||||
"""
|
||||
Gibt den Pfad zur Log-Datei für eine bestimmte Kategorie zurück.
|
||||
|
||||
Args:
|
||||
category: Log-Kategorie (app, scheduler, auth, jobs, printers, errors)
|
||||
|
||||
Returns:
|
||||
str: Pfad zur Log-Datei
|
||||
"""
|
||||
if category not in LOG_SUBDIRS:
|
||||
category = "app"
|
||||
|
||||
return os.path.join(LOG_DIR, category, f"{category}.log")
|
||||
|
||||
def ensure_log_directories():
|
||||
"""Erstellt alle erforderlichen Log-Verzeichnisse."""
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
for subdir in LOG_SUBDIRS:
|
||||
os.makedirs(os.path.join(LOG_DIR, subdir), exist_ok=True)
|
||||
|
||||
def ensure_database_directory():
|
||||
"""Erstellt das Datenbank-Verzeichnis."""
|
||||
db_dir = os.path.dirname(DATABASE_PATH)
|
||||
if db_dir:
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
def ensure_ssl_directory():
|
||||
"""Erstellt das SSL-Verzeichnis, falls es nicht existiert."""
|
||||
ssl_dir = os.path.dirname(SSL_CERT_PATH)
|
||||
if ssl_dir and not os.path.exists(ssl_dir):
|
||||
os.makedirs(ssl_dir, exist_ok=True)
|
||||
|
||||
def get_ssl_context():
|
||||
"""
|
||||
Gibt den SSL-Kontext für Flask zurück, wenn SSL aktiviert ist.
|
||||
|
||||
Returns:
|
||||
tuple oder None: Tuple mit Zertifikat- und Schlüsselpfad, wenn SSL aktiviert ist, sonst None
|
||||
"""
|
||||
if not SSL_ENABLED:
|
||||
return None
|
||||
|
||||
# Wenn Zertifikate nicht existieren, diese automatisch erstellen
|
||||
if not os.path.exists(SSL_CERT_PATH) or not os.path.exists(SSL_KEY_PATH):
|
||||
ensure_ssl_directory()
|
||||
|
||||
# Prüfen, ob wir uns im Entwicklungsmodus befinden
|
||||
if FLASK_DEBUG:
|
||||
print("SSL-Zertifikate nicht gefunden. Erstelle selbstsignierte Zertifikate...")
|
||||
|
||||
# Pfad zum create_ssl_cert.sh-Skript ermitteln
|
||||
script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
||||
"install", "create_ssl_cert.sh")
|
||||
|
||||
# Ausführungsrechte setzen
|
||||
if os.path.exists(script_path):
|
||||
os.system(f"chmod +x {script_path}")
|
||||
|
||||
# Zertifikate erstellen mit spezifischem Hostnamen
|
||||
os.system(f"{script_path} -c {SSL_CERT_PATH} -k {SSL_KEY_PATH} -h {SSL_HOSTNAME}")
|
||||
else:
|
||||
print(f"WARNUNG: SSL-Zertifikat-Generator nicht gefunden: {script_path}")
|
||||
return None
|
||||
else:
|
||||
print("WARNUNG: SSL-Zertifikate nicht gefunden und Nicht-Debug-Modus. SSL wird deaktiviert.")
|
||||
return None
|
||||
|
||||
return (SSL_CERT_PATH, SSL_KEY_PATH)
|
@ -1,2 +0,0 @@
|
||||
# Database package initialization file
|
||||
# Makes the directory a proper Python package
|
@ -1,133 +0,0 @@
|
||||
import os
|
||||
import logging
|
||||
from typing import List, Optional, Any
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import create_engine, func
|
||||
from sqlalchemy.orm import sessionmaker, Session, joinedload
|
||||
|
||||
from models import User, Printer, Job, Stats, Base
|
||||
from config.settings import DATABASE_PATH, ensure_database_directory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DatabaseManager:
|
||||
"""Database manager class to handle database operations."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the database manager."""
|
||||
ensure_database_directory()
|
||||
self.engine = create_engine(f"sqlite:///{DATABASE_PATH}")
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
|
||||
def get_session(self) -> Session:
|
||||
"""Get a new database session.
|
||||
|
||||
Returns:
|
||||
Session: A new SQLAlchemy session.
|
||||
"""
|
||||
return self.Session()
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""Test the database connection.
|
||||
|
||||
Returns:
|
||||
bool: True if the connection is successful, False otherwise.
|
||||
"""
|
||||
try:
|
||||
session = self.get_session()
|
||||
session.execute("SELECT 1")
|
||||
session.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Database connection test failed: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_all_jobs(self) -> List[Job]:
|
||||
"""Get all jobs with eager loading of relationships.
|
||||
|
||||
Returns:
|
||||
List[Job]: A list of all jobs.
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
jobs = session.query(Job).options(
|
||||
joinedload(Job.user),
|
||||
joinedload(Job.printer)
|
||||
).all()
|
||||
return jobs
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_jobs_by_status(self, status: str) -> List[Job]:
|
||||
"""Get jobs by status with eager loading of relationships.
|
||||
|
||||
Args:
|
||||
status: The job status to filter by.
|
||||
|
||||
Returns:
|
||||
List[Job]: A list of jobs with the specified status.
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
jobs = session.query(Job).options(
|
||||
joinedload(Job.user),
|
||||
joinedload(Job.printer)
|
||||
).filter(Job.status == status).all()
|
||||
return jobs
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_job_by_id(self, job_id: int) -> Optional[Job]:
|
||||
"""Get a job by ID with eager loading of relationships.
|
||||
|
||||
Args:
|
||||
job_id: The job ID to find.
|
||||
|
||||
Returns:
|
||||
Optional[Job]: The job if found, None otherwise.
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
job = session.query(Job).options(
|
||||
joinedload(Job.user),
|
||||
joinedload(Job.printer)
|
||||
).filter(Job.id == job_id).first()
|
||||
return job
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_available_printers(self) -> List[Printer]:
|
||||
"""Get all available printers.
|
||||
|
||||
Returns:
|
||||
List[Printer]: A list of available printers.
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
printers = session.query(Printer).filter(
|
||||
Printer.active == True,
|
||||
Printer.status != "busy"
|
||||
).all()
|
||||
return printers
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_jobs_since(self, since_date: datetime) -> List[Job]:
|
||||
"""Get jobs created since a specific date.
|
||||
|
||||
Args:
|
||||
since_date: The date to filter jobs from.
|
||||
|
||||
Returns:
|
||||
List[Job]: A list of jobs created since the specified date.
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
jobs = session.query(Job).options(
|
||||
joinedload(Job.user),
|
||||
joinedload(Job.printer)
|
||||
).filter(Job.created_at >= since_date).all()
|
||||
return jobs
|
||||
finally:
|
||||
session.close()
|
@ -1,345 +0,0 @@
|
||||
# Admin Panel & Einstellungen - Reparaturen und Verbesserungen
|
||||
|
||||
## Übersicht der durchgeführten Arbeiten
|
||||
|
||||
### 🔧 Reparierte Admin-Panel Funktionen
|
||||
|
||||
#### 1. Fehlende API-Endpunkte hinzugefügt
|
||||
|
||||
- **`/api/admin/cache/clear`** - System-Cache leeren
|
||||
- **`/api/admin/system/restart`** - System-Neustart (Development)
|
||||
- **`/api/admin/printers/update-all`** - Alle Drucker-Status aktualisieren
|
||||
- **`/api/admin/settings`** (GET/POST) - Admin-Einstellungen verwalten
|
||||
- **`/api/admin/logs/export`** - System-Logs exportieren
|
||||
|
||||
#### 2. JavaScript-Funktionen implementiert
|
||||
|
||||
- **`showSystemSettings()`** - System-Einstellungen Modal
|
||||
- **`saveSystemSettings()`** - Einstellungen speichern
|
||||
- **`updateAllPrinters()`** - Drucker-Status aktualisieren
|
||||
- **`restartSystem()`** - System-Neustart
|
||||
- **`managePrinter()`** - Drucker-Verwaltung
|
||||
- **`showPrinterSettings()`** - Drucker-Einstellungen
|
||||
- **`editUser()`** - Benutzer bearbeiten
|
||||
- **`deleteUser()`** - Benutzer löschen
|
||||
- **`handleJobAction()`** - Job-Aktionen (cancel, delete, finish)
|
||||
- **`filterUsers()`** - Benutzer-Suche
|
||||
- **`filterJobs()`** - Job-Filter
|
||||
- **`exportData()`** - Daten-Export
|
||||
- **`loadAnalyticsData()`** - Analytics laden
|
||||
- **`startLiveAnalytics()`** - Live-Analytics starten
|
||||
|
||||
#### 3. UI-Verbesserungen
|
||||
|
||||
- **Loading-Overlay** hinzugefügt für bessere UX
|
||||
- **Moderne Benachrichtigungen** mit Animationen
|
||||
- **Responsive Modals** für alle Admin-Funktionen
|
||||
- **Fehlerbehandlung** für alle API-Aufrufe
|
||||
- **CSRF-Token** Unterstützung für Sicherheit
|
||||
|
||||
### ⚙️ Reparierte Einstellungen-Funktionen
|
||||
|
||||
#### 1. API-Endpunkte für Benutzereinstellungen
|
||||
|
||||
- **`/api/user/settings`** (GET) - Einstellungen abrufen
|
||||
- **`/user/update-settings`** (POST) - Einstellungen speichern (erweitert)
|
||||
|
||||
#### 2. JavaScript-Funktionen implementiert
|
||||
|
||||
- **`loadUserSettings()`** - Einstellungen beim Laden abrufen
|
||||
- **`saveAllSettings()`** - Alle Einstellungen speichern
|
||||
- **Theme-Switcher** - Vollständig funktional
|
||||
- **Kontrast-Einstellungen** - Implementiert
|
||||
- **Benachrichtigungseinstellungen** - Funktional
|
||||
- **Datenschutz & Sicherheit** - Vollständig implementiert
|
||||
|
||||
#### 3. Persistierung
|
||||
|
||||
- **Session-basierte Speicherung** als Fallback
|
||||
- **Datenbank-Integration** vorbereitet
|
||||
- **LocalStorage** für Theme und Kontrast
|
||||
- **Automatisches Laden** beim Seitenaufruf
|
||||
|
||||
### 🛡️ Sicherheitsverbesserungen
|
||||
|
||||
#### 1. CSRF-Schutz
|
||||
|
||||
- **CSRF-Token** in allen Templates
|
||||
- **Token-Validierung** in allen API-Aufrufen
|
||||
- **Sichere Headers** für AJAX-Requests
|
||||
|
||||
#### 2. Admin-Berechtigung
|
||||
|
||||
- **`@admin_required`** Decorator für alle Admin-Funktionen
|
||||
- **Berechtigungsprüfung** in JavaScript
|
||||
- **Sichere API-Endpunkte** mit Validierung
|
||||
|
||||
#### 3. Fehlerbehandlung
|
||||
|
||||
- **Try-Catch** Blöcke in allen Funktionen
|
||||
- **Benutzerfreundliche Fehlermeldungen**
|
||||
- **Logging** für alle kritischen Operationen
|
||||
|
||||
### 📊 Funktionale Verbesserungen
|
||||
|
||||
#### 1. Real-Time Updates
|
||||
|
||||
- **Live-Statistiken** alle 30 Sekunden
|
||||
- **System-Status** alle 10 Sekunden
|
||||
- **Drucker-Status** mit Caching
|
||||
- **Job-Monitoring** in Echtzeit
|
||||
|
||||
#### 2. Performance-Optimierungen
|
||||
|
||||
- **Caching-System** für häufige Abfragen
|
||||
- **Lazy Loading** für große Datensätze
|
||||
- **Optimierte Datenbankabfragen**
|
||||
- **Session-basiertes Caching**
|
||||
|
||||
#### 3. Benutzerfreundlichkeit
|
||||
|
||||
- **Animierte Übergänge** und Effekte
|
||||
- **Responsive Design** für alle Geräte
|
||||
- **Intuitive Navigation** und Bedienung
|
||||
- **Sofortiges Feedback** bei Aktionen
|
||||
|
||||
## 🧪 Getestete Funktionen
|
||||
|
||||
### Admin Panel
|
||||
|
||||
✅ **System-Cache leeren** - Funktional
|
||||
✅ **Datenbank optimieren** - Funktional
|
||||
✅ **Backup erstellen** - Funktional
|
||||
✅ **System-Einstellungen** - Modal funktional
|
||||
✅ **Drucker aktualisieren** - Funktional
|
||||
✅ **System-Neustart** - Funktional (Development)
|
||||
✅ **Benutzer-Verwaltung** - CRUD-Operationen
|
||||
✅ **Drucker-Verwaltung** - Vollständig funktional
|
||||
✅ **Job-Verwaltung** - Alle Aktionen verfügbar
|
||||
✅ **Live-Analytics** - Real-time Updates
|
||||
✅ **Log-Export** - ZIP-Download funktional
|
||||
|
||||
### Einstellungen
|
||||
|
||||
✅ **Theme-Switcher** - Light/Dark/System
|
||||
✅ **Kontrast-Einstellungen** - Normal/Hoch
|
||||
✅ **Benachrichtigungen** - Alle Optionen
|
||||
✅ **Datenschutz & Sicherheit** - Vollständig
|
||||
✅ **Automatisches Laden** - Beim Seitenaufruf
|
||||
✅ **Persistierung** - Session & LocalStorage
|
||||
|
||||
## 🔄 Nächste Schritte
|
||||
|
||||
### Empfohlene Verbesserungen
|
||||
|
||||
1. **Datenbank-Schema erweitern** um `settings` Spalte in User-Tabelle
|
||||
2. **WebSocket-Integration** für noch bessere Real-time Updates
|
||||
3. **Erweiterte Analytics** mit Charts und Grafiken
|
||||
4. **Backup-Scheduling** für automatische Backups
|
||||
5. **Erweiterte Benutzerrollen** und Berechtigungen
|
||||
|
||||
### Wartung
|
||||
|
||||
- **Regelmäßige Cache-Bereinigung** implementiert
|
||||
- **Automatische Datenbank-Optimierung** alle 5 Minuten
|
||||
- **Log-Rotation** für bessere Performance
|
||||
- **Session-Management** optimiert
|
||||
|
||||
## 📝 Technische Details
|
||||
|
||||
### Verwendete Technologien
|
||||
|
||||
- **Backend**: Flask, SQLAlchemy, SQLite mit WAL-Modus
|
||||
- **Frontend**: Vanilla JavaScript, Tailwind CSS
|
||||
- **Sicherheit**: CSRF-Token, Admin-Decorators
|
||||
- **Performance**: Caching, Lazy Loading, Optimierte Queries
|
||||
|
||||
### Architektur-Verbesserungen
|
||||
|
||||
- **Modulare JavaScript-Struktur** für bessere Wartbarkeit
|
||||
- **Einheitliche API-Responses** mit Erfolgs-/Fehler-Handling
|
||||
- **Konsistente Fehlerbehandlung** in allen Komponenten
|
||||
- **Responsive Design-Patterns** für alle Bildschirmgrößen
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ **VOLLSTÄNDIG FUNKTIONAL**
|
||||
**Letzte Aktualisierung**: 27.05.2025
|
||||
**Getestet auf**: Windows 10, Python 3.x, Flask 2.x
|
||||
|
||||
# Admin Panel Features Dokumentation
|
||||
|
||||
## Neue Features - Gastanfragen-Verwaltung
|
||||
|
||||
**Datum:** 2025-05-29 12:20:00
|
||||
**Feature:** Vollständige Administrator-Oberfläche für Gastanfragen mit Genehmigung/Ablehnung und Begründungen
|
||||
|
||||
### Implementierte Features
|
||||
|
||||
### 1. Erweiterte Datenbank-Struktur ✅
|
||||
**Neue Felder in `guest_requests` Tabelle:**
|
||||
- `processed_by` (INTEGER) - ID des Admins der die Anfrage bearbeitet hat
|
||||
- `processed_at` (DATETIME) - Zeitpunkt der Bearbeitung
|
||||
- `approval_notes` (TEXT) - Notizen bei Genehmigung
|
||||
- `rejection_reason` (TEXT) - Grund bei Ablehnung
|
||||
|
||||
**Migration durchgeführt:** Alle neuen Felder erfolgreich hinzugefügt
|
||||
|
||||
### 2. Erweiterte API-Endpoints ✅
|
||||
|
||||
#### Admin-Verwaltung:
|
||||
- `GET /api/admin/requests` - Alle Gastanfragen mit Filterung und Pagination
|
||||
- `GET /api/admin/requests/<id>` - Detaillierte Anfrage-Informationen
|
||||
- `PUT /api/admin/requests/<id>/update` - Anfrage aktualisieren
|
||||
|
||||
#### Erweiterte Genehmigung/Ablehnung:
|
||||
- `POST /api/requests/<id>/approve` - Mit Begründungen und Drucker-Zuweisung
|
||||
- `POST /api/requests/<id>/deny` - Mit verpflichtender Ablehnungsbegründung
|
||||
|
||||
### 3. Admin-Oberfläche ✅
|
||||
|
||||
**Route:** `/admin/requests`
|
||||
**Template:** `admin_guest_requests.html`
|
||||
|
||||
**Features:**
|
||||
- ✅ **Übersichtliche Darstellung** aller Gastanfragen
|
||||
- ✅ **Echtzeit-Statistiken** (Gesamt, Wartend, Genehmigt, Abgelehnt)
|
||||
- ✅ **Filter-System** nach Status (Alle, Wartend, Genehmigt, Abgelehnt)
|
||||
- ✅ **Such-Funktion** nach Name, E-Mail, Begründung
|
||||
- ✅ **Pagination** für große Anzahl von Anfragen
|
||||
- ✅ **Dringlichkeits-Kennzeichnung** für Anfragen > 24h alt
|
||||
- ✅ **Drucker-Zuweisung** bei Genehmigung
|
||||
- ✅ **Verpflichtende Begründung** bei Ablehnung
|
||||
- ✅ **Detail-Modal** mit vollständigen Informationen
|
||||
- ✅ **Aktions-Tracking** (wer hat wann bearbeitet)
|
||||
|
||||
### 4. Benutzerfreundlichkeit ✅
|
||||
|
||||
#### Design:
|
||||
- **Moderne UI** mit Tailwind CSS
|
||||
- **Responsive Design** für Desktop und Mobile
|
||||
- **Intuitive Icons** und Status-Badges
|
||||
- **Color-Coding** für verschiedene Status
|
||||
|
||||
#### Funktionalität:
|
||||
- **Ein-Klick-Aktionen** für Genehmigung/Ablehnung
|
||||
- **Modale Dialoge** für detaillierte Bearbeitung
|
||||
- **Echtzeit-Updates** nach Aktionen
|
||||
- **Fehlerbehandlung** mit benutzerfreundlichen Meldungen
|
||||
|
||||
### 5. Admin-Workflow ✅
|
||||
|
||||
#### Genehmigungsworkflow:
|
||||
1. **Drucker auswählen** (optional, falls nicht bereits zugewiesen)
|
||||
2. **Genehmigungsnotizen** hinzufügen (optional)
|
||||
3. **Automatische Job-Erstellung** mit OTP-Generierung
|
||||
4. **Admin-Tracking** wird gespeichert
|
||||
|
||||
#### Ablehnungsworkflow:
|
||||
1. **Verpflichtende Begründung** eingeben
|
||||
2. **Detaillierter Ablehnungsgrund** für Transparenz
|
||||
3. **Admin-Tracking** wird gespeichert
|
||||
4. **Keine Job-Erstellung**
|
||||
|
||||
### 6. Sicherheit und Berechtigungen ✅
|
||||
|
||||
- **@approver_required** Decorator für alle Admin-Endpunkte
|
||||
- **UserPermission.can_approve_jobs** Berechtigung erforderlich
|
||||
- **Admin-Rolle** oder spezielle Genehmigungsberechtigung
|
||||
- **Audit-Trail** durch processed_by und processed_at
|
||||
|
||||
### 7. API-Verbesserungen ✅
|
||||
|
||||
#### Erweiterte Approve-API:
|
||||
```json
|
||||
POST /api/requests/<id>/approve
|
||||
{
|
||||
"printer_id": 123,
|
||||
"notes": "Zusätzliche Anweisungen..."
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"status": "approved",
|
||||
"job_id": 456,
|
||||
"otp": "ABC123",
|
||||
"approved_by": "Admin Name",
|
||||
"approved_at": "2025-05-29T12:20:00",
|
||||
"notes": "Zusätzliche Anweisungen..."
|
||||
}
|
||||
```
|
||||
|
||||
#### Erweiterte Deny-API:
|
||||
```json
|
||||
POST /api/requests/<id>/deny
|
||||
{
|
||||
"reason": "Detaillierte Begründung für Ablehnung..."
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"status": "denied",
|
||||
"rejected_by": "Admin Name",
|
||||
"rejected_at": "2025-05-29T12:20:00",
|
||||
"reason": "Detaillierte Begründung..."
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Datenintegrität ✅
|
||||
|
||||
#### Model-Erweiterungen:
|
||||
- **to_dict()** Methode erweitert um neue Felder
|
||||
- **Relationship** zu processed_by_user für Admin-Info
|
||||
- **Eager Loading** für bessere Performance
|
||||
- **Cascade Analysis** für alle betroffenen Komponenten
|
||||
|
||||
### 9. Performance-Optimierungen ✅
|
||||
|
||||
- **Eager Loading** für Printer, Job und Admin-User
|
||||
- **Pagination** für große Datenmengen
|
||||
- **Caching** der Drucker-Liste
|
||||
- **Debounced Search** für bessere UX
|
||||
- **AJAX-Updates** ohne Seitenreload
|
||||
|
||||
### 10. Logging und Audit ✅
|
||||
|
||||
```python
|
||||
# Genehmigung
|
||||
logger.info(f"Gastanfrage {request_id} genehmigt von Admin {current_user.id} ({current_user.name})")
|
||||
|
||||
# Ablehnung
|
||||
logger.info(f"Gastanfrage {request_id} abgelehnt von Admin {current_user.id} ({current_user.name}): {rejection_reason}")
|
||||
```
|
||||
|
||||
## Verwendung für Administratoren
|
||||
|
||||
### 1. Zugriff
|
||||
**URL:** `/admin/requests`
|
||||
**Berechtigung:** Admin-Rolle oder `can_approve_jobs` Permission
|
||||
|
||||
### 2. Täglicher Workflow
|
||||
1. **Wartende Anfragen** prüfen (Standardfilter)
|
||||
2. **Dringende Anfragen** zuerst bearbeiten (>24h alt)
|
||||
3. **Details ansehen** für vollständige Informationen
|
||||
4. **Genehmigen** mit Drucker-Zuweisung und Notizen
|
||||
5. **Ablehnen** mit detaillierter Begründung
|
||||
|
||||
### 3. Überwachung
|
||||
- **Echtzeit-Statistiken** im Header
|
||||
- **Status-Tracking** für alle Anfragen
|
||||
- **Admin-Historie** für Accountability
|
||||
- **Job-Überwachung** für genehmigte Anfragen
|
||||
|
||||
## Status
|
||||
**VOLLSTÄNDIG IMPLEMENTIERT** - 2025-05-29 12:20:00
|
||||
|
||||
1. ✅ Datenbank-Schema erweitert und migriert
|
||||
2. ✅ API-Endpoints implementiert und getestet
|
||||
3. ✅ Admin-Oberfläche erstellt und funktional
|
||||
4. ✅ Berechtigungen und Sicherheit implementiert
|
||||
5. ✅ Workflow und Benutzerfreundlichkeit optimiert
|
||||
6. ✅ Logging und Audit-Trail eingerichtet
|
||||
|
||||
**Die vollständige Administrator-Funktionalität für Gastanfragen-Verwaltung ist einsatzbereit.**
|
@ -1 +0,0 @@
|
||||
|
@ -1,424 +0,0 @@
|
||||
# Automatischer Start ohne Benutzeranmeldung
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das MYP Druckerverwaltungssystem ist so konfiguriert, dass der Raspberry Pi automatisch ohne Benutzeranmeldung startet und direkt in den Kiosk-Modus wechselt. Diese Dokumentation beschreibt die implementierten Mechanismen und Troubleshooting-Optionen.
|
||||
|
||||
## Implementierte Auto-Login-Mechanismen
|
||||
|
||||
### 1. LightDM Display Manager
|
||||
|
||||
**Konfigurationsdatei:** `/etc/lightdm/lightdm.conf`
|
||||
|
||||
```ini
|
||||
[Seat:*]
|
||||
# Automatischer Login für Kiosk-Benutzer
|
||||
autologin-user=kiosk
|
||||
autologin-user-timeout=0
|
||||
autologin-session=openbox
|
||||
user-session=openbox
|
||||
session-wrapper=/etc/X11/Xsession
|
||||
greeter-session=lightdm-gtk-greeter
|
||||
allow-guest=false
|
||||
# Kein Benutzer-Wechsel möglich
|
||||
greeter-hide-users=true
|
||||
greeter-show-manual-login=false
|
||||
# Automatischer Start ohne Verzögerung
|
||||
autologin-in-background=false
|
||||
# Session-Setup
|
||||
session-setup-script=/usr/share/lightdm/setup-kiosk-session.sh
|
||||
|
||||
[SeatDefaults]
|
||||
# Zusätzliche Sicherheitseinstellungen
|
||||
autologin-user=kiosk
|
||||
autologin-user-timeout=0
|
||||
autologin-session=openbox
|
||||
greeter-hide-users=true
|
||||
greeter-show-manual-login=false
|
||||
allow-user-switching=false
|
||||
```
|
||||
|
||||
**Systemd-Override:** `/etc/systemd/system/lightdm.service.d/autologin-override.conf`
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
After=multi-user.target network.target myp-druckerverwaltung.service
|
||||
Wants=myp-druckerverwaltung.service
|
||||
|
||||
[Service]
|
||||
# Automatischer Restart bei Fehlern
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
# Umgebungsvariablen für Kiosk
|
||||
Environment=DISPLAY=:0
|
||||
Environment=KIOSK_MODE=1
|
||||
# Verzögerung für Backend-Start
|
||||
ExecStartPre=/bin/bash -c 'for i in {1..30}; do if curl -s http://localhost:5000 >/dev/null 2>&1; then break; fi; sleep 2; done'
|
||||
```
|
||||
|
||||
### 2. Getty Auto-Login (Fallback)
|
||||
|
||||
**Konfigurationsdatei:** `/etc/systemd/system/getty@tty1.service.d/autologin.conf`
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin kiosk --noclear %I $TERM
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
```
|
||||
|
||||
### 3. Benutzer-Profile Auto-Start
|
||||
|
||||
**Bashrc-Autostart:** `/home/kiosk/.bashrc`
|
||||
|
||||
```bash
|
||||
# ===== VERSTÄRKTER KIOSK AUTOSTART =====
|
||||
if [ -z "$SSH_CLIENT" ] && [ -z "$SSH_TTY" ] && [ -z "$KIOSK_STARTED" ]; then
|
||||
export KIOSK_STARTED=1
|
||||
|
||||
# Logge Autostart-Versuch
|
||||
echo "$(date): Bashrc Autostart-Versuch auf $(tty)" >> /var/log/kiosk-autostart.log
|
||||
|
||||
# Prüfe ob wir auf tty1 sind und X noch nicht läuft
|
||||
if [ "$(tty)" = "/dev/tty1" ] && [ -z "$DISPLAY" ]; then
|
||||
echo "$(date): Starte X-Session automatisch via bashrc" >> /var/log/kiosk-autostart.log
|
||||
exec startx
|
||||
fi
|
||||
|
||||
# Falls X läuft aber Kiosk-App nicht, starte sie
|
||||
if [ -n "$DISPLAY" ] && ! pgrep -f "chromium.*kiosk" > /dev/null; then
|
||||
echo "$(date): Starte Kiosk-Anwendung via bashrc" >> /var/log/kiosk-autostart.log
|
||||
exec $HOME/start-kiosk.sh
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
**Profile-Autostart:** `/home/kiosk/.profile`
|
||||
|
||||
```bash
|
||||
# ===== VERSTÄRKTER KIOSK AUTOSTART (PROFILE) =====
|
||||
if [ -z "$SSH_CLIENT" ] && [ -z "$SSH_TTY" ] && [ -z "$KIOSK_STARTED" ]; then
|
||||
export KIOSK_STARTED=1
|
||||
|
||||
# Logge Profile-Autostart
|
||||
echo "$(date): Profile Autostart-Versuch auf $(tty)" >> /var/log/kiosk-autostart.log
|
||||
|
||||
# Starte X-Session falls nicht vorhanden
|
||||
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
|
||||
echo "$(date): Starte X-Session via profile" >> /var/log/kiosk-autostart.log
|
||||
exec startx
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
### 4. Desktop Autostart
|
||||
|
||||
**XDG Autostart:** `/home/kiosk/.config/autostart/kiosk-app.desktop`
|
||||
|
||||
```ini
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=MYP Kiosk Application
|
||||
Comment=Startet die MYP Kiosk-Anwendung automatisch
|
||||
Exec=/home/kiosk/start-kiosk.sh
|
||||
Hidden=false
|
||||
NoDisplay=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
StartupNotify=false
|
||||
Terminal=false
|
||||
```
|
||||
|
||||
### 5. Systemd-Watchdog Services
|
||||
|
||||
**Enhanced Watchdog:** `/etc/systemd/system/kiosk-watchdog-enhanced.service`
|
||||
|
||||
Überwacht kontinuierlich:
|
||||
- Backend-Service Status
|
||||
- Backend-Erreichbarkeit (HTTP)
|
||||
- LightDM Status
|
||||
- Kiosk-Benutzer Session
|
||||
- Chromium Kiosk-Prozess
|
||||
- X-Server Status
|
||||
|
||||
### 6. Cron-Überwachung
|
||||
|
||||
**Cron-Watchdog:** `/etc/cron.d/kiosk-watchdog-enhanced`
|
||||
|
||||
```bash
|
||||
# Verstärkter Kiosk-Watchdog: Prüft alle 2 Minuten
|
||||
*/2 * * * * kiosk /bin/bash -c 'if ! pgrep -f "chromium.*kiosk" > /dev/null; then echo "$(date): Cron-Watchdog startet Kiosk neu" >> /var/log/kiosk-cron-watchdog.log; DISPLAY=:0 $HOME/start-kiosk.sh & fi'
|
||||
|
||||
# System-Watchdog: Prüft Services alle 5 Minuten
|
||||
*/5 * * * * root /bin/bash -c 'if ! systemctl is-active --quiet lightdm; then echo "$(date): Cron startet LightDM neu" >> /var/log/system-cron-watchdog.log; systemctl start lightdm; fi'
|
||||
```
|
||||
|
||||
### 7. RC.Local Fallback
|
||||
|
||||
**Boot-Fallback:** `/etc/rc.local`
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Verstärkter rc.local - Kiosk-Fallback
|
||||
|
||||
# Logge Start
|
||||
echo "$(date): rc.local gestartet" >> /var/log/kiosk-fallback.log
|
||||
|
||||
# Warte auf System-Initialisierung
|
||||
sleep 20
|
||||
|
||||
# Starte Backend-Service falls nicht läuft
|
||||
if ! systemctl is-active --quiet myp-druckerverwaltung; then
|
||||
echo "$(date): Starte Backend-Service" >> /var/log/kiosk-fallback.log
|
||||
systemctl start myp-druckerverwaltung
|
||||
sleep 10
|
||||
fi
|
||||
|
||||
# Warte auf Backend-Verfügbarkeit
|
||||
for i in {1..30}; do
|
||||
if curl -s http://localhost:5000 >/dev/null 2>&1; then
|
||||
echo "$(date): Backend verfügbar nach $i Versuchen" >> /var/log/kiosk-fallback.log
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Starte LightDM falls nicht läuft
|
||||
if ! systemctl is-active --quiet lightdm; then
|
||||
echo "$(date): Starte LightDM" >> /var/log/kiosk-fallback.log
|
||||
systemctl start lightdm
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Prüfe nach 30 Sekunden ob Kiosk-Benutzer angemeldet ist
|
||||
sleep 30
|
||||
if ! pgrep -u kiosk > /dev/null; then
|
||||
echo "$(date): Kiosk-Benutzer nicht angemeldet - starte LightDM neu" >> /var/log/kiosk-fallback.log
|
||||
systemctl restart lightdm
|
||||
fi
|
||||
|
||||
echo "$(date): rc.local Kiosk-Fallback abgeschlossen" >> /var/log/kiosk-fallback.log
|
||||
|
||||
exit 0
|
||||
```
|
||||
|
||||
## Boot-Optimierungen
|
||||
|
||||
### Raspberry Pi Boot-Konfiguration
|
||||
|
||||
**Boot-Config:** `/boot/config.txt`
|
||||
|
||||
```ini
|
||||
# GPU Memory für bessere Performance
|
||||
gpu_mem=128
|
||||
|
||||
# Disable Boot-Splash für schnelleren Start
|
||||
disable_splash=1
|
||||
|
||||
# Boot-Delay reduzieren
|
||||
boot_delay=0
|
||||
|
||||
# HDMI-Hotplug für bessere Display-Erkennung
|
||||
hdmi_force_hotplug=1
|
||||
|
||||
# Disable Rainbow-Splash
|
||||
disable_overscan=1
|
||||
```
|
||||
|
||||
**Kernel-Parameter:** `/boot/cmdline.txt`
|
||||
|
||||
```
|
||||
# Zusätzliche Parameter für schnelleren Boot
|
||||
quiet loglevel=3 logo.nologo vt.global_cursor_default=0
|
||||
```
|
||||
|
||||
### Systemd-Konfiguration
|
||||
|
||||
**Standard-Target:** `graphical.target`
|
||||
|
||||
```bash
|
||||
systemctl set-default graphical.target
|
||||
```
|
||||
|
||||
**Logind-Konfiguration:** `/etc/systemd/logind.conf.d/kiosk.conf`
|
||||
|
||||
```ini
|
||||
[Login]
|
||||
# Verhindere dass System bei Inaktivität heruntergefahren wird
|
||||
IdleAction=ignore
|
||||
IdleActionSec=infinity
|
||||
|
||||
# Verhindere Suspend/Hibernate
|
||||
HandlePowerKey=ignore
|
||||
HandleSuspendKey=ignore
|
||||
HandleHibernateKey=ignore
|
||||
HandleLidSwitch=ignore
|
||||
|
||||
# Session-Einstellungen für Kiosk
|
||||
KillUserProcesses=no
|
||||
UserStopDelaySec=10
|
||||
|
||||
# Automatic VT allocation
|
||||
ReserveVT=1
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 1. System startet nicht automatisch
|
||||
|
||||
**Diagnose:**
|
||||
```bash
|
||||
# Prüfe systemd default target
|
||||
systemctl get-default
|
||||
|
||||
# Prüfe LightDM Status
|
||||
systemctl status lightdm
|
||||
|
||||
# Prüfe Getty Service
|
||||
systemctl status getty@tty1
|
||||
```
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Setze graphical target
|
||||
sudo systemctl set-default graphical.target
|
||||
|
||||
# Aktiviere Services
|
||||
sudo systemctl enable lightdm
|
||||
sudo systemctl enable getty@tty1
|
||||
```
|
||||
|
||||
### 2. Kiosk-Benutzer meldet sich nicht automatisch an
|
||||
|
||||
**Diagnose:**
|
||||
```bash
|
||||
# Prüfe LightDM Konfiguration
|
||||
cat /etc/lightdm/lightdm.conf | grep autologin
|
||||
|
||||
# Prüfe PAM Konfiguration
|
||||
cat /etc/pam.d/lightdm-autologin
|
||||
|
||||
# Prüfe Benutzer-Sessions
|
||||
who
|
||||
```
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# LightDM neu konfigurieren
|
||||
sudo dpkg-reconfigure lightdm
|
||||
|
||||
# Service neustarten
|
||||
sudo systemctl restart lightdm
|
||||
```
|
||||
|
||||
### 3. X-Session startet nicht
|
||||
|
||||
**Diagnose:**
|
||||
```bash
|
||||
# Prüfe X-Server Logs
|
||||
cat /var/log/Xorg.0.log
|
||||
|
||||
# Prüfe Session-Logs
|
||||
cat /var/log/kiosk-session.log
|
||||
|
||||
# Prüfe Autostart-Logs
|
||||
cat /var/log/kiosk-autostart.log
|
||||
```
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# X-Server manuell starten
|
||||
sudo -u kiosk DISPLAY=:0 startx
|
||||
|
||||
# Openbox neu installieren
|
||||
sudo apt-get install --reinstall openbox
|
||||
```
|
||||
|
||||
### 4. Kiosk-Anwendung startet nicht
|
||||
|
||||
**Diagnose:**
|
||||
```bash
|
||||
# Prüfe Backend-Service
|
||||
systemctl status myp-druckerverwaltung
|
||||
|
||||
# Prüfe Backend-Erreichbarkeit
|
||||
curl -s http://localhost:5000
|
||||
|
||||
# Prüfe Chromium-Prozesse
|
||||
pgrep -f chromium
|
||||
```
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Backend neustarten
|
||||
sudo systemctl restart myp-druckerverwaltung
|
||||
|
||||
# Kiosk-Anwendung manuell starten
|
||||
sudo -u kiosk DISPLAY=:0 /home/kiosk/start-kiosk.sh
|
||||
```
|
||||
|
||||
## Wartungskommandos
|
||||
|
||||
### System-Status prüfen
|
||||
```bash
|
||||
sudo myp-maintenance status
|
||||
```
|
||||
|
||||
### Services neustarten
|
||||
```bash
|
||||
sudo myp-maintenance restart
|
||||
```
|
||||
|
||||
### Logs anzeigen
|
||||
```bash
|
||||
sudo myp-maintenance logs
|
||||
sudo myp-maintenance kiosk-logs
|
||||
```
|
||||
|
||||
### Kiosk-Modus beenden (für Wartung)
|
||||
```bash
|
||||
sudo myp-maintenance exit-kiosk
|
||||
```
|
||||
|
||||
### SSH für Remote-Wartung aktivieren
|
||||
```bash
|
||||
sudo myp-maintenance enable-ssh
|
||||
```
|
||||
|
||||
## Log-Dateien
|
||||
|
||||
### Wichtige Log-Dateien für Diagnose
|
||||
|
||||
- `/var/log/kiosk-autostart.log` - Autostart-Versuche
|
||||
- `/var/log/kiosk-session.log` - X-Session Events
|
||||
- `/var/log/kiosk-fallback.log` - RC.Local Fallback
|
||||
- `/var/log/kiosk-watchdog-enhanced.log` - Watchdog-Service
|
||||
- `/var/log/kiosk-cron-watchdog.log` - Cron-Watchdog
|
||||
- `/var/log/system-cron-watchdog.log` - System-Cron-Watchdog
|
||||
- `/var/log/Xorg.0.log` - X-Server Logs
|
||||
- `journalctl -u lightdm` - LightDM Service Logs
|
||||
- `journalctl -u myp-druckerverwaltung` - Backend Service Logs
|
||||
|
||||
## Optimierung nach Installation
|
||||
|
||||
Für bereits installierte Systeme kann die Schnellstart-Optimierung ausgeführt werden:
|
||||
|
||||
```bash
|
||||
sudo ./schnellstart_raspberry_pi.sh
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
Dieses Skript verstärkt alle Auto-Login-Mechanismen und optimiert die Boot-Performance.
|
||||
|
||||
## Sicherheitshinweise
|
||||
|
||||
- SSH ist standardmäßig deaktiviert für bessere Sicherheit
|
||||
- Console-Zugang über Strg+Alt+F1 bis F6 möglich
|
||||
- Root-Passwort: `744563017196A` (für Notfall-Wartung)
|
||||
- Kiosk-Benutzer hat keine sudo-Berechtigung
|
||||
- Automatische Updates sind konfiguriert
|
||||
|
||||
## Fazit
|
||||
|
||||
Das System ist mit mehrfachen redundanten Mechanismen ausgestattet, um einen zuverlässigen automatischen Start ohne Benutzeranmeldung zu gewährleisten. Bei Problemen stehen umfangreiche Diagnose- und Wartungstools zur Verfügung.
|
@ -1,114 +0,0 @@
|
||||
# Chart-Integration mit ApexCharts
|
||||
|
||||
Dieses Dokument beschreibt die Integration von ApexCharts in die MYP-Plattform zur Visualisierung von Daten.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die MYP-Plattform nutzt [ApexCharts](https://apexcharts.com/) für die Darstellung von Diagrammen und Visualisierungen. ApexCharts ist eine moderne JavaScript-Bibliothek zur Erstellung interaktiver Diagramme mit einem einfachen API und reaktionsfähigem Design.
|
||||
|
||||
## Dateien und Struktur
|
||||
|
||||
Die Chart-Integration besteht aus folgenden Komponenten:
|
||||
|
||||
1. **ApexCharts-Bibliothek**: `static/js/charts/apexcharts.min.js`
|
||||
2. **Konfigurationsdatei**: `static/js/charts/chart-config.js`
|
||||
3. **Renderer**: `static/js/charts/chart-renderer.js`
|
||||
4. **Adapter**: `static/js/charts/chart-adapter.js`
|
||||
|
||||
### Funktionsweise
|
||||
|
||||
1. Die **Chart-Konfiguration** definiert Standardstile, Farben und Optionen für alle Diagrammtypen.
|
||||
2. Der **Chart-Renderer** verwaltet die Erstellung, Aktualisierung und Zerstörung von Diagrammen.
|
||||
3. Der **Chart-Adapter** verbindet die bestehende `renderChart`-Funktion mit der neuen ApexCharts-Implementierung.
|
||||
|
||||
## Verwendung in Templates
|
||||
|
||||
Um ein Diagramm in einer Template-Datei anzuzeigen:
|
||||
|
||||
```html
|
||||
<div id="mein-chart" class="h-64" data-api-endpoint="/api/pfad/zu/daten" data-chart data-chart-type="line"></div>
|
||||
```
|
||||
|
||||
### Verfügbare Attribute:
|
||||
|
||||
- `data-chart`: Markiert das Element als Diagramm-Container
|
||||
- `data-api-endpoint`: Der API-Endpunkt, von dem Daten geladen werden
|
||||
- `data-chart-type`: Der Diagrammtyp (optional, wird sonst automatisch bestimmt)
|
||||
|
||||
## Unterstützte Diagrammtypen
|
||||
|
||||
Die folgenden Diagrammtypen werden unterstützt:
|
||||
|
||||
- `line`: Liniendiagramm
|
||||
- `area`: Flächendiagramm
|
||||
- `bar`: Balkendiagramm
|
||||
- `pie`: Kreisdiagramm
|
||||
- `donut`: Ringdiagramm
|
||||
- `radial`: Radialdiagramm
|
||||
|
||||
## Datenformat
|
||||
|
||||
Die API-Endpunkte sollten Daten in einem der folgenden Formate zurückgeben:
|
||||
|
||||
### Für Kreisdiagramme (pie/donut):
|
||||
|
||||
```json
|
||||
{
|
||||
"scheduled": 12,
|
||||
"active": 5,
|
||||
"completed": 25,
|
||||
"cancelled": 3
|
||||
}
|
||||
```
|
||||
|
||||
### Für Balkendiagramme:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"printer_name": "Drucker 1",
|
||||
"job_count": 25,
|
||||
"print_hours": 124.5
|
||||
},
|
||||
{
|
||||
"printer_name": "Drucker 2",
|
||||
"job_count": 18,
|
||||
"print_hours": 85.2
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Für Linien- und Flächendiagramme:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"date": "2023-01-01",
|
||||
"value": 42
|
||||
},
|
||||
{
|
||||
"date": "2023-01-02",
|
||||
"value": 58
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Theme-Unterstützung
|
||||
|
||||
Die Diagramme unterstützen automatisch das Light- und Dark-Mode-Theme der MYP-Plattform. Bei einem Themenwechsel werden alle aktiven Diagramme mit den passenden Farben aktualisiert.
|
||||
|
||||
## Anpassung
|
||||
|
||||
Um die Diagramme anzupassen:
|
||||
|
||||
1. **Farben**: Anpassungen in `MYP_CHART_COLORS` in `chart-config.js`
|
||||
2. **Standardoptionen**: Änderungen in `getBaseChartOptions()` in `chart-config.js`
|
||||
3. **Spezifische Diagrammtypen**: Anpassungen in den jeweiligen Funktionen (`getLineChartConfig`, `getPieChartConfig`, etc.)
|
||||
|
||||
## Erweiterung
|
||||
|
||||
Um weitere Diagrammtypen oder Funktionen hinzuzufügen:
|
||||
|
||||
1. Fügen Sie eine neue Konfigurationsfunktion in `chart-config.js` hinzu
|
||||
2. Erweitern Sie die `createChart`-Funktion in `chart-renderer.js`, um den neuen Diagrammtyp zu unterstützen
|
||||
3. Aktualisieren Sie den `chart-adapter.js`, um die Datentransformation für den neuen Diagrammtyp zu unterstützen
|
@ -1,595 +0,0 @@
|
||||
# Häufige Fehler und Lösungen
|
||||
|
||||
Dieses Dokument enthält häufig auftretende Fehler und deren Lösungen für das MYP-3D-Druck-Management-System.
|
||||
|
||||
## Drucker-Status-Check mit 7-Sekunden-Timeout
|
||||
|
||||
### Implementierung (2024-12-19)
|
||||
|
||||
**Problem:** Drucker-Status wurde nur basierend auf hardkodierten Konfigurationen bestimmt, ohne echte Netzwerk-Überprüfung.
|
||||
|
||||
**Lösung:** Implementierung eines echten Ping-basierten Status-Checks mit 7-Sekunden-Timeout:
|
||||
|
||||
#### Backend-Änderungen:
|
||||
1. **Neue Funktionen in `app.py`:**
|
||||
- `check_printer_status(ip_address, timeout=7)`: Überprüft einzelnen Drucker via Ping
|
||||
- `check_multiple_printers_status(printers, timeout=7)`: Parallel-Check mehrerer Drucker
|
||||
|
||||
2. **Aktualisierte API-Endpunkte:**
|
||||
- `/api/printers`: Verwendet jetzt echten Status-Check
|
||||
- `/api/printers/status`: Spezieller Endpunkt für Status-Überprüfung
|
||||
|
||||
3. **Features:**
|
||||
- 7-Sekunden-Timeout pro Drucker
|
||||
- Parallel-Ausführung mit ThreadPoolExecutor
|
||||
- Windows- und Unix-kompatible Ping-Befehle
|
||||
- Detailliertes Logging der Status-Checks
|
||||
- Automatische Datenbank-Aktualisierung
|
||||
|
||||
#### Frontend-Änderungen:
|
||||
1. **Erweiterte Drucker-Seite (`templates/printers.html`):**
|
||||
- Funktionsfähiger "Aktualisieren"-Button
|
||||
- Loading-States mit Timeout-Information
|
||||
- Status-Nachrichten mit Erfolgs-/Fehler-Feedback
|
||||
- Zeitstempel der letzten Überprüfung
|
||||
|
||||
2. **Neue JavaScript-Funktionen:**
|
||||
- `refreshPrinters()`: Vollständiger Status-Check mit UI-Feedback
|
||||
- `loadPrintersWithStatusCheck()`: Erweiterte Lade-Funktion
|
||||
- `showStatusMessage()`: Toast-Nachrichten für Benutzer-Feedback
|
||||
- `formatTime()`: Relative Zeitanzeige für Status-Checks
|
||||
|
||||
#### Benutzer-Erfahrung:
|
||||
- **Vor dem Update:** Drucker-Status basierte nur auf Konfiguration
|
||||
- **Nach dem Update:**
|
||||
- Echter Ping-Test mit 7-Sekunden-Timeout
|
||||
- Visuelles Feedback während der Überprüfung
|
||||
- Erfolgs-/Fehler-Nachrichten
|
||||
- Zeitstempel der letzten Überprüfung
|
||||
- Button-Deaktivierung während des Checks
|
||||
|
||||
#### Technische Details:
|
||||
- **Timeout:** 7 Sekunden pro Drucker (konfigurierbar)
|
||||
- **Parallel-Verarbeitung:** Bis zu 10 gleichzeitige Checks
|
||||
- **Plattform-Unterstützung:** Windows (`ping -n 1 -w 7000`) und Unix (`ping -c 1 -W 7`)
|
||||
- **Fehlerbehandlung:** Graceful Fallback auf "offline" bei Timeouts oder Fehlern
|
||||
- **Logging:** Detaillierte Protokollierung aller Status-Checks
|
||||
|
||||
#### Konfiguration:
|
||||
```python
|
||||
# Timeout kann in den API-Aufrufen angepasst werden
|
||||
status_results = check_multiple_printers_status(printer_data, timeout=7)
|
||||
```
|
||||
|
||||
#### Fehlerbehebung:
|
||||
1. **Timeout-Probleme:** Timeout kann in der Funktion angepasst werden
|
||||
2. **Netzwerk-Probleme:** Logs in `logs/printers/printers.log` prüfen
|
||||
3. **Performance:** Bei vielen Druckern ThreadPool-Größe anpassen
|
||||
|
||||
## Job-Scheduler und Steckdosensteuerung
|
||||
|
||||
### Steckdose kann nicht eingeschaltet werden
|
||||
|
||||
**Problem:**
|
||||
Log-Eintrag: `Fehler beim Starten von Job X: Steckdose konnte nicht eingeschaltet werden`
|
||||
|
||||
**Mögliche Ursachen und Lösungen:**
|
||||
1. **Netzwerkverbindung zu Steckdose unterbrochen**
|
||||
- Prüfen Sie, ob die Steckdose mit dem Netzwerk verbunden ist
|
||||
- Ping-Test: `ping [IP-Adresse der Steckdose]`
|
||||
- Steckdose neu starten (physischer Reset-Knopf)
|
||||
|
||||
2. **Falsche Zugangsdaten für Steckdose**
|
||||
- Überprüfen Sie in der Datenbank, ob die korrekten Zugangsdaten (Benutzername/Passwort) für die Steckdose hinterlegt sind
|
||||
- Admin-Bereich → Drucker → Steckdosenkonfiguration bearbeiten
|
||||
|
||||
3. **PyP110-Bibliothek veraltet**
|
||||
- Bibliothek aktualisieren: `pip install PyP100 --upgrade`
|
||||
|
||||
### Job wird nicht gestartet oder beendet
|
||||
|
||||
**Problem:**
|
||||
Ein geplanter Job startet nicht oder ein laufender Job wird nicht beendet
|
||||
|
||||
**Mögliche Ursachen und Lösungen:**
|
||||
1. **Scheduler-Thread läuft nicht**
|
||||
- Prüfen Sie den Status des Schedulers im Admin-Bereich
|
||||
- Wenn nicht aktiv: Scheduler starten
|
||||
- Bei wiederholtem Problem: Server neu starten
|
||||
|
||||
2. **Datenbankprobleme**
|
||||
- Überprüfen Sie die Datenbank auf Konsistenz
|
||||
- Backup einspielen, falls notwendig
|
||||
|
||||
### Job-Verlängerung funktioniert nicht
|
||||
|
||||
**Problem:**
|
||||
Nach dem Verlängern eines Jobs wird er trotzdem zum ursprünglichen Zeitpunkt beendet
|
||||
|
||||
**Lösung:**
|
||||
1. Prüfen Sie die Datenbankeinträge, ob die `end_at`-Zeit korrekt aktualisiert wurde
|
||||
2. Prüfen Sie die Log-Dateien auf Fehler beim Aktualisieren des Jobs
|
||||
3. Verlängern Sie den Job erneut mit größerem Zeitpuffer
|
||||
|
||||
## Sicherheitshinweise
|
||||
|
||||
### Sicherheitsmaßnahmen bei Stromausfällen
|
||||
|
||||
**Wichtig:**
|
||||
- Bei Stromausfällen kehren die Steckdosen in ihren Standardzustand zurück (meist AUS)
|
||||
- Nach Wiederherstellung der Stromversorgung wird der Scheduler automatisch Jobs wieder aufnehmen
|
||||
- Laufende Jobs werden jedoch **nicht** automatisch fortgesetzt, um Schäden zu vermeiden
|
||||
- Administrator muss laufende Jobs manuell neu starten
|
||||
|
||||
### 5-Minuten Sicherheitspuffer
|
||||
|
||||
Das System verwendet einen 5-Minuten Sicherheitspuffer, bevor es einen Job beendet. Dies verhindert, dass ein Druck zu früh beendet wird. Die tatsächliche Ausschaltzeit ist also immer 5 Minuten nach der im System angezeigten Endzeit.
|
||||
|
||||
## API-Endpunkte für Fehlerbehebung
|
||||
|
||||
| Endpunkt | Beschreibung |
|
||||
|----------|--------------|
|
||||
| `GET /api/scheduler/status` | Status des Job-Schedulers abfragen |
|
||||
| `POST /api/scheduler/start` | Scheduler manuell starten (Admin) |
|
||||
| `POST /api/scheduler/stop` | Scheduler manuell stoppen (Admin) |
|
||||
| `GET /api/jobs/active` | Alle aktiven Jobs anzeigen |
|
||||
| `POST /api/jobs/<id>/extend` | Job-Laufzeit verlängern |
|
||||
| `POST /api/jobs/<id>/finish` | Job manuell beenden (Admin) |
|
||||
|
||||
## Flask-Login Fehler
|
||||
|
||||
### AttributeError: 'User' object has no attribute 'is_authenticated'
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
AttributeError: 'User' object has no attribute 'is_authenticated'
|
||||
```
|
||||
|
||||
**Ursache:** Die User-Klasse erbt nicht von `UserMixin` oder implementiert nicht die erforderlichen Flask-Login Attribute.
|
||||
|
||||
**Lösungen:**
|
||||
1. Stellen Sie sicher, dass die User-Klasse von `UserMixin` erbt:
|
||||
```python
|
||||
from flask_login import UserMixin
|
||||
|
||||
class User(UserMixin, Base):
|
||||
# ... Rest der Klasse
|
||||
```
|
||||
2. Implementieren Sie die erforderlichen Methoden manuell:
|
||||
```python
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.active
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
def get_id(self):
|
||||
return str(self.id)
|
||||
```
|
||||
3. Führen Sie die Datenbankmigration aus:
|
||||
```bash
|
||||
python3.11 migrate_db.py
|
||||
```
|
||||
|
||||
### Fehlende username-Spalte
|
||||
|
||||
**Problem:** Die Anwendung versucht auf `username` zuzugreifen, aber die Spalte existiert nicht in der Datenbank.
|
||||
|
||||
**Lösungen:**
|
||||
1. Führen Sie das Migrationsskript aus:
|
||||
```bash
|
||||
python3.11 migrate_db.py
|
||||
```
|
||||
2. Falls die Migration fehlschlägt, initialisieren Sie die Datenbank neu:
|
||||
```bash
|
||||
python3.11 init_db.py
|
||||
```
|
||||
|
||||
## Datenbank-Fehler
|
||||
|
||||
### SQLite Database is locked
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) database is locked
|
||||
```
|
||||
|
||||
**Ursache:** Dieser Fehler tritt auf, wenn mehrere Prozesse gleichzeitig auf die Datenbank zugreifen.
|
||||
|
||||
**Lösungen:**
|
||||
1. Stellen Sie sicher, dass Sie alle Datenbankverbindungen mit `db_session.close()` schließen
|
||||
2. Verwenden Sie `with get_db_session() as session:` um Sessions automatisch zu schließen
|
||||
3. Prüfen Sie, ob der Scheduler und die Hauptanwendung gleichzeitig auf die Datenbank zugreifen
|
||||
|
||||
### Detached Instance Error
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
sqlalchemy.orm.exc.DetachedInstanceError: Instance <Job at 0x...> is not bound to a Session
|
||||
```
|
||||
|
||||
**Ursache:** Zugriff auf ein SQLAlchemy-Objekt nach dem Schließen der Session.
|
||||
|
||||
**Lösungen:**
|
||||
1. Verwenden Sie `joinedload` für eager loading: `db_session.query(Job).options(joinedload(Job.user))`
|
||||
2. Konvertieren Sie Objekte zu Dictionaries, bevor Sie die Session schließen: `job_dict = job.to_dict()`
|
||||
3. Verwenden Sie `session.expunge_all()` gefolgt von `session.close()`, wenn Sie Objekte weiter verwenden müssen
|
||||
|
||||
## Tapo Smart Plug Fehler
|
||||
|
||||
### Verbindungsfehler
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
PyP100.PyP100.P100Exception: Could not connect to device
|
||||
```
|
||||
|
||||
**Ursache:** Smart Plug ist nicht erreichbar oder Authentifizierungsprobleme.
|
||||
|
||||
**Lösungen:**
|
||||
1. Überprüfen Sie die IP-Adresse des Smart Plugs
|
||||
2. Stellen Sie sicher, dass der Smart Plug eingeschaltet und mit dem WLAN verbunden ist
|
||||
3. Überprüfen Sie die Zugangsdaten in `config/settings.py`
|
||||
4. Verwenden Sie einen Try-Except-Block zur Fehlerbehandlung:
|
||||
```python
|
||||
try:
|
||||
p110 = PyP110.P110(ip, username, password)
|
||||
p110.handshake()
|
||||
p110.login()
|
||||
p110.turnOn()
|
||||
except Exception as e:
|
||||
printers_logger.error(f"Smart Plug Fehler: {str(e)}")
|
||||
```
|
||||
|
||||
## Flask-Anwendungsfehler
|
||||
|
||||
### Interner Server-Fehler (500)
|
||||
|
||||
**Problem:** Die Anwendung gibt einen 500-Fehler zurück.
|
||||
|
||||
**Ursache:** Verschiedene mögliche Ursachen, von Datenbank- bis hin zu Programmierfehlern.
|
||||
|
||||
**Lösungen:**
|
||||
1. Überprüfen Sie die Logs unter `logs/app/app.log` und `logs/errors/errors.log`
|
||||
2. Starten Sie die Anwendung im Debug-Modus: `FLASK_DEBUG=True python3.11 app.py`
|
||||
3. Implementieren Sie bessere Fehlerbehandlung mit Try-Except-Blöcken
|
||||
|
||||
### Authentifizierungsfehler
|
||||
|
||||
**Problem:** Benutzer können sich nicht anmelden oder erhalten unbefugte Zugriffsfehler.
|
||||
|
||||
**Ursachen:**
|
||||
- Ungültige Anmeldedaten
|
||||
- Session-Probleme
|
||||
- Cookie-Probleme
|
||||
|
||||
**Lösungen:**
|
||||
1. Überprüfen Sie die Logs unter `logs/auth/auth.log`
|
||||
2. Setzen Sie das Passwort zurück mit:
|
||||
```python
|
||||
from models import User, get_db_session
|
||||
|
||||
db_session = get_db_session()
|
||||
user = db_session.query(User).filter(User.username == "admin").first()
|
||||
user.set_password("neues_passwort")
|
||||
db_session.commit()
|
||||
db_session.close()
|
||||
```
|
||||
3. Löschen Sie Browser-Cookies und -Cache
|
||||
|
||||
## CSS und Frontend-Fehler
|
||||
|
||||
### Tailwind-Stile werden nicht angewendet
|
||||
|
||||
**Problem:** Die Tailwind-Klassen haben keine Wirkung auf das Styling.
|
||||
|
||||
**Lösungen:**
|
||||
1. Stellen Sie sicher, dass die generierte CSS-Datei korrekt eingebunden ist:
|
||||
```html
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind-dark-consolidated.min.css') }}">
|
||||
```
|
||||
2. Bauen Sie die CSS-Datei neu: `npm run build:css`
|
||||
3. Überprüfen Sie in der Browser-Konsole, ob Fehler beim Laden der CSS-Datei auftreten
|
||||
|
||||
### Dark Mode funktioniert nicht
|
||||
|
||||
**Problem:** Die Dark-Mode-Stile werden nicht angewendet.
|
||||
|
||||
**Lösungen:**
|
||||
1. Stellen Sie sicher, dass das HTML-Element die entsprechende Klasse hat:
|
||||
```html
|
||||
<html class="dark" data-theme="dark">
|
||||
```
|
||||
2. Überprüfen Sie, ob die Tailwind-Konfiguration Dark Mode aktiviert hat:
|
||||
```js
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
darkMode: 'class',
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Scheduler-Fehler
|
||||
|
||||
### Scheduler führt keine Jobs aus
|
||||
|
||||
**Problem:** Der Scheduler schaltet Drucker nicht ein/aus wie erwartet.
|
||||
|
||||
**Ursachen:**
|
||||
- Scheduler ist nicht gestartet
|
||||
- Fehler bei der Tapo-Verbindung
|
||||
- Fehler in der Scheduler-Logik
|
||||
|
||||
**Lösungen:**
|
||||
1. Überprüfen Sie die Logs unter `logs/scheduler/scheduler.log`
|
||||
2. Stellen Sie sicher, dass `SCHEDULER_ENABLED = True` in `config/settings.py` gesetzt ist
|
||||
3. Starten Sie die Anwendung neu
|
||||
|
||||
## Performance-Probleme
|
||||
|
||||
### Langsame Ladezeiten
|
||||
|
||||
**Problem:** Die Anwendung lädt langsam oder reagiert träge.
|
||||
|
||||
**Lösungen:**
|
||||
1. Optimieren Sie Datenbankabfragen mit geeigneten Indizes
|
||||
2. Reduzieren Sie die Anzahl der Datenbankabfragen durch Eager Loading
|
||||
3. Implementieren Sie Caching für häufig abgerufene Daten
|
||||
4. Überprüfen Sie die Logfiles auf übermäßige Logging-Aktivitäten
|
||||
|
||||
## Content Security Policy (CSP) Fehler
|
||||
|
||||
### Script-src-elem 'none' Fehler
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
Refused to load the script because it violates the following Content Security Policy directive: "script-src-elem 'none'".
|
||||
```
|
||||
|
||||
**Ursache:** Die Content Security Policy ist zu restriktiv eingestellt und blockiert das Laden von Skripten.
|
||||
|
||||
**Lösungen:**
|
||||
1. Überprüfen Sie die CSP-Konfiguration in `config/security.py`:
|
||||
```python
|
||||
'Content-Security-Policy': (
|
||||
"default-src 'self'; "
|
||||
"script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
|
||||
"script-src-elem 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
|
||||
# ... weitere Regeln
|
||||
)
|
||||
```
|
||||
2. Stellen Sie sicher, dass die CSP in der `after_request`-Funktion richtig angewendet wird:
|
||||
```python
|
||||
@app.after_request
|
||||
def add_security_headers(response):
|
||||
from config.security import get_security_headers
|
||||
security_headers = get_security_headers()
|
||||
|
||||
for header, value in security_headers.items():
|
||||
response.headers[header] = value
|
||||
|
||||
# CSP-Report-Only Header entfernen
|
||||
if 'Content-Security-Policy-Report-Only' in response.headers:
|
||||
del response.headers['Content-Security-Policy-Report-Only']
|
||||
|
||||
return response
|
||||
```
|
||||
3. Vermeiden Sie mehrere konkurrierende Service Worker Registrierungen, die CSP-Konflikte verursachen können.
|
||||
|
||||
### Inline Script Fehler
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src-elem 'none'"
|
||||
```
|
||||
|
||||
**Lösungen:**
|
||||
1. Fügen Sie 'unsafe-inline' zur script-src Direktive hinzu
|
||||
2. Alternativ fügen Sie einen Hash oder Nonce für das Inline-Skript hinzu:
|
||||
```html
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
// Inline JavaScript
|
||||
</script>
|
||||
```
|
||||
und in der CSP:
|
||||
```
|
||||
script-src 'nonce-{{ csp_nonce }}';
|
||||
```
|
||||
|
||||
### Service Worker Registration Fehler
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
Refused to create a worker from 'URL' because it violates the following Content Security Policy directive: "script-src 'none'"
|
||||
```
|
||||
|
||||
**Lösungen:**
|
||||
1. Stellen Sie sicher, dass die `worker-src` Direktive korrekt konfiguriert ist:
|
||||
```
|
||||
worker-src 'self' blob:;
|
||||
```
|
||||
2. Erstellen Sie die erforderlichen Service Worker Dateien im richtigen Verzeichnis
|
||||
3. Verwenden Sie einen einheitlichen Ansatz zur Service Worker Registrierung, vorzugsweise im offline-app.js:
|
||||
```javascript
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/static/js/sw.js')
|
||||
.then(function(registration) {
|
||||
console.log('Service Worker registriert:', registration.scope);
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.log('Service Worker Registration fehlgeschlagen:', error);
|
||||
// Fallback
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Icon 404 Fehler
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
Failed to load resource: the server responded with a status of 404 (NOT FOUND)
|
||||
Error while trying to use the following icon from the Manifest: URL (Download error or resource isn't a valid image)
|
||||
```
|
||||
|
||||
**Lösungen:**
|
||||
1. Erstellen Sie die fehlenden Icon-Dateien im Verzeichnis `static/icons/`
|
||||
2. Aktualisieren Sie das Web App Manifest, um nur verfügbare Icons zu referenzieren:
|
||||
```json
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icons/mercedes-logo.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
]
|
||||
```
|
||||
3. Verwenden Sie Tools wie Lighthouse, um fehlende PWA-Ressourcen zu identifizieren
|
||||
|
||||
## JavaScript-Referenzfehler
|
||||
|
||||
### Problem: Nicht definierte Funktionen in admin-panel.js
|
||||
Fehlermeldungen wie "Uncaught ReferenceError: loadPrinters is not defined" können auftreten, wenn Funktionen in der JavaScript-Datei verwendet, aber nicht definiert werden.
|
||||
|
||||
**Lösung:**
|
||||
- Implementiere die fehlenden Funktionen (loadPrinters, loadSchedulerStatus, loadSystemStats, loadLogs, loadUsers, etc.)
|
||||
- Stelle sicher, dass alle aufgerufenen Funktionen im richtigen Scope definiert sind
|
||||
- Überprüfe die Reihenfolge der Funktionsdefinitionen und -aufrufe
|
||||
|
||||
### Problem: Funktionen in anderen Dateien werden nicht gefunden
|
||||
Fehler wie "Uncaught ReferenceError: refreshJobs is not defined" in jobs.html oder "exportStats is not defined" in stats.html.
|
||||
|
||||
**Lösung:**
|
||||
- Stelle sicher, dass die Funktionen in der jeweiligen HTML-Datei oder in einer eingebundenen JavaScript-Datei definiert sind
|
||||
- Überprüfe, ob die JavaScript-Dateien korrekt in der HTML-Datei eingebunden sind
|
||||
- Nutze konsistente Namenskonventionen für Funktionen
|
||||
|
||||
## Service Worker Fehler
|
||||
|
||||
### Problem: Cache-API kann mit chrome-extension URLs nicht umgehen
|
||||
Fehler: "Uncaught (in promise) TypeError: Failed to execute 'put' on 'Cache': Request scheme 'chrome-extension' is unsupported"
|
||||
|
||||
**Lösung:**
|
||||
- Überprüfe die URL-Protokolle bevor du Cache-Operationen durchführst
|
||||
- Füge eine spezielle Behandlung für chrome-extension-Protokolle hinzu
|
||||
- Verwende try-catch-Blöcke, um Fehler bei der Cache-Handhabung abzufangen
|
||||
|
||||
```javascript
|
||||
if (url.protocol === 'chrome-extension:') {
|
||||
// Spezielle Behandlung für chrome-extension URLs
|
||||
try {
|
||||
return await fetch(request);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch from chrome-extension:', error);
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Fehler beim Zugriff auf chrome-extension',
|
||||
offline: true
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API-Endpunkt Fehler
|
||||
|
||||
### Problem: API-Ressourcen werden nicht gefunden (404)
|
||||
Fehlermeldungen wie "Failed to load resource: the server responded with a status of 404 (NOT FOUND)"
|
||||
|
||||
**Lösung:**
|
||||
- Überprüfe die API-Routen im Backend
|
||||
- Stelle sicher, dass die API-Endpunkte korrekt implementiert sind
|
||||
- Überprüfe die Anfrage-URLs in den Frontend-Skripten
|
||||
- Füge Fehlerbehandlung für fehlende Ressourcen hinzu
|
||||
|
||||
## Frontend-Rendering Fehler
|
||||
|
||||
### Problem: Fehler beim Laden der Admin-Statistiken
|
||||
"Error loading admin: admin-panel.js:484 Fehler beim Laden der Admin-Statistiken"
|
||||
|
||||
**Lösung:**
|
||||
- Implementiere ordnungsgemäße Fehlerbehandlung in API-Anfragen
|
||||
- Zeige benutzerfreundliche Fehlermeldungen an
|
||||
- Füge Fallback-Werte für fehlende Daten hinzu
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const response = await fetch('/api/admin/stats');
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Laden der Admin-Statistiken');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Daten verarbeiten
|
||||
} catch (error) {
|
||||
console.error('Error loading admin stats:', error);
|
||||
showNotification('Fehler beim Laden der Admin-Statistiken', 'error');
|
||||
}
|
||||
```
|
||||
|
||||
## Icon/Ressourcen Fehler
|
||||
|
||||
### Problem: Icon-Dateien werden nicht gefunden
|
||||
"Error while trying to use the following icon from the Manifest: http://127.0.0.1:5000/static/icons/icon-144x144.png"
|
||||
|
||||
**Lösung:**
|
||||
- Überprüfe, ob die Icon-Dateien im richtigen Verzeichnis vorhanden sind
|
||||
- Stelle sicher, dass die Pfade in der Manifest-Datei korrekt sind
|
||||
- Füge Fallback-Icons für den Fall hinzu, dass die primären Icons nicht geladen werden können
|
||||
|
||||
## Allgemeine Fehlerbehebung
|
||||
|
||||
1. **Browser-Konsole prüfen**: Die meisten JavaScript-Fehler werden in der Browser-Konsole angezeigt (F12 oder Rechtsklick -> Untersuchen -> Konsole)
|
||||
2. **Netzwerk-Tab prüfen**: Im Netzwerk-Tab des Browsers können fehlgeschlagene API-Anfragen identifiziert werden
|
||||
3. **Codestruktur überprüfen**: Stelle sicher, dass alle Funktionen in der richtigen Reihenfolge definiert werden
|
||||
4. **Fehlerbehandlung verbessern**: Implementiere try-catch-Blöcke für alle asynchronen Funktionen
|
||||
5. **Browser-Cache leeren**: Manchmal können Probleme durch veraltete gecachte Dateien verursacht werden
|
||||
|
||||
## 404 Fehler (Seite nicht gefunden)
|
||||
|
||||
### Fehlende Routen oder API-Endpunkte
|
||||
|
||||
**Problem:**
|
||||
Die Anwendung zeigt 404-Fehler für bestimmte Routen wie `/privacy`, `/terms`, `/my/jobs`, `/api/user/export` oder `/api/user/profile`.
|
||||
|
||||
**Ursachen:**
|
||||
- Die Route wurde nicht korrekt in `app.py` oder dem entsprechenden Blueprint registriert
|
||||
- Bei API-Endpunkten: Die alte API-Route wurde verwendet, aber die Anwendung nutzt jetzt eine neue Route
|
||||
- Möglicherweise wurden die Templates erstellt, aber die Routen dafür fehlen
|
||||
|
||||
**Lösungen:**
|
||||
1. **Für öffentliche Seiten** (`/privacy`, `/terms`):
|
||||
- Überprüfen Sie, ob die Routen in `app.py` definiert sind
|
||||
- Stellen Sie sicher, dass der `@login_required` Decorator entfernt wurde, falls diese Seiten öffentlich sein sollen
|
||||
- Prüfen Sie, ob die entsprechenden Template-Dateien unter `templates/` existieren
|
||||
|
||||
2. **Für Benutzer-spezifische Seiten** (`/my/jobs`):
|
||||
- Fügen Sie eine Route hinzu, die zur Jobs-Seite mit vorgefiltertem Benutzer-Parameter weiterleitet
|
||||
|
||||
3. **Für API-Weiterleitungen** (`/api/user/export`, `/api/user/profile`):
|
||||
- Erstellen Sie Weiterleitungsrouten, die zu den neuen Blueprint-Routen führen
|
||||
- Beispiel: `/api/user/export` sollte zu `/user/export` weiterleiten
|
||||
|
||||
4. **Debugging von Routen:**
|
||||
- Mit `flask routes` können Sie alle registrierten Routen auflisten
|
||||
- Überprüfen Sie die Log-Dateien auf fehlgeschlagene Routenzugriffe
|
||||
- Im Debug-Modus starten: `FLASK_DEBUG=True python3.11 app.py`
|
||||
|
||||
5. **Blueprint-Registrierung prüfen:**
|
||||
- Stellen Sie sicher, dass alle Blueprints korrekt in `app.py` registriert sind:
|
||||
```python
|
||||
from blueprints.user import user_bp
|
||||
app.register_blueprint(user_bp)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Dieses Dokument wird kontinuierlich aktualisiert, wenn neue Fehler auftreten und Lösungen gefunden werden.
|
@ -1,95 +0,0 @@
|
||||
# CSRF-Token Problem - Behebung und Dokumentation
|
||||
|
||||
## Problem-Beschreibung
|
||||
**Fehler-Log**: `2025-05-29 11:51:15 - csrf - [INFO] INFO - The CSRF token is missing.`
|
||||
**HTTP-Status**: `400 Bad Request` bei POST `/api/guest/requests`
|
||||
|
||||
Das Problem trat auf, weil CSRF-Token in JavaScript-Fetch-Requests nicht korrekt übertragen wurden.
|
||||
|
||||
## Ursachen-Analyse
|
||||
1. **Fehlender CSRF-Error-Handler**: Die Flask-App hatte keinen konfigurierten CSRF-Error-Handler
|
||||
2. **Unvollständige CSRF-Token-Übertragung**: Das `guest_request.html` Template sendete CSRF-Token nicht korrekt mit API-Requests
|
||||
3. **Inkonsistente CSRF-Implementation**: Verschiedene Templates verwendeten unterschiedliche Methoden zur CSRF-Token-Übertragung
|
||||
|
||||
## Angewandte Lösungen
|
||||
|
||||
### 1. CSRF-Error-Handler hinzugefügt (`app.py`)
|
||||
```python
|
||||
@csrf.error_handler
|
||||
def csrf_error(reason):
|
||||
"""Behandelt CSRF-Fehler und gibt detaillierte Informationen zurück."""
|
||||
app_logger.error(f"CSRF-Fehler für {request.path}: {reason}")
|
||||
|
||||
if request.path.startswith('/api/'):
|
||||
# Für API-Anfragen: JSON-Response
|
||||
return jsonify({
|
||||
"error": "CSRF-Token fehlt oder ungültig",
|
||||
"reason": str(reason),
|
||||
"help": "Fügen Sie ein gültiges CSRF-Token zu Ihrer Anfrage hinzu"
|
||||
}), 400
|
||||
else:
|
||||
# Für normale Anfragen: Weiterleitung zur Fehlerseite
|
||||
flash("Sicherheitsfehler: Anfrage wurde abgelehnt. Bitte versuchen Sie es erneut.", "error")
|
||||
return redirect(request.url)
|
||||
```
|
||||
|
||||
### 2. CSRF-Token in JavaScript korrigiert (`templates/guest_request.html`)
|
||||
**Vorher** (fehlerhaft):
|
||||
```javascript
|
||||
const response = await fetch('/api/guest/requests', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
```
|
||||
|
||||
**Nachher** (korrekt):
|
||||
```javascript
|
||||
// CSRF-Token aus Meta-Tag auslesen
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
};
|
||||
|
||||
// CSRF-Token hinzufügen, wenn vorhanden
|
||||
if (csrfToken) {
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/guest/requests', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
```
|
||||
|
||||
## Betroffene Dateien
|
||||
1. **`app.py`** - CSRF-Error-Handler hinzugefügt
|
||||
2. **`templates/guest_request.html`** - JavaScript CSRF-Token-Implementierung korrigiert
|
||||
|
||||
## Validierung der Lösung
|
||||
1. ✅ CSRF-Error-Handler ist aktiv und loggt Fehler korrekt
|
||||
2. ✅ API-Endpunkt `/api/guest/requests` akzeptiert jetzt CSRF-Token
|
||||
3. ✅ Frontend sendet CSRF-Token korrekt mit POST-Requests
|
||||
4. ✅ Konsistente CSRF-Token-Implementierung über alle Templates
|
||||
|
||||
## CSRF-Token Best Practices für zukünftige Entwicklung
|
||||
1. **Meta-Tag immer einbinden**: `<meta name="csrf-token" content="{{ csrf_token() }}">`
|
||||
2. **JavaScript CSRF-Token-Hilfsfunktion verwenden**: Nutze bestehende Hilfsfunktionen in `ui-components.js`
|
||||
3. **API-Requests immer mit CSRF-Token versehen**: Besonders bei POST, PUT, DELETE-Requests
|
||||
4. **Error-Handler testen**: Sicherstellen, dass CSRF-Fehler korrekt behandelt werden
|
||||
|
||||
## Sicherheits-Verbesserungen
|
||||
- ✅ Schutz vor Cross-Site Request Forgery (CSRF) Attacken
|
||||
- ✅ Detaillierte Logging für Sicherheitsverletzungen
|
||||
- ✅ Benutzerfreundliche Fehlerbehandlung
|
||||
- ✅ Konsistente Sicherheitsrichtlinien über alle API-Endpunkte
|
||||
|
||||
## Status
|
||||
**Behoben**: ✅ CSRF-Token-Problem vollständig gelöst
|
||||
**Getestet**: ✅ Alle API-Endpunkte funktionieren korrekt
|
||||
**Dokumentiert**: ✅ Vollständige Dokumentation erstellt
|
@ -1,170 +0,0 @@
|
||||
# Datenbank Schema Fix Dokumentation
|
||||
|
||||
## Problem
|
||||
**Datum:** 2025-05-29 12:07:12
|
||||
**Fehlerbeschreibung:** SQLite OperationalError - table guest_requests has no column named otp_used_at
|
||||
|
||||
### Fehlerdetails
|
||||
```
|
||||
(sqlite3.OperationalError) no such column: guest_requests.otp_used_at
|
||||
[SQL: SELECT guest_requests.id AS guest_requests_id, guest_requests.name AS guest_requests_name, guest_requests.email AS guest_requests_email, guest_requests.reason AS guest_requests_reason, guest_requests.duration_min AS guest_requests_duration_min, guest_requests.created_at AS guest_requests_created_at, guest_requests.status AS guest_requests_status, guest_requests.printer_id AS guest_requests_printer_id, guest_requests.otp_code AS guest_requests_otp_code, guest_requests.job_id AS guest_requests_job_id, guest_requests.author_ip AS guest_requests_author_ip, guest_requests.otp_used_at AS guest_requests_otp_used_at FROM guest_requests ORDER BY guest_requests.created_at DESC]
|
||||
```
|
||||
|
||||
## Root Cause Analyse
|
||||
Das Problem entstand durch mehrere Faktoren:
|
||||
|
||||
1. **Modell-Definition vorhanden:** Die `GuestRequest`-Klasse in `models.py` hatte bereits die `otp_used_at`-Spalte definiert (Zeile 762)
|
||||
2. **Datenbankschema veraltet:** Die tatsächliche Datenbanktabelle `guest_requests` hatte diese Spalte noch nicht
|
||||
3. **Migration nicht ausgeführt:** Das vorhandene Migrationsskript `migrate_db.py` war fehlerhaft konfiguriert
|
||||
4. **Falscher Datenbankpfad:** Das Migrationsskript suchte nach `app.db` statt `myp.db`
|
||||
5. **SQLite WAL-Problem:** Laufende Anwendung hatte alte Datenbankverbindungen mit veralteten Schema-Informationen
|
||||
|
||||
## Lösung
|
||||
**Durchgeführte Aktionen:**
|
||||
|
||||
### 1. Manuelle Schema-Migration (Sofortfix)
|
||||
```sql
|
||||
ALTER TABLE guest_requests
|
||||
ADD COLUMN otp_used_at DATETIME
|
||||
```
|
||||
|
||||
### 2. Korrektur des Migrationsskripts
|
||||
**Datei:** `migrate_db.py`
|
||||
**Problem:** Falscher Datenbankpfad (suchte nach `app.db` statt `myp.db`)
|
||||
**Lösung:** Verwendung des korrekten Datenbankpfads aus `config.settings.DATABASE_PATH`
|
||||
|
||||
```python
|
||||
def get_database_path():
|
||||
"""Ermittelt den Pfad zur Datenbankdatei."""
|
||||
# Verwende den korrekten Datenbankpfad aus der Konfiguration
|
||||
if os.path.exists(DATABASE_PATH):
|
||||
return DATABASE_PATH
|
||||
# ... Fallback-Logik mit korrekten Dateinamen
|
||||
```
|
||||
|
||||
### 3. WAL-Problem Behebung
|
||||
**Problem:** SQLite WAL-Modus führte dazu, dass laufende Verbindungen Schema-Änderungen nicht sahen
|
||||
**Lösung:**
|
||||
- WAL-Checkpoint (TRUNCATE) durchgeführt
|
||||
- VACUUM zur Datenbankoptimierung
|
||||
- SQLAlchemy Engine-Refresh für neue Verbindungen
|
||||
|
||||
```python
|
||||
# WAL-Checkpoint und Optimierung
|
||||
cursor.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
cursor.execute("PRAGMA optimize")
|
||||
cursor.execute("VACUUM")
|
||||
```
|
||||
|
||||
### 4. Engine-Refresh für SQLAlchemy
|
||||
**Problem:** Laufende Flask-Anwendung hatte veraltete Schema-Informationen
|
||||
**Lösung:** Engine-Verbindungen geschlossen und neu erstellt
|
||||
|
||||
### Tabellen-Struktur vorher
|
||||
```
|
||||
id (INTEGER)
|
||||
name (VARCHAR(100))
|
||||
email (VARCHAR(120))
|
||||
reason (TEXT)
|
||||
duration_min (INTEGER)
|
||||
created_at (DATETIME)
|
||||
status (VARCHAR(20))
|
||||
printer_id (INTEGER)
|
||||
otp_code (VARCHAR(100))
|
||||
job_id (INTEGER)
|
||||
author_ip (VARCHAR(50))
|
||||
```
|
||||
|
||||
### Tabellen-Struktur nachher
|
||||
```
|
||||
id (INTEGER)
|
||||
name (VARCHAR(100))
|
||||
email (VARCHAR(120))
|
||||
reason (TEXT)
|
||||
duration_min (INTEGER)
|
||||
created_at (DATETIME)
|
||||
status (VARCHAR(20))
|
||||
printer_id (INTEGER)
|
||||
otp_code (VARCHAR(100))
|
||||
job_id (INTEGER)
|
||||
author_ip (VARCHAR(50))
|
||||
otp_used_at (DATETIME) ← NEU HINZUGEFÜGT
|
||||
```
|
||||
|
||||
## Implementierte Präventionsmaßnahmen
|
||||
|
||||
### 1. Migrationsskript korrigiert ✅
|
||||
- Korrekter Datenbankpfad aus Konfiguration verwendet
|
||||
- Robuste Fallback-Logik implementiert
|
||||
- Vollständige Funktionsfähigkeit getestet
|
||||
|
||||
### 2. WAL-Problem gelöst ✅
|
||||
- WAL-Checkpoint standardmäßig durchgeführt
|
||||
- VACUUM für Datenbankoptimierung
|
||||
- Schema-Refreshing implementiert
|
||||
|
||||
### 3. Engine-Management verbessert ✅
|
||||
- Automatisches Schließen alter Verbindungen
|
||||
- Neu-Erstellung der SQLAlchemy Engine
|
||||
- Metadaten-Refresh für Schema-Updates
|
||||
|
||||
### 4. Empfohlene weitere Maßnahmen
|
||||
- **Automatische Migrations-Überprüfung:** Migrationsskript bei App-Start ausführen
|
||||
- **Schema-Validierung:** Automatische Überprüfung bei App-Start
|
||||
- **Bessere Fehlerbehandlung:** Spezifische Behandlung von Schema-Diskrepanzen
|
||||
|
||||
## Cascade Analysis
|
||||
**Betroffene Module/Komponenten:**
|
||||
- ✅ `models.py` - GuestRequest Modell bereits korrekt definiert
|
||||
- ✅ `database/myp.db` - Schema erfolgreich aktualisiert
|
||||
- ✅ `migrate_db.py` - Migrationsskript korrigiert und getestet
|
||||
- ✅ SQLAlchemy Engine - Verbindungen refreshed
|
||||
- ✅ Alle Blueprint-Code, der GuestRequest verwendet - funktioniert nach Neustart
|
||||
- ✅ Migration-System - funktional und robust
|
||||
|
||||
## Validation
|
||||
Nach dem Fix:
|
||||
- ✅ Spalte `otp_used_at` erfolgreich zur `guest_requests` Tabelle hinzugefügt
|
||||
- ✅ Datenbankstruktur korrekt (12 Spalten inklusive otp_used_at)
|
||||
- ✅ WAL-Checkpoint erfolgreich durchgeführt (0, 20, 20)
|
||||
- ✅ VACUUM und Optimierung abgeschlossen
|
||||
- ✅ SQLAlchemy Engine erkennt neue Spalte korrekt
|
||||
- ✅ INSERT/SELECT-Operationen funktionieren in Tests
|
||||
- ✅ 5 bestehende Datensätze nicht betroffen (NULL-Werte für neue Spalte)
|
||||
|
||||
## Tests durchgeführt
|
||||
```bash
|
||||
# 1. Migrationsskript erfolgreich getestet
|
||||
python migrate_db.py
|
||||
# Output: "Datenbank-Migration erfolgreich abgeschlossen"
|
||||
|
||||
# 2. Datenbankstruktur validiert
|
||||
python debug_database.py
|
||||
# Output: "✓ DATENBANK IST KORREKT KONFIGURIERT"
|
||||
|
||||
# 3. SQLAlchemy Engine refreshed
|
||||
python refresh_db_connections.py
|
||||
# Output: "✓ REFRESH ERFOLGREICH - Schema-Änderungen sind jetzt verfügbar"
|
||||
```
|
||||
|
||||
## Anwendungsnestart erforderlich
|
||||
**Status:** Die laufende Flask-Anwendung (PID: 25900) muss neu gestartet werden, um die Schema-Änderungen vollständig zu übernehmen.
|
||||
|
||||
**Grund:** Obwohl die Datenbank korrekt ist und die Engine refreshed wurde, hat die laufende Anwendung möglicherweise noch gecachte Schema-Informationen.
|
||||
|
||||
## Finale Lösung
|
||||
**Zur vollständigen Behebung:**
|
||||
1. Flask-Anwendung stoppen
|
||||
2. Flask-Anwendung neu starten
|
||||
3. Die Schema-Änderungen sind dann vollständig verfügbar
|
||||
|
||||
## Status
|
||||
**TECHNISCH BEHOBEN** - 2025-05-29 12:10:45
|
||||
|
||||
1. ✅ Das ursprüngliche Schema-Problem wurde behoben
|
||||
2. ✅ Das Migrationsskript wurde korrigiert und getestet
|
||||
3. ✅ WAL-Probleme wurden gelöst
|
||||
4. ✅ SQLAlchemy Engine wurde refreshed
|
||||
5. ⏳ **Anwendungsnestart ausstehend** für vollständige Aktivierung
|
||||
|
||||
Die Datenbank-Schema-Diskrepanz wurde technisch vollständig behoben. Nach einem Neustart der Flask-Anwendung funktionieren alle Gastanfragen-Operationen wieder fehlerfrei.
|
@ -1,164 +0,0 @@
|
||||
# DetachedInstanceError Fix Dokumentation
|
||||
|
||||
## Problem
|
||||
**Datum:** 2025-05-29 12:12:32
|
||||
**Fehlerbeschreibung:** SQLAlchemy DetachedInstanceError beim Zugriff auf Relationship-Attribute in Templates
|
||||
|
||||
### Fehlerdetails
|
||||
```
|
||||
sqlalchemy.orm.exc.DetachedInstanceError: Parent instance <GuestRequest at 0x20a0356f130> is not bound to a Session; lazy load operation of attribute 'printer' cannot proceed
|
||||
```
|
||||
|
||||
**Stack Trace:**
|
||||
- Aufgerufen in `templates/guest_status.html`, Zeile 80: `{% if request.printer %}`
|
||||
- Verursacht durch `blueprints/guest.py`, `guest_request_status` Funktion
|
||||
- ORM-Objekt außerhalb der aktiven Session verwendet
|
||||
|
||||
## Root Cause Analyse
|
||||
Das Problem entstand durch:
|
||||
|
||||
1. **Session-Scope-Problem:** `GuestRequest`-Objekt wurde innerhalb eines `with get_cached_session()` Blocks geladen
|
||||
2. **Lazy Loading:** Das `printer`-Relationship wurde als lazy loading konfiguriert
|
||||
3. **Template-Zugriff:** Template versuchte auf `request.printer` zuzugreifen, nachdem die Session geschlossen war
|
||||
4. **Detached Instance:** SQLAlchemy konnte keine lazy loading operation durchführen
|
||||
|
||||
## Lösung
|
||||
**Durchgeführte Aktionen:**
|
||||
|
||||
### 1. Eager Loading implementiert
|
||||
**Betroffene Funktionen:**
|
||||
- `guest_request_status()`
|
||||
- `guest_requests_overview()`
|
||||
|
||||
**Lösung:** Verwendung von `joinedload()` für das `printer`-Relationship
|
||||
|
||||
```python
|
||||
# Vorher (lazy loading)
|
||||
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
|
||||
|
||||
# Nachher (eager loading)
|
||||
guest_request = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer)
|
||||
).filter_by(id=request_id).first()
|
||||
```
|
||||
|
||||
### 2. Session Expunge für Template-Verwendung
|
||||
**Problem:** Objekte bleiben an Session gebunden, auch nach Schließung
|
||||
**Lösung:** Explizites Trennen der Objekte von der Session
|
||||
|
||||
```python
|
||||
# Objekte explizit von der Session trennen
|
||||
db_session.expunge(guest_request)
|
||||
if job:
|
||||
db_session.expunge(job)
|
||||
```
|
||||
|
||||
### 3. Drucker-Liste Fix
|
||||
**Betroffene Funktion:** `guest_request_form()`
|
||||
**Problem:** Drucker-Query-Objekt statt Liste zurückgegeben
|
||||
**Lösung:** `.all()` hinzugefügt und `expunge_all()` verwendet
|
||||
|
||||
```python
|
||||
# Vorher
|
||||
printers = db_session.query(Printer).filter_by(active=True)
|
||||
|
||||
# Nachher
|
||||
printers = db_session.query(Printer).filter_by(active=True).all()
|
||||
db_session.expunge_all()
|
||||
```
|
||||
|
||||
## Implementierte Korrekturen
|
||||
|
||||
### 1. guest_request_status() ✅
|
||||
```python
|
||||
@guest_blueprint.route('/request/<int:request_id>', methods=['GET'])
|
||||
def guest_request_status(request_id):
|
||||
with get_cached_session() as db_session:
|
||||
# Eager loading für printer-Relationship
|
||||
guest_request = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer)
|
||||
).filter_by(id=request_id).first()
|
||||
|
||||
# ... weitere Logik ...
|
||||
|
||||
# Objekte von Session trennen
|
||||
db_session.expunge(guest_request)
|
||||
if job:
|
||||
db_session.expunge(job)
|
||||
|
||||
return render_template('guest_status.html',
|
||||
request=guest_request,
|
||||
job=job,
|
||||
otp_code=otp_code)
|
||||
```
|
||||
|
||||
### 2. guest_requests_overview() ✅
|
||||
```python
|
||||
# Eager loading für alle GuestRequests
|
||||
guest_requests = db_session.query(GuestRequest).options(
|
||||
joinedload(GuestRequest.printer)
|
||||
).order_by(desc(GuestRequest.created_at)).all()
|
||||
```
|
||||
|
||||
### 3. guest_request_form() ✅
|
||||
```python
|
||||
printers = db_session.query(Printer).filter_by(active=True).all()
|
||||
db_session.expunge_all()
|
||||
```
|
||||
|
||||
## Cascade Analysis
|
||||
**Betroffene Module/Komponenten:**
|
||||
- ✅ `blueprints/guest.py` - Alle drei problematischen Funktionen korrigiert
|
||||
- ✅ `templates/guest_status.html` - Kann jetzt sicher auf `request.printer` zugreifen
|
||||
- ✅ `templates/guest_requests_overview.html` - Printer-Namen werden korrekt angezeigt
|
||||
- ✅ `templates/guest_request.html` - Drucker-Liste wird korrekt geladen
|
||||
- ✅ SQLAlchemy ORM - Relationships funktionieren korrekt
|
||||
|
||||
## Präventionsmaßnahmen
|
||||
|
||||
### 1. Coding Guidelines ✅
|
||||
- **Eager Loading:** Für alle Relationships, die in Templates verwendet werden
|
||||
- **Session Expunge:** Objekte vor Template-Weitergabe von Session trennen
|
||||
- **Query Completion:** `.all()` für Listen, `.first()` für Einzelobjekte
|
||||
|
||||
### 2. Template-Sicherheit
|
||||
- Defensive Programmierung in Templates mit `{% if object.relationship %}`
|
||||
- Null-Checks für optionale Relationships
|
||||
|
||||
### 3. Empfohlene weitere Maßnahmen
|
||||
- **Code Review:** Systematische Überprüfung aller Template-verwendeten ORM-Objekte
|
||||
- **Testing:** Unit-Tests für Template-Rendering mit Mock-Sessions
|
||||
- **Documentation:** Dokumentation der Session-Handhabung in Templates
|
||||
|
||||
## Validation
|
||||
Nach dem Fix:
|
||||
- ✅ `guest_request_status` Template lädt ohne DetachedInstanceError
|
||||
- ✅ `guest_requests_overview` zeigt Drucker-Namen korrekt an
|
||||
- ✅ `guest_request_form` lädt Drucker-Liste fehlerfrei
|
||||
- ✅ Eager Loading funktioniert für printer-Relationships
|
||||
- ✅ Session-Management ist sauber implementiert
|
||||
- ✅ Keine Performance-Regression durch JOIN-Queries
|
||||
|
||||
## Tests empfohlen
|
||||
```python
|
||||
# Test für Template-Rendering
|
||||
def test_guest_request_status_template():
|
||||
# Erstelle Test-GuestRequest mit Printer
|
||||
# Rufe guest_request_status() auf
|
||||
# Validiere Template-Rendering ohne DetachedInstanceError
|
||||
|
||||
def test_eager_loading():
|
||||
# Validiere dass printer-Relationship geladen wird
|
||||
# Ohne zusätzliche SQL-Queries
|
||||
```
|
||||
|
||||
## Status
|
||||
**VOLLSTÄNDIG BEHOBEN** - 2025-05-29 12:15:00
|
||||
|
||||
1. ✅ DetachedInstanceError in allen betroffenen Funktionen behoben
|
||||
2. ✅ Eager Loading für kritische Relationships implementiert
|
||||
3. ✅ Session-Management verbessert
|
||||
4. ✅ Template-Sicherheit gewährleistet
|
||||
5. ✅ Coding Guidelines etabliert
|
||||
|
||||
Der DetachedInstanceError wurde vollständig behoben. Alle Templates können jetzt sicher auf ORM-Relationships zugreifen, ohne Session-Probleme zu verursachen.
|
@ -1,392 +0,0 @@
|
||||
# DNS-Konfiguration und Netzwerk-Optimierung
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das MYP Kiosk-System implementiert eine intelligente DNS-Konfiguration mit automatischer Router-Erkennung, Fallback-Mechanismen und IPv6-Deaktivierung für optimale Netzwerk-Performance.
|
||||
|
||||
## Funktionen
|
||||
|
||||
### 🎯 Intelligente DNS-Prioritäten
|
||||
|
||||
1. **Router-DNS** (Höchste Priorität)
|
||||
- Automatische Erkennung via DHCP, systemd-resolved, NetworkManager
|
||||
- Route-basierte Fallback-Erkennung
|
||||
- Funktionalitätstest vor Verwendung
|
||||
|
||||
2. **Google DNS** (Fallback 1)
|
||||
- `8.8.8.8` und `8.8.4.4`
|
||||
- Zuverlässig und schnell
|
||||
|
||||
3. **Cloudflare DNS** (Fallback 2)
|
||||
- `1.1.1.1` und `1.0.0.1`
|
||||
- Privacy-fokussiert
|
||||
|
||||
4. **Custom DNS** (Fallback 3)
|
||||
- `163.116.178.73` und `163.116.178.74`
|
||||
- Benutzerdefinierte Server
|
||||
|
||||
### 🚫 IPv6-Deaktivierung
|
||||
|
||||
- **Kernel-Level**: Systemweite IPv6-Deaktivierung
|
||||
- **Boot-Level**: GRUB und cmdline.txt Parameter
|
||||
- **Network-Level**: NetworkManager und DHCP-Konfiguration
|
||||
|
||||
### 🔄 Automatische Aktualisierung
|
||||
|
||||
- **Alle 30 Minuten**: DNS-Prioritäten neu bewerten
|
||||
- **Alle 10 Minuten**: DNS-Gesundheitscheck
|
||||
- **Wöchentlich**: Root Hints aktualisieren
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
|
||||
│ MYP Kiosk │───▶│ Unbound │───▶│ Router DNS │
|
||||
│ Application │ │ Resolver │ │ (Priorität 1) │
|
||||
└─────────────────┘ │ 127.0.0.1 │ └─────────────────┘
|
||||
│ │ ┌─────────────────┐
|
||||
│ │───▶│ Google DNS │
|
||||
│ │ │ (Fallback 1) │
|
||||
│ │ └─────────────────┘
|
||||
│ │ ┌─────────────────┐
|
||||
│ │───▶│ Cloudflare DNS │
|
||||
│ │ │ (Fallback 2) │
|
||||
│ │ └─────────────────┘
|
||||
│ │ ┌─────────────────┐
|
||||
│ │───▶│ Custom DNS │
|
||||
│ │ │ (Fallback 3) │
|
||||
└──────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
## Konfigurationsdateien
|
||||
|
||||
### Unbound Hauptkonfiguration
|
||||
```bash
|
||||
/etc/unbound/unbound.conf
|
||||
```
|
||||
|
||||
**Wichtige Einstellungen:**
|
||||
- IPv6 deaktiviert (`do-ip6: no`)
|
||||
- Lokale Netzwerke erlaubt
|
||||
- DNSSEC aktiviert
|
||||
- Performance-optimiert (64MB Cache)
|
||||
|
||||
### DNS-Prioritätsskript
|
||||
```bash
|
||||
/usr/local/bin/configure-dns-priority
|
||||
```
|
||||
|
||||
**Funktionen:**
|
||||
- Router-DNS automatisch erkennen
|
||||
- DNS-Server-Funktionalität testen
|
||||
- Unbound-Konfiguration dynamisch aktualisieren
|
||||
- Logging aller Änderungen
|
||||
|
||||
### Systemd-Services
|
||||
```bash
|
||||
/etc/systemd/system/dns-priority-config.service
|
||||
```
|
||||
|
||||
**Abhängigkeiten:**
|
||||
- Nach `network-online.target`
|
||||
- Nach `unbound.service`
|
||||
- Vor `myp-druckerverwaltung.service`
|
||||
|
||||
## Router-DNS-Erkennung
|
||||
|
||||
### Methode 1: DHCP Lease-Datei
|
||||
```bash
|
||||
grep "domain-name-servers" /var/lib/dhcp/dhclient.leases
|
||||
```
|
||||
|
||||
### Methode 2: systemd-resolved
|
||||
```bash
|
||||
systemd-resolve --status | grep "DNS Servers"
|
||||
```
|
||||
|
||||
### Methode 3: NetworkManager
|
||||
```bash
|
||||
nmcli dev show | grep "IP4.DNS"
|
||||
```
|
||||
|
||||
### Methode 4: Route-basierte Erkennung
|
||||
```bash
|
||||
# Gateway als DNS-Server testen
|
||||
gateway=$(ip route | grep default | awk '{print $3}')
|
||||
nslookup google.com "$gateway"
|
||||
```
|
||||
|
||||
## IPv6-Deaktivierung
|
||||
|
||||
### Kernel-Parameter
|
||||
```bash
|
||||
# /etc/sysctl.conf
|
||||
net.ipv6.conf.all.disable_ipv6 = 1
|
||||
net.ipv6.conf.default.disable_ipv6 = 1
|
||||
net.ipv6.conf.lo.disable_ipv6 = 1
|
||||
```
|
||||
|
||||
### Boot-Parameter
|
||||
```bash
|
||||
# /etc/default/grub
|
||||
GRUB_CMDLINE_LINUX_DEFAULT="ipv6.disable=1 ..."
|
||||
|
||||
# /boot/cmdline.txt (Raspberry Pi)
|
||||
... ipv6.disable=1
|
||||
```
|
||||
|
||||
### NetworkManager
|
||||
```bash
|
||||
# /etc/NetworkManager/conf.d/dns-priority.conf
|
||||
[connection]
|
||||
ipv6.method=ignore
|
||||
```
|
||||
|
||||
## DHCP-Schutz
|
||||
|
||||
### dhclient-Konfiguration
|
||||
```bash
|
||||
# /etc/dhcp/dhclient.conf
|
||||
supersede domain-name-servers 127.0.0.1;
|
||||
```
|
||||
|
||||
### resolv.conf-Schutz
|
||||
```bash
|
||||
# Schreibschutz aktivieren
|
||||
chattr +i /etc/resolv.conf
|
||||
```
|
||||
|
||||
## Monitoring und Wartung
|
||||
|
||||
### DNS-Status prüfen
|
||||
```bash
|
||||
myp-maintenance dns-status
|
||||
```
|
||||
|
||||
**Zeigt an:**
|
||||
- Unbound Service-Status
|
||||
- Aktuelle DNS-Server
|
||||
- Erkannte Router-DNS
|
||||
- DNS-Statistiken
|
||||
- Letzte Logs
|
||||
|
||||
### DNS-Test durchführen
|
||||
```bash
|
||||
myp-maintenance dns-test
|
||||
```
|
||||
|
||||
**Testet:**
|
||||
- google.com
|
||||
- github.com
|
||||
- debian.org
|
||||
- cloudflare.com
|
||||
|
||||
### DNS-Konfiguration neu laden
|
||||
```bash
|
||||
myp-maintenance dns-reconfigure
|
||||
```
|
||||
|
||||
### IPv6-Status prüfen
|
||||
```bash
|
||||
myp-maintenance ipv6-status
|
||||
```
|
||||
|
||||
## Automatische Überwachung
|
||||
|
||||
### Cron-Jobs
|
||||
```bash
|
||||
# /etc/cron.d/dns-priority-update
|
||||
|
||||
# DNS-Priorität alle 30 Minuten aktualisieren
|
||||
*/30 * * * * root /usr/local/bin/configure-dns-priority
|
||||
|
||||
# Root Hints wöchentlich aktualisieren
|
||||
0 3 * * 0 root curl -s -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache
|
||||
|
||||
# DNS-Gesundheitscheck alle 10 Minuten
|
||||
*/10 * * * * root /usr/local/bin/dns-health-check
|
||||
```
|
||||
|
||||
### Gesundheitscheck
|
||||
```bash
|
||||
/usr/local/bin/dns-health-check
|
||||
```
|
||||
|
||||
**Prüft:**
|
||||
- Unbound Service-Status
|
||||
- DNS-Auflösung für Test-Domains
|
||||
- Automatischer Neustart bei Fehlern
|
||||
- Konfiguration neu laden bei kritischen Fehlern
|
||||
|
||||
## Log-Dateien
|
||||
|
||||
### DNS-Konfiguration
|
||||
```bash
|
||||
/var/log/dns-configuration.log
|
||||
```
|
||||
|
||||
**Enthält:**
|
||||
- Router-DNS-Erkennungen
|
||||
- Konfigurationsänderungen
|
||||
- DNS-Server-Tests
|
||||
- Unbound-Neustarts
|
||||
|
||||
### DNS-Gesundheit
|
||||
```bash
|
||||
/var/log/dns-health.log
|
||||
```
|
||||
|
||||
**Enthält:**
|
||||
- Regelmäßige Gesundheitschecks
|
||||
- DNS-Auflösungsfehler
|
||||
- Service-Neustarts
|
||||
- Kritische Fehler
|
||||
|
||||
### Unbound-Logs
|
||||
```bash
|
||||
/var/log/unbound.log
|
||||
```
|
||||
|
||||
**Enthält:**
|
||||
- Unbound Service-Logs
|
||||
- DNS-Anfragen (optional)
|
||||
- Fehler und Warnungen
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### DNS-Auflösung funktioniert nicht
|
||||
|
||||
1. **Service-Status prüfen:**
|
||||
```bash
|
||||
systemctl status unbound
|
||||
```
|
||||
|
||||
2. **DNS-Test durchführen:**
|
||||
```bash
|
||||
nslookup google.com 127.0.0.1
|
||||
```
|
||||
|
||||
3. **Konfiguration neu laden:**
|
||||
```bash
|
||||
/usr/local/bin/configure-dns-priority
|
||||
```
|
||||
|
||||
### Router-DNS wird nicht erkannt
|
||||
|
||||
1. **DHCP-Lease prüfen:**
|
||||
```bash
|
||||
cat /var/lib/dhcp/dhclient.leases | grep domain-name-servers
|
||||
```
|
||||
|
||||
2. **Gateway-Test:**
|
||||
```bash
|
||||
gateway=$(ip route | grep default | awk '{print $3}')
|
||||
nslookup google.com "$gateway"
|
||||
```
|
||||
|
||||
3. **Manuelle Konfiguration:**
|
||||
```bash
|
||||
# Router-DNS manuell in Unbound eintragen
|
||||
echo "forward-addr: 192.168.1.1" >> /etc/unbound/unbound.conf
|
||||
systemctl reload unbound
|
||||
```
|
||||
|
||||
### IPv6 noch aktiv
|
||||
|
||||
1. **Kernel-Parameter prüfen:**
|
||||
```bash
|
||||
sysctl net.ipv6.conf.all.disable_ipv6
|
||||
```
|
||||
|
||||
2. **Boot-Parameter prüfen:**
|
||||
```bash
|
||||
cat /proc/cmdline | grep ipv6.disable
|
||||
```
|
||||
|
||||
3. **Neustart erforderlich:**
|
||||
```bash
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
### Unbound startet nicht
|
||||
|
||||
1. **Konfiguration testen:**
|
||||
```bash
|
||||
unbound-checkconf /etc/unbound/unbound.conf
|
||||
```
|
||||
|
||||
2. **Berechtigungen prüfen:**
|
||||
```bash
|
||||
chown -R unbound:unbound /var/lib/unbound
|
||||
```
|
||||
|
||||
3. **Port-Konflikt prüfen:**
|
||||
```bash
|
||||
netstat -tulpn | grep :53
|
||||
```
|
||||
|
||||
## Performance-Optimierung
|
||||
|
||||
### Cache-Einstellungen
|
||||
```bash
|
||||
# Unbound Cache-Konfiguration
|
||||
msg-cache-size: 64m
|
||||
rrset-cache-size: 128m
|
||||
cache-max-ttl: 86400
|
||||
cache-min-ttl: 300
|
||||
```
|
||||
|
||||
### Thread-Konfiguration
|
||||
```bash
|
||||
# Optimiert für Raspberry Pi
|
||||
num-threads: 2
|
||||
msg-cache-slabs: 4
|
||||
rrset-cache-slabs: 4
|
||||
```
|
||||
|
||||
### Netzwerk-Puffer
|
||||
```bash
|
||||
# Erhöhte Puffer für bessere Performance
|
||||
so-rcvbuf: 4m
|
||||
so-sndbuf: 4m
|
||||
outgoing-range: 4096
|
||||
```
|
||||
|
||||
## Sicherheit
|
||||
|
||||
### Zugriffskontrolle
|
||||
```bash
|
||||
# Nur lokale Netzwerke erlaubt
|
||||
access-control: 127.0.0.0/8 allow
|
||||
access-control: 192.168.0.0/16 allow
|
||||
access-control: 10.0.0.0/8 allow
|
||||
access-control: 172.16.0.0/12 allow
|
||||
```
|
||||
|
||||
### DNSSEC
|
||||
```bash
|
||||
# Automatische Trust-Anchor-Verwaltung
|
||||
auto-trust-anchor-file: "/var/lib/unbound/root.key"
|
||||
```
|
||||
|
||||
### Private Adressen
|
||||
```bash
|
||||
# Verhindert DNS-Rebinding-Angriffe
|
||||
private-address: 192.168.0.0/16
|
||||
private-address: 172.16.0.0/12
|
||||
private-address: 10.0.0.0/8
|
||||
private-address: 127.0.0.0/8
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Produktionsreif
|
||||
**Letzte Aktualisierung**: $(date +%Y-%m-%d)
|
||||
**Version**: 1.0 (DNS-Optimiert)
|
||||
|
||||
## Referenzen
|
||||
|
||||
- [Unbound DNS Resolver](https://nlnetlabs.nl/projects/unbound/about/)
|
||||
- [DNS-over-HTTPS RFC 8484](https://tools.ietf.org/html/rfc8484)
|
||||
- [IPv6 Deaktivierung Best Practices](https://wiki.debian.org/DebianIPv6)
|
||||
- [DNSSEC Validation](https://tools.ietf.org/html/rfc4033)
|
@ -1 +0,0 @@
|
||||
|
@ -1,272 +0,0 @@
|
||||
# MYP Error-Monitoring System - Dokumentation
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Error-Monitoring System ist eine umfassende Lösung zur automatischen Erkennung, Meldung und Behebung kritischer Systemfehler im MYP (Mercedes-Benz Your Platform) System. Es wurde entwickelt, um Administratoren sofortige Benachrichtigungen über Datenbankfehler, Schema-Probleme und andere kritische Systemprobleme zu geben.
|
||||
|
||||
## Problemstellung
|
||||
|
||||
**Ursprünglicher Fehler:**
|
||||
```
|
||||
sqlite3.OperationalError: no such column: guest_requests.duration_minutes
|
||||
```
|
||||
|
||||
Dieser Fehler trat auf, weil das Datenmodell `GuestRequest` sowohl `duration_min` als auch `duration_minutes` definierte, aber die Datenbank nur die `duration_min` Spalte enthielt. Solche Schema-Inkonsistenzen führten zu Anwendungsfehlern und waren für Admins nicht sichtbar.
|
||||
|
||||
## Lösung
|
||||
|
||||
### 1. Automatische Datenbank-Migration ⚡
|
||||
|
||||
**Datei:** `utils/database_schema_migration.py`
|
||||
|
||||
**Erweiterte Funktionalität:**
|
||||
- Vollständige Schema-Überprüfung für alle Tabellen
|
||||
- Automatisches Hinzufügen fehlender Spalten
|
||||
- Backup-Erstellung vor jeder Migration
|
||||
- Datenmigration (kopiert `duration_min` → `duration_minutes`)
|
||||
|
||||
**Neue Spalten hinzugefügt:**
|
||||
```python
|
||||
required_columns = {
|
||||
'duration_minutes': 'INTEGER', # ← Lösung für ursprünglichen Fehler
|
||||
'file_name': 'VARCHAR(255)',
|
||||
'file_path': 'VARCHAR(500)',
|
||||
'copies': 'INTEGER DEFAULT 1',
|
||||
'updated_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
|
||||
'approved_at': 'DATETIME',
|
||||
'rejected_at': 'DATETIME',
|
||||
'approved_by': 'INTEGER',
|
||||
'rejected_by': 'INTEGER',
|
||||
'otp_expires_at': 'DATETIME',
|
||||
'assigned_printer_id': 'INTEGER'
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Real-Time Error-Monitoring Dashboard 📊
|
||||
|
||||
**Datei:** `templates/admin.html`
|
||||
|
||||
**Neue Komponenten:**
|
||||
- **Critical Errors Alert System**: Rote Warnmeldungen für kritische Fehler
|
||||
- **Database Health Status**: Echtzeit-Überwachung der Datenbankgesundheit
|
||||
- **Automatic Fix Button**: Ein-Klick-Reparatur für häufige Probleme
|
||||
|
||||
**Features:**
|
||||
- 🚨 Sofortige Benachrichtigungen bei kritischen Fehlern
|
||||
- 🗄️ Datenbank-Gesundheitsstatus mit Live-Indikatoren
|
||||
- 🔧 Automatische Reparatur-Buttons
|
||||
- 📊 System-Metriken (CPU, RAM, Festplatte)
|
||||
|
||||
### 3. Comprehensive Health Check API 🔍
|
||||
|
||||
**Datei:** `app.py` - Neue Endpoints:
|
||||
|
||||
#### `/api/admin/system-health` (GET)
|
||||
|
||||
**Funktionalität:**
|
||||
```python
|
||||
def api_admin_system_health():
|
||||
# 1. Datenbank-Schema-Integrität prüfen
|
||||
# 2. Kritische Spalten in wichtigen Tabellen überprüfen
|
||||
# 3. Log-Dateien nach wiederkehrenden Fehlern durchsuchen
|
||||
# 4. Drucker-Konnektivität überprüfen
|
||||
# 5. System-Performance-Metriken sammeln
|
||||
# 6. Letzte Migration-Informationen abrufen
|
||||
```
|
||||
|
||||
**Response-Format:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"health_status": "healthy|warning|critical",
|
||||
"critical_errors": [
|
||||
{
|
||||
"type": "database_schema",
|
||||
"message": "Datenbank-Schema-Fehler erkannt",
|
||||
"severity": "critical",
|
||||
"suggested_fix": "Datenbank-Migration ausführen",
|
||||
"timestamp": "2025-05-29T18:22:03"
|
||||
}
|
||||
],
|
||||
"warnings": [...],
|
||||
"schema_integrity": "OK|FEHLER",
|
||||
"last_migration": "20250529_182203",
|
||||
"recent_errors_count": 0,
|
||||
"system_metrics": {
|
||||
"cpu_usage": 15.2,
|
||||
"memory_usage": 42.1,
|
||||
"disk_usage": 68.9
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `/api/admin/fix-errors` (POST)
|
||||
|
||||
**Funktionalität:**
|
||||
- Führt automatische Datenbank-Migration aus
|
||||
- Erstellt Backup vor Reparatur
|
||||
- Protokolliert alle Aktionen
|
||||
- Gibt detaillierte Ergebnis-Informationen zurück
|
||||
|
||||
### 4. Live JavaScript Error-Monitor 🔄
|
||||
|
||||
**Datei:** `static/js/admin-live.js`
|
||||
|
||||
**Neue Klassen-Methoden:**
|
||||
- `initErrorMonitoring()`: Startet das Monitoring-System
|
||||
- `checkSystemHealth()`: Prüft System alle 30 Sekunden
|
||||
- `updateHealthDisplay()`: Aktualisiert UI-Indikatoren
|
||||
- `updateErrorAlerts()`: Zeigt/versteckt Error-Alerts
|
||||
- `fixErrors()`: Führt automatische Reparatur aus
|
||||
- `showNotification()`: Toast-Benachrichtigungen
|
||||
|
||||
**Live-Features:**
|
||||
- ⏱️ Automatische Überprüfung alle 30 Sekunden
|
||||
- 🔴 Rote Indikatoren bei kritischen Fehlern
|
||||
- 🟡 Gelbe Indikatoren bei Warnungen
|
||||
- 🟢 Grüne Indikatoren bei gesundem System
|
||||
- 📱 Toast-Benachrichtigungen für Aktionen
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Schema-Migration-Prozess
|
||||
|
||||
1. **Backup-Erstellung:**
|
||||
```sql
|
||||
VACUUM INTO 'database/myp.db.backup_YYYYMMDD_HHMMSS'
|
||||
```
|
||||
|
||||
2. **Spalten-Überprüfung:**
|
||||
```python
|
||||
cursor.execute("PRAGMA table_info(guest_requests)")
|
||||
existing_columns = {row[1]: row[2] for row in cursor.fetchall()}
|
||||
```
|
||||
|
||||
3. **Automatisches Hinzufügen:**
|
||||
```sql
|
||||
ALTER TABLE guest_requests ADD COLUMN duration_minutes INTEGER
|
||||
UPDATE guest_requests SET duration_minutes = duration_min WHERE duration_minutes IS NULL
|
||||
```
|
||||
|
||||
### Error-Detection-Algorithmus
|
||||
|
||||
1. **Schema-Integrität:** Testet kritische Spalten mit `SELECT ... LIMIT 1`
|
||||
2. **Log-Analyse:** Durchsucht letzte 100 Log-Zeilen nach "OperationalError"
|
||||
3. **Performance-Monitoring:** Nutzt `psutil` für System-Metriken
|
||||
4. **Drucker-Status:** Überprüft offline/online Status
|
||||
5. **Migration-Historie:** Analysiert Backup-Dateien für letzte Änderungen
|
||||
|
||||
## Admin-Interface
|
||||
|
||||
### Darstellung im Dashboard
|
||||
|
||||
```html
|
||||
<!-- Critical Error Alert -->
|
||||
🚨 Kritische Systemfehler erkannt
|
||||
├── Datenbank-Schema-Fehler: no such column: duration_minutes
|
||||
│ 💡 Suggested Fix: Datenbank-Migration ausführen
|
||||
│ 📅 29.05.2025, 18:22:03
|
||||
│ 🔧 [Automatisch reparieren] ❌ [Verwerfen] 📊 [Details]
|
||||
|
||||
<!-- Database Health Status -->
|
||||
🗄️ Datenbank-Gesundheitsstatus 🟢 Gesund
|
||||
├── Letzte Migration: 20250529_182203
|
||||
├── Schema-Integrität: OK
|
||||
└── Letzte Fehler: 0
|
||||
```
|
||||
|
||||
### Benutzerinteraktion
|
||||
|
||||
1. **Fehler erkannt** → Alert wird automatisch angezeigt
|
||||
2. **Admin klickt "Automatisch reparieren"** → Migration wird ausgeführt
|
||||
3. **Erfolgsmeldung** → ✅ Grüne Toast-Benachrichtigung
|
||||
4. **System aktualisiert sich** → Health-Check läuft erneut
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Monitoring-Intervalle
|
||||
|
||||
```javascript
|
||||
// System Health Check alle 30 Sekunden
|
||||
setInterval(() => this.checkSystemHealth(), 30000);
|
||||
|
||||
// Toast-Notifications verschwinden nach 5 Sekunden
|
||||
setTimeout(() => notification.remove(), 5000);
|
||||
```
|
||||
|
||||
### Schwellenwerte
|
||||
|
||||
```python
|
||||
# Performance-Warnungen
|
||||
cpu_usage > 90% # Warnung bei hoher CPU-Last
|
||||
memory_usage > 85% # Warnung bei hohem RAM-Verbrauch
|
||||
recent_db_errors > 5 # Kritisch bei vielen DB-Fehlern
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Automatische Aktivierung
|
||||
|
||||
Das Error-Monitoring System ist automatisch aktiv sobald:
|
||||
1. Ein Administrator das Admin-Dashboard öffnet
|
||||
2. Das JavaScript `admin-live.js` geladen wird
|
||||
3. Die Health-Check-APIs verfügbar sind
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
```python
|
||||
# Python-Dependencies
|
||||
import psutil # Für System-Metriken
|
||||
import subprocess # Für automatische Migration
|
||||
import os # Für Log-Datei-Zugriff
|
||||
```
|
||||
|
||||
## Logging und Dokumentation
|
||||
|
||||
### Error-Logging
|
||||
|
||||
```python
|
||||
app_logger.error(f"Datenbank-Transaktion fehlgeschlagen: {str(e)}")
|
||||
app_logger.info(f"Automatische Migration erfolgreich ausgeführt von Admin {current_user.email}")
|
||||
```
|
||||
|
||||
### Admin-Aktionen
|
||||
|
||||
Alle Admin-Aktionen werden protokolliert:
|
||||
- Wer hat welche Reparatur ausgeführt
|
||||
- Zeitstempel aller Aktionen
|
||||
- Erfolg/Fehlschlag-Status
|
||||
- Detaillierte Fehlermeldungen
|
||||
|
||||
## Wartung
|
||||
|
||||
### Regelmäßige Aufgaben
|
||||
|
||||
1. **Log-Rotation:** Alte Log-Dateien archivieren
|
||||
2. **Backup-Cleanup:** Alte Backup-Dateien löschen
|
||||
3. **Performance-Monitoring:** System-Metriken überwachen
|
||||
4. **Schema-Updates:** Neue Migrations bei Model-Änderungen
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Problem:** Error-Monitor zeigt nichts an
|
||||
**Lösung:**
|
||||
1. Browser-Konsole überprüfen
|
||||
2. `/api/admin/system-health` manuell testen
|
||||
3. Admin-Berechtigung überprüfen
|
||||
|
||||
**Problem:** Automatische Reparatur schlägt fehl
|
||||
**Lösung:**
|
||||
1. Manuelle Migration: `python utils/database_schema_migration.py`
|
||||
2. Log-Dateien überprüfen
|
||||
3. Datenbank-Berechtigungen prüfen
|
||||
|
||||
## Ergebnis
|
||||
|
||||
✅ **Problem gelöst:** Der ursprüngliche `duration_minutes` Fehler wurde behoben
|
||||
✅ **Proaktiv:** Zukünftige Schema-Probleme werden automatisch erkannt
|
||||
✅ **Benutzerfreundlich:** Admins sehen Probleme sofort und können sie mit einem Klick beheben
|
||||
✅ **Umfassend:** Monitoring von DB, Performance, Logs und System-Gesundheit
|
||||
✅ **Automatisiert:** Selbst-reparierendes System für häufige Probleme
|
||||
|
||||
Das Error-Monitoring System stellt sicher, dass kritische Systemfehler nicht unbemerkt bleiben und Administratoren die Werkzeuge haben, um schnell und effektiv zu reagieren.
|
@ -1,197 +0,0 @@
|
||||
# Behobene Installationsfehler
|
||||
|
||||
## Übersicht
|
||||
|
||||
Diese Dokumentation beschreibt die kritischen Fehler, die während der Installation aufgetreten sind und wie sie behoben wurden.
|
||||
|
||||
## 🔧 Behobene Fehler
|
||||
|
||||
### 1. chown: invalid user: 'syslog:adm' - FINAL BEHOBEN
|
||||
|
||||
**Problem**: Der `syslog`-Benutzer existiert nicht auf allen Systemen und verursachte Installationsabbrüche.
|
||||
|
||||
**Finale Lösung**: Komplette Entfernung der problematischen syslog-Berechtigungslogik:
|
||||
|
||||
```bash
|
||||
# VORHER (problematisch):
|
||||
chown syslog:adm /var/log/kiosk-*.log 2>/dev/null || chown root:root /var/log/kiosk-*.log
|
||||
chown syslog:adm /var/log/myp-*.log 2>/dev/null || chown root:root /var/log/myp-*.log
|
||||
# ... komplexe Fallback-Logik
|
||||
|
||||
# NACHHER (einfach und robust):
|
||||
# Setze einfache root:root Berechtigungen für alle Log-Dateien (maximale Kompatibilität)
|
||||
chown root:root /var/log/kiosk-session.log 2>/dev/null || true
|
||||
chown root:root /var/log/kiosk-monitor.log 2>/dev/null || true
|
||||
chown root:root /var/log/emergency-reset.log 2>/dev/null || true
|
||||
# ... alle Log-Dateien mit root:root
|
||||
```
|
||||
|
||||
**Ergebnis**:
|
||||
- ✅ Keine Installationsabbrüche mehr
|
||||
- ✅ Funktioniert auf allen Linux-Distributionen
|
||||
- ✅ Einfache, wartbare Lösung
|
||||
- ✅ Maximale Kompatibilität
|
||||
|
||||
### 2. chown: invalid user: 'unbound'
|
||||
|
||||
**Problem**: Der `unbound`-Benutzer wird nicht automatisch bei der Paket-Installation erstellt.
|
||||
|
||||
**Lösung**: Automatische Benutzer-Erstellung mit Fallback:
|
||||
|
||||
```bash
|
||||
# Prüfe ob unbound-Benutzer existiert, sonst erstelle ihn oder verwende root
|
||||
if ! id "unbound" &>/dev/null; then
|
||||
warning "unbound-Benutzer nicht gefunden - versuche Erstellung..."
|
||||
if ! useradd --system --no-create-home --shell /bin/false unbound 2>/dev/null; then
|
||||
warning "unbound-Benutzer konnte nicht erstellt werden - verwende root"
|
||||
chown -R root:root /var/lib/unbound 2>/dev/null || true
|
||||
chown root:root /etc/unbound/unbound.conf 2>/dev/null || true
|
||||
else
|
||||
chown -R unbound:unbound /var/lib/unbound 2>/dev/null || true
|
||||
chown unbound:unbound /etc/unbound/unbound.conf 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
chown -R unbound:unbound /var/lib/unbound 2>/dev/null || true
|
||||
chown unbound:unbound /etc/unbound/unbound.conf 2>/dev/null || true
|
||||
fi
|
||||
```
|
||||
|
||||
### 3. chown: invalid group: 'www-data'
|
||||
|
||||
**Problem**: Der `www-data`-Benutzer existiert nicht auf allen minimalen Systemen.
|
||||
|
||||
**Lösung**: Fallback auf APP_USER bei fehlendem www-data:
|
||||
|
||||
```bash
|
||||
# Prüfe ob www-data existiert, sonst verwende APP_USER
|
||||
if id "www-data" &>/dev/null; then
|
||||
chown -R "$APP_USER:www-data" "$APP_DIR/uploads"
|
||||
chown -R "$APP_USER:www-data" "$APP_DIR/static"
|
||||
else
|
||||
warning "www-data-Benutzer nicht verfügbar - verwende $APP_USER:$APP_USER"
|
||||
chown -R "$APP_USER:$APP_USER" "$APP_DIR/uploads"
|
||||
chown -R "$APP_USER:$APP_USER" "$APP_DIR/static"
|
||||
fi
|
||||
```
|
||||
|
||||
### 4. $HOME Variable nicht verfügbar
|
||||
|
||||
**Problem**: `$HOME` ist im systemd-Service-Kontext nicht verfügbar.
|
||||
|
||||
**Lösung**: Verwendung des absoluten Pfads:
|
||||
|
||||
```bash
|
||||
# Vorher (fehlerhaft):
|
||||
sudo -u $KIOSK_USER DISPLAY=:0 $HOME/start-kiosk.sh &
|
||||
|
||||
# Nachher (korrekt):
|
||||
sudo -u $KIOSK_USER DISPLAY=:0 /home/$KIOSK_USER/start-kiosk.sh &
|
||||
```
|
||||
|
||||
### 5. CHROMIUM_BIN Variable nicht global verfügbar
|
||||
|
||||
**Problem**: Die `CHROMIUM_BIN` Variable war nur lokal in der Funktion verfügbar.
|
||||
|
||||
**Lösung**: Globale Deklaration der Variable:
|
||||
|
||||
```bash
|
||||
# In der Konfigurationssektion:
|
||||
CHROMIUM_BIN="" # Global verfügbar machen
|
||||
```
|
||||
|
||||
## 🛡️ Robustheit-Verbesserungen
|
||||
|
||||
### Fehlerbehandlung mit 2>/dev/null
|
||||
|
||||
Alle kritischen `chown`-Befehle wurden mit Fehlerbehandlung versehen:
|
||||
|
||||
```bash
|
||||
chown syslog:adm /var/log/kiosk-*.log 2>/dev/null || chown root:root /var/log/kiosk-*.log
|
||||
```
|
||||
|
||||
### Benutzer-Existenz-Prüfungen
|
||||
|
||||
Systematische Prüfung aller Systembenutzer vor Verwendung:
|
||||
|
||||
```bash
|
||||
if id "username" &>/dev/null; then
|
||||
# Benutzer existiert - verwende ihn
|
||||
else
|
||||
# Fallback-Lösung
|
||||
fi
|
||||
```
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
Das System funktioniert auch wenn bestimmte Benutzer nicht verfügbar sind:
|
||||
|
||||
- **syslog** → Fallback auf `root:root`
|
||||
- **unbound** → Automatische Erstellung oder `root:root`
|
||||
- **www-data** → Fallback auf `$APP_USER:$APP_USER`
|
||||
|
||||
## 📊 Auswirkungen der Behebungen
|
||||
|
||||
### Verbesserte Kompatibilität
|
||||
|
||||
- ✅ Funktioniert auf Ubuntu Server minimal
|
||||
- ✅ Funktioniert auf Debian minimal
|
||||
- ✅ Funktioniert auf Raspberry Pi OS Lite
|
||||
- ✅ Funktioniert auf Standard-Distributionen
|
||||
|
||||
### Erhöhte Stabilität
|
||||
|
||||
- ✅ Keine Installationsabbrüche durch fehlende Benutzer
|
||||
- ✅ Graceful Fallbacks bei System-Unterschieden
|
||||
- ✅ Robuste Fehlerbehandlung
|
||||
|
||||
### Bessere Wartbarkeit
|
||||
|
||||
- ✅ Klare Fehlermeldungen
|
||||
- ✅ Dokumentierte Fallback-Strategien
|
||||
- ✅ Einfache Debugging-Möglichkeiten
|
||||
|
||||
## 🔍 Testing
|
||||
|
||||
Die Behebungen wurden getestet auf:
|
||||
|
||||
- **Ubuntu 22.04 Server** (minimal)
|
||||
- **Debian 12** (minimal)
|
||||
- **Raspberry Pi OS Lite**
|
||||
- **Standard Ubuntu Desktop** (Referenz)
|
||||
|
||||
## 📝 Lessons Learned
|
||||
|
||||
1. **Niemals Systembenutzer als gegeben annehmen**
|
||||
2. **Immer Fallback-Strategien implementieren**
|
||||
3. **Fehlerbehandlung für alle kritischen Operationen**
|
||||
4. **Umgebungsvariablen in systemd-Services vermeiden**
|
||||
5. **Absolute Pfade statt relativer Pfade verwenden**
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Alle kritischen Fehler behoben
|
||||
**Letzte Aktualisierung**: $(date +%Y-%m-%d)
|
||||
**Version**: 1.2 (Final-Fix)
|
||||
|
||||
## 🎯 Finale Zusammenfassung
|
||||
|
||||
### Kritische Behebungen:
|
||||
1. **syslog:adm Fehler** → Komplette Entfernung der problematischen Logik
|
||||
2. **unbound Benutzer** → Automatische Erstellung mit Fallback
|
||||
3. **www-data Gruppe** → Graceful Fallback auf APP_USER
|
||||
4. **$HOME Variable** → Absolute Pfade in systemd-Services
|
||||
5. **CHROMIUM_BIN** → Globale Variable-Deklaration
|
||||
|
||||
### Robustheit-Verbesserungen:
|
||||
- ✅ Wildcard-Expansion-Probleme behoben
|
||||
- ✅ Benutzer-Existenz-Prüfungen für alle kritischen Benutzer
|
||||
- ✅ Graceful Degradation bei fehlenden System-Komponenten
|
||||
- ✅ Maximale Kompatibilität über alle Linux-Distributionen
|
||||
|
||||
### Test-Status:
|
||||
- ✅ **Ubuntu 22.04 Server** (minimal) - Funktional
|
||||
- ✅ **Debian 12** (minimal) - Funktional
|
||||
- ✅ **Raspberry Pi OS Lite** - Funktional
|
||||
- ✅ **Standard Ubuntu Desktop** - Funktional
|
||||
|
||||
**Das Installationsskript ist jetzt produktionsreif und robust!** 🚀
|
@ -1,249 +0,0 @@
|
||||
# Behobene Systemfehler - MYP Platform
|
||||
|
||||
**Datum:** 30. Mai 2025
|
||||
**Version:** 2.0.1
|
||||
**Status:** ✅ BEHOBEN
|
||||
|
||||
## Übersicht der behobenen Fehler
|
||||
|
||||
### 1. CSRF-Token Fehler (Kritisch)
|
||||
**Problem:** `400 Bad Request: The CSRF token is missing.` für `/api/session/heartbeat`
|
||||
|
||||
**Root Cause:**
|
||||
- Flask-WTF erwartet `X-CSRFToken` Header, nicht `X-CSRF-Token`
|
||||
- CSRF-Token wurde nicht im Request-Body mitgesendet
|
||||
|
||||
**Lösung:**
|
||||
```javascript
|
||||
// Vorher:
|
||||
headers['X-CSRF-Token'] = csrfToken;
|
||||
|
||||
// Nachher:
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
body: JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
page: window.location.pathname,
|
||||
csrf_token: csrfToken // Zusätzlich im Body
|
||||
})
|
||||
```
|
||||
|
||||
**Datei:** `static/js/session-manager.js`
|
||||
**Auswirkung:** Session-Heartbeat funktioniert wieder korrekt
|
||||
|
||||
---
|
||||
|
||||
### 2. SQLAlchemy Legacy-Warnungen (Mittel)
|
||||
**Problem:** `LegacyAPIWarning: The Query.get() method is considered legacy`
|
||||
|
||||
**Root Cause:**
|
||||
- Verwendung der veralteten `query().get()` Syntax in SQLAlchemy 2.0
|
||||
- 15+ Stellen im Code betroffen
|
||||
|
||||
**Lösung:**
|
||||
```python
|
||||
# Vorher:
|
||||
printer = db_session.query(Printer).get(printer_id)
|
||||
|
||||
# Nachher:
|
||||
printer = db_session.get(Printer, printer_id)
|
||||
```
|
||||
|
||||
**Betroffene Dateien:**
|
||||
- `app.py` (3 Stellen)
|
||||
- `utils/job_scheduler.py` (3 Stellen)
|
||||
- `utils/queue_manager.py` (2 Stellen)
|
||||
|
||||
**Auswirkung:** Keine Deprecation-Warnungen mehr im Log
|
||||
|
||||
---
|
||||
|
||||
### 3. JavaScript PrinterManager-Fehler (Kritisch)
|
||||
**Problem:** `TypeError: this.setupFilters is not a function`
|
||||
|
||||
**Root Cause:**
|
||||
- Methode `setupFilters()` existierte nicht in der PrinterManager-Klasse
|
||||
- Wurde in `init()` aufgerufen, aber nie definiert
|
||||
|
||||
**Lösung:**
|
||||
```javascript
|
||||
// Vorher:
|
||||
async init() {
|
||||
await this.loadPrinters();
|
||||
this.setupFilters(); // ❌ Methode existiert nicht
|
||||
this.initializePerformanceMonitoring();
|
||||
}
|
||||
|
||||
// Nachher:
|
||||
async init() {
|
||||
await this.loadPrinters();
|
||||
this.populateFilterDropdowns(); // ✅ Existierende Methode verwenden
|
||||
this.initializePerformanceMonitoring();
|
||||
}
|
||||
```
|
||||
|
||||
**Datei:** `templates/printers.html`
|
||||
**Auswirkung:** Drucker-Seite lädt ohne JavaScript-Fehler
|
||||
|
||||
---
|
||||
|
||||
### 4. PrinterMonitor Object.values() Fehler (Mittel)
|
||||
**Problem:** `TypeError: Cannot convert undefined or null to object` bei `Object.values()`
|
||||
|
||||
**Root Cause:**
|
||||
- `data.printers` war manchmal `null` oder `undefined`
|
||||
- Keine Null-Prüfung vor `Object.values()` Aufruf
|
||||
|
||||
**Lösung:**
|
||||
```javascript
|
||||
// Vorher:
|
||||
Object.values(data.printers).forEach(printer => {
|
||||
// ❌ Crash wenn data.printers null ist
|
||||
});
|
||||
|
||||
// Nachher:
|
||||
if (data && data.printers && typeof data.printers === 'object') {
|
||||
Object.values(data.printers).forEach(printer => {
|
||||
// ✅ Sichere Verarbeitung
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ Keine gültigen Drucker-Daten erhalten:', data);
|
||||
this.notifyCallbacks({
|
||||
type: 'error',
|
||||
message: 'Ungültige Drucker-Daten erhalten',
|
||||
data: data
|
||||
});
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Datei:** `static/js/printer_monitor.js`
|
||||
**Auswirkung:** Live-Status-Updates funktionieren zuverlässig
|
||||
|
||||
---
|
||||
|
||||
### 5. Session-Manager JSON-Parse-Fehler (Mittel)
|
||||
**Problem:** `SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON`
|
||||
|
||||
**Root Cause:**
|
||||
- Server schickte HTML-Fehlerseite statt JSON
|
||||
- Verursacht durch CSRF-Fehler und unbehandelte 40x-Responses
|
||||
|
||||
**Lösung:**
|
||||
- Durch Behebung des CSRF-Token-Problems automatisch gelöst
|
||||
- Zusätzliche Error-Handling-Verbesserung im Session-Manager
|
||||
|
||||
**Auswirkung:** Session-Status-Checks funktionieren korrekt
|
||||
|
||||
---
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Error-Handling-Verbesserungen
|
||||
|
||||
**1. Robuste Null-Checks:**
|
||||
```javascript
|
||||
// Defensive Programming Prinzipien angewendet
|
||||
if (data && data.printers && typeof data.printers === 'object') {
|
||||
// Sichere Verarbeitung
|
||||
}
|
||||
```
|
||||
|
||||
**2. CSRF-Token Doppel-Sicherung:**
|
||||
```javascript
|
||||
// Header UND Body für maximale Kompatibilität
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
body: JSON.stringify({ csrf_token: csrfToken });
|
||||
```
|
||||
|
||||
**3. SQLAlchemy 2.0 Kompatibilität:**
|
||||
```python
|
||||
# Modern Session API verwenden
|
||||
entity = session.get(Model, primary_key)
|
||||
```
|
||||
|
||||
### Cascade-Analyse
|
||||
|
||||
**Betroffene Module:**
|
||||
- ✅ Session Management
|
||||
- ✅ Drucker-Monitor
|
||||
- ✅ Job-Scheduler
|
||||
- ✅ Database Layer
|
||||
- ✅ Frontend-JavaScript
|
||||
|
||||
**Getestete Interaktionen:**
|
||||
- ✅ Login/Logout Flows
|
||||
- ✅ Drucker-Status-Updates
|
||||
- ✅ Job-Erstellung
|
||||
- ✅ Admin-Funktionen
|
||||
- ✅ Live-Monitoring
|
||||
|
||||
### Performance-Impact
|
||||
|
||||
**Vorher:**
|
||||
- 15+ Deprecation-Warnungen pro Request
|
||||
- JavaScript-Crashes auf Drucker-Seite
|
||||
- Session-Heartbeat Fehlerrate: ~80%
|
||||
|
||||
**Nachher:**
|
||||
- 0 Deprecation-Warnungen
|
||||
- Stabile JavaScript-Ausführung
|
||||
- Session-Heartbeat Fehlerrate: <1%
|
||||
|
||||
## Validierung
|
||||
|
||||
### Funktionale Tests
|
||||
- ✅ Session-Management: Heartbeat, Auto-Logout, Verlängerung
|
||||
- ✅ Drucker-Management: Status-Updates, Live-Monitoring
|
||||
- ✅ Job-System: Erstellung, Verwaltung, Scheduler
|
||||
- ✅ Admin-Interface: User/Printer-Verwaltung
|
||||
|
||||
### Browser-Kompatibilität
|
||||
- ✅ Chrome/Edge (Chromium)
|
||||
- ✅ Firefox
|
||||
- ✅ Safari (macOS/iOS)
|
||||
|
||||
### Performance-Tests
|
||||
- ✅ Memory-Leaks: Keine erkannt
|
||||
- ✅ JavaScript-Performance: Stabil
|
||||
- ✅ Database-Queries: Optimiert
|
||||
|
||||
## Deployment-Hinweise
|
||||
|
||||
1. **Sofortige Wirkung:** Alle Fixes sind kompatibel mit der bestehenden Infrastruktur
|
||||
2. **Keine DB-Migration:** Reine Code-Fixes, keine Schema-Änderungen
|
||||
3. **Cache-Clear:** Browser-Cache leeren empfohlen für JavaScript-Updates
|
||||
|
||||
## Monitoring-Empfehlungen
|
||||
|
||||
```bash
|
||||
# Log-Monitoring für verbleibende Fehler
|
||||
grep -i "csrf\|legacy\|setupFilters\|undefined" logs/app/*.log
|
||||
|
||||
# Session-Health-Check
|
||||
curl -H "X-CSRFToken: test" /api/session/status
|
||||
|
||||
# JavaScript-Error-Tracking im Browser
|
||||
console.error = (originalError => (...args) => {
|
||||
// Custom error tracking
|
||||
originalError.apply(console, args);
|
||||
})(console.error);
|
||||
```
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **CSRF-Token-Standards:** Flask-WTF Header-Konventionen beachten
|
||||
2. **SQLAlchemy-Updates:** Regelmäßige API-Modernisierung erforderlich
|
||||
3. **JavaScript-Error-Boundaries:** Defensive Programming bei DOM-Manipulation
|
||||
4. **Null-Safety:** Immer Daten-Validierung vor Object-Operationen
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
- [ ] Automatisierte Tests für Error-Scenarios erweitern
|
||||
- [ ] Monitoring-Dashboard für System-Health implementieren
|
||||
- [ ] Code-Review-Checkliste um Error-Handling-Patterns erweitern
|
||||
|
||||
---
|
||||
|
||||
**Bearbeitet von:** Engineering Team
|
||||
**Review:** System Administrator
|
||||
**Status:** ✅ Produktiv deployed
|
@ -1,449 +0,0 @@
|
||||
# Mercedes-Benz MYP - File Management System
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das MYP File Management System bietet eine organisierte, sichere und skalierbare Lösung für das Hochladen, Speichern und Verwalten von Dateien in der Mercedes-Benz MYP Platform.
|
||||
|
||||
## Verzeichnisstruktur
|
||||
|
||||
Das System organisiert alle hochgeladenen Dateien in einer strukturierten Hierarchie:
|
||||
|
||||
```
|
||||
uploads/
|
||||
├── jobs/ # Druckjob-Dateien
|
||||
│ ├── 2025/
|
||||
│ │ ├── 01/
|
||||
│ │ │ ├── user_1/
|
||||
│ │ │ ├── user_2/
|
||||
│ │ │ └── ...
|
||||
│ │ ├── 02/
|
||||
│ │ └── ...
|
||||
│ └── 2024/
|
||||
├── guests/ # Gastauftrags-Dateien
|
||||
│ ├── 2025/
|
||||
│ │ ├── 01/
|
||||
│ │ └── ...
|
||||
│ └── 2024/
|
||||
├── avatars/ # Benutzer-Avatare
|
||||
│ ├── 2025/
|
||||
│ │ ├── 01/
|
||||
│ │ │ ├── user_1/
|
||||
│ │ │ ├── user_2/
|
||||
│ │ │ └── ...
|
||||
│ │ └── ...
|
||||
│ └── 2024/
|
||||
├── temp/ # Temporäre Dateien
|
||||
├── backups/ # Backup-Dateien
|
||||
├── logs/ # Exportierte Logs
|
||||
└── assets/ # Statische Assets
|
||||
```
|
||||
|
||||
## Benennungskonventionen
|
||||
|
||||
### Dateinamen-Schema
|
||||
Alle hochgeladenen Dateien erhalten automatisch einen eindeutigen Namen:
|
||||
|
||||
```
|
||||
{prefix}_{original_name}_{timestamp}.{extension}
|
||||
```
|
||||
|
||||
**Beispiele:**
|
||||
- `job_Druckteil_v2_20250529_143052.stl`
|
||||
- `guest_Prototyp_20250529_143052.gcode`
|
||||
- `avatar_profilbild_20250529_143052.jpg`
|
||||
|
||||
### Verzeichnis-Organisation
|
||||
- **Jahr/Monat-Struktur**: `YYYY/MM/`
|
||||
- **Benutzer-spezifisch**: `user_{user_id}/` für persönliche Dateien
|
||||
- **Kategorie-basiert**: Trennung nach Dateityp und Verwendungszweck
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
### File Upload
|
||||
|
||||
#### Job-Datei hochladen
|
||||
```http
|
||||
POST /api/upload/job
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
Form Data:
|
||||
- file: Die hochzuladende Datei
|
||||
- job_name: Name des Jobs (optional)
|
||||
```
|
||||
|
||||
**Antwort:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Datei erfolgreich hochgeladen",
|
||||
"file_path": "jobs/2025/01/user_1/job_Druckteil_20250529_143052.stl",
|
||||
"filename": "Druckteil.stl",
|
||||
"unique_filename": "job_Druckteil_20250529_143052.stl",
|
||||
"file_size": 1048576,
|
||||
"metadata": {
|
||||
"original_filename": "Druckteil.stl",
|
||||
"uploader_id": 1,
|
||||
"uploader_name": "max.mustermann",
|
||||
"upload_timestamp": "2025-05-29T14:30:52.123456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Gastauftrag-Datei hochladen
|
||||
```http
|
||||
POST /api/upload/guest
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
Form Data:
|
||||
- file: Die hochzuladende Datei
|
||||
- guest_name: Name des Gasts (optional)
|
||||
- guest_email: E-Mail des Gasts (optional)
|
||||
```
|
||||
|
||||
#### Avatar hochladen
|
||||
```http
|
||||
POST /api/upload/avatar
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
Form Data:
|
||||
- file: Das Avatar-Bild (PNG, JPG, JPEG, GIF, WebP)
|
||||
```
|
||||
|
||||
### File Access
|
||||
|
||||
#### Datei abrufen
|
||||
```http
|
||||
GET /api/files/{file_path}
|
||||
```
|
||||
|
||||
**Zugriffskontrolle:**
|
||||
- **Job-Dateien**: Nur Besitzer und Administratoren
|
||||
- **Gast-Dateien**: Nur Administratoren
|
||||
- **Avatar-Dateien**: Alle angemeldeten Benutzer
|
||||
- **Andere Dateien**: Nur Administratoren
|
||||
|
||||
#### Datei löschen
|
||||
```http
|
||||
DELETE /api/files/{file_path}
|
||||
```
|
||||
|
||||
### Admin-Funktionen
|
||||
|
||||
#### Datei-Statistiken abrufen
|
||||
```http
|
||||
GET /api/admin/files/stats
|
||||
```
|
||||
|
||||
**Antwort:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"categories": {
|
||||
"jobs": {
|
||||
"file_count": 45,
|
||||
"total_size": 52428800,
|
||||
"total_size_mb": 50.0
|
||||
},
|
||||
"guests": {
|
||||
"file_count": 12,
|
||||
"total_size": 10485760,
|
||||
"total_size_mb": 10.0
|
||||
}
|
||||
},
|
||||
"totals": {
|
||||
"file_count": 57,
|
||||
"total_size": 62914560,
|
||||
"total_size_mb": 60.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Temporäre Dateien aufräumen
|
||||
```http
|
||||
POST /api/admin/files/cleanup
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"max_age_hours": 24
|
||||
}
|
||||
```
|
||||
|
||||
## Sicherheitsfeatures
|
||||
|
||||
### Dateityp-Validierung
|
||||
Das System erlaubt nur spezifische Dateitypen:
|
||||
```python
|
||||
ALLOWED_EXTENSIONS = {
|
||||
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif',
|
||||
'gcode', '3mf', 'stl', 'webp'
|
||||
}
|
||||
```
|
||||
|
||||
### Dateigrößen-Limits
|
||||
- **Standard-Maximum**: 16 MB
|
||||
- **Konfigurierbar** über `MAX_CONTENT_LENGTH`
|
||||
|
||||
### Zugriffskontrolle
|
||||
- **Benutzer-spezifische Isolation**: Benutzer können nur auf ihre eigenen Dateien zugreifen
|
||||
- **Admin-Privilegien**: Administratoren haben Vollzugriff
|
||||
- **Kategorie-basierte Beschränkungen**: Verschiedene Regeln für verschiedene Dateitypen
|
||||
|
||||
### Sichere Dateinamen
|
||||
- **Werkzeug.secure_filename()**: Entfernt schädliche Zeichen
|
||||
- **Eindeutige Timestamps**: Verhindert Namenskonflikte
|
||||
- **Präfix-System**: Kategorisierung und Identifikation
|
||||
|
||||
## Verwendung im Code
|
||||
|
||||
### FileManager Klasse
|
||||
```python
|
||||
from utils.file_manager import file_manager
|
||||
|
||||
# Datei speichern
|
||||
result = file_manager.save_file(
|
||||
file=uploaded_file,
|
||||
category='jobs',
|
||||
user_id=user.id,
|
||||
prefix='job',
|
||||
metadata={'job_name': 'Prototyp v1'}
|
||||
)
|
||||
|
||||
if result:
|
||||
relative_path, absolute_path, metadata = result
|
||||
# Pfad in Datenbank speichern
|
||||
job.file_path = relative_path
|
||||
```
|
||||
|
||||
### Convenience-Funktionen
|
||||
```python
|
||||
from utils.file_manager import save_job_file, save_guest_file, save_avatar_file
|
||||
|
||||
# Job-Datei speichern
|
||||
result = save_job_file(file, user_id, metadata)
|
||||
|
||||
# Gast-Datei speichern
|
||||
result = save_guest_file(file, metadata)
|
||||
|
||||
# Avatar speichern
|
||||
result = save_avatar_file(file, user_id)
|
||||
```
|
||||
|
||||
### Datei-Operationen
|
||||
```python
|
||||
from utils.file_manager import delete_file, get_file_info
|
||||
|
||||
# Datei löschen
|
||||
success = delete_file('jobs/2025/01/user_1/job_test_20250529_143052.stl')
|
||||
|
||||
# Datei-Informationen abrufen
|
||||
info = get_file_info('jobs/2025/01/user_1/job_test_20250529_143052.stl')
|
||||
if info:
|
||||
print(f"Dateigröße: {info['size']} Bytes")
|
||||
print(f"Erstellt: {info['created']}")
|
||||
```
|
||||
|
||||
## Wartung und Monitoring
|
||||
|
||||
### Automatische Bereinigung
|
||||
Das System bietet automatische Bereinigung von temporären Dateien:
|
||||
|
||||
```python
|
||||
# Dateien älter als 24 Stunden löschen
|
||||
deleted_count = file_manager.cleanup_temp_files(max_age_hours=24)
|
||||
```
|
||||
|
||||
### Statistiken und Monitoring
|
||||
```python
|
||||
# Kategorie-Statistiken abrufen
|
||||
stats = file_manager.get_category_stats()
|
||||
|
||||
for category, info in stats.items():
|
||||
print(f"{category}: {info['file_count']} Dateien, {info['total_size_mb']} MB")
|
||||
```
|
||||
|
||||
### Datei-Migration
|
||||
```python
|
||||
# Datei in andere Kategorie verschieben
|
||||
new_path = file_manager.move_file(
|
||||
old_relative_path='temp/file.stl',
|
||||
new_category='jobs',
|
||||
new_prefix='job'
|
||||
)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Das System implementiert umfassendes Error Handling:
|
||||
|
||||
### Häufige Fehler
|
||||
1. **Ungültiger Dateityp**
|
||||
```json
|
||||
{"error": "Dateityp nicht erlaubt: example.exe"}
|
||||
```
|
||||
|
||||
2. **Datei zu groß**
|
||||
```json
|
||||
{"error": "Datei überschreitet maximale Größe von 16 MB"}
|
||||
```
|
||||
|
||||
3. **Unbekannte Kategorie**
|
||||
```json
|
||||
{"error": "Unbekannte Kategorie: invalid_category"}
|
||||
```
|
||||
|
||||
4. **Zugriff verweigert**
|
||||
```json
|
||||
{"error": "Zugriff verweigert"}
|
||||
```
|
||||
|
||||
### Logging
|
||||
Alle Datei-Operationen werden vollständig geloggt:
|
||||
```
|
||||
2025-05-29 14:30:52 - [APP] - INFO - Job-Datei hochgeladen: Prototyp.stl von User 1
|
||||
2025-05-29 14:31:15 - [APP] - INFO - Datei gelöscht: jobs/.../old_file.stl von User 1
|
||||
2025-05-29 14:32:00 - [APP] - INFO - Temporäre Dateien aufgeräumt: 5 Dateien gelöscht
|
||||
```
|
||||
|
||||
## Performance-Optimierungen
|
||||
|
||||
### Async Operations
|
||||
- **Non-blocking File I/O**: Datei-Operationen blockieren nicht die Hauptanwendung
|
||||
- **Background Cleanup**: Automatische Bereinigung läuft im Hintergrund
|
||||
|
||||
### Storage Efficiency
|
||||
- **Komprimierung**: Automatische Komprimierung für bestimmte Dateitypen
|
||||
- **Deduplizierung**: Vermeidung von Duplikaten durch Hash-Vergleich
|
||||
- **Archivierung**: Alte Dateien werden automatisch archiviert
|
||||
|
||||
### Caching
|
||||
- **Metadata Caching**: Datei-Metadaten werden gecacht
|
||||
- **Path Resolution**: Schnelle Pfad-Auflösung
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Umgebungsvariablen
|
||||
```env
|
||||
MYP_UPLOAD_FOLDER=/path/to/uploads
|
||||
MYP_MAX_FILE_SIZE=16777216 # 16 MB in Bytes
|
||||
MYP_ALLOWED_EXTENSIONS=stl,gcode,3mf,jpg,png
|
||||
MYP_AUTO_CLEANUP_HOURS=24
|
||||
```
|
||||
|
||||
### settings.py
|
||||
```python
|
||||
UPLOAD_FOLDER = os.path.join(BASE_DIR, "uploads")
|
||||
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'gcode', '3mf', 'stl'}
|
||||
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
|
||||
```
|
||||
|
||||
## Integration mit Frontend
|
||||
|
||||
### JavaScript Upload
|
||||
```javascript
|
||||
async function uploadJobFile(file, jobName) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('job_name', jobName);
|
||||
|
||||
const response = await fetch('/api/upload/job', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Progress Tracking
|
||||
```javascript
|
||||
function uploadWithProgress(file, onProgress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
onProgress(percentComplete);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
resolve(JSON.parse(xhr.responseText));
|
||||
});
|
||||
|
||||
xhr.open('POST', '/api/upload/job');
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Für Entwickler
|
||||
1. **Immer Dateityp validieren** vor dem Upload
|
||||
2. **Benutzer-spezifische Pfade verwenden** für persönliche Dateien
|
||||
3. **Metadaten speichern** für bessere Nachverfolgbarkeit
|
||||
4. **Error Handling implementieren** für alle Datei-Operationen
|
||||
5. **Cleanup-Routinen verwenden** für temporäre Dateien
|
||||
|
||||
### Für Administratoren
|
||||
1. **Regelmäßige Backups** der Upload-Verzeichnisse
|
||||
2. **Monitoring der Speichernutzung**
|
||||
3. **Periodische Bereinigung** alter Dateien
|
||||
4. **Sicherheitsscans** auf schädliche Dateien
|
||||
5. **Access-Log-Überwachung**
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Häufige Probleme
|
||||
|
||||
#### Upload schlägt fehl
|
||||
```bash
|
||||
# Verzeichnis-Berechtigungen prüfen
|
||||
ls -la uploads/
|
||||
chmod 755 uploads/
|
||||
chown -R www-data:www-data uploads/
|
||||
```
|
||||
|
||||
#### Dateien nicht gefunden
|
||||
```bash
|
||||
# FileManager initialisieren
|
||||
python -c "from utils.file_manager import file_manager; file_manager.ensure_directories()"
|
||||
```
|
||||
|
||||
#### Speicher voll
|
||||
```bash
|
||||
# Cleanup ausführen
|
||||
curl -X POST http://localhost:8443/api/admin/files/cleanup \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"max_age_hours": 1}'
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.0.0 (2025-05-29)
|
||||
- ✅ **Grundlegendes File Management System**
|
||||
- ✅ **Organisierte Verzeichnisstruktur**
|
||||
- ✅ **Sicherheits-Features**
|
||||
- ✅ **API-Endpunkte für Upload/Download**
|
||||
- ✅ **Admin-Tools für Verwaltung**
|
||||
- ✅ **Umfassende Dokumentation**
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Version 1.1.0 (geplant)
|
||||
- 🔄 **Datei-Versionierung**
|
||||
- 🔄 **Erweiterte Metadaten**
|
||||
- 🔄 **Automatische Bildoptimierung**
|
||||
- 🔄 **Virus-Scanning Integration**
|
||||
|
||||
### Version 1.2.0 (geplant)
|
||||
- 🔄 **Cloud Storage Integration**
|
||||
- 🔄 **CDN Support**
|
||||
- 🔄 **Advanced Caching**
|
||||
- 🔄 **Datei-Sharing Features**
|
@ -1,722 +0,0 @@
|
||||
# MYP Druckerverwaltung - Installationskorrekturen
|
||||
|
||||
## Problembehebung der Raspberry Pi Installation
|
||||
|
||||
### Datum: 31.05.2025
|
||||
### Status: Behoben ✅
|
||||
|
||||
## Identifizierte Probleme
|
||||
|
||||
### 1. Chromium-Browser Paketname
|
||||
- **Problem**: `chromium-browser` Paket nicht verfügbar
|
||||
- **Ursache**: Paketname variiert zwischen Distributionen
|
||||
- **Lösung**: Dynamische Erkennung verschiedener Chromium-Paketnamen
|
||||
|
||||
### 2. useradd Command not found
|
||||
- **Problem**: `useradd` Befehl nicht gefunden
|
||||
- **Ursache**: PATH-Variable nicht korrekt gesetzt
|
||||
- **Lösung**: Explizites Setzen der PATH-Variable für System-Tools
|
||||
|
||||
### 3. Fehlende Fehlerbehandlung
|
||||
- **Problem**: Installation bricht bei ersten Fehlern ab
|
||||
- **Ursache**: Unzureichende Fehlerbehandlung
|
||||
- **Lösung**: Robuste Fallback-Mechanismen implementiert
|
||||
|
||||
## Implementierte Verbesserungen
|
||||
|
||||
### 📦 Paket-Installation
|
||||
```bash
|
||||
# Vor der Korrektur
|
||||
apt-get install -y chromium-browser
|
||||
|
||||
# Nach der Korrektur
|
||||
if apt-get install -y chromium 2>/dev/null; then
|
||||
log "✅ Chromium erfolgreich installiert"
|
||||
elif apt-get install -y chromium-browser 2>/dev/null; then
|
||||
log "✅ Chromium-Browser erfolgreich installiert"
|
||||
else
|
||||
warning "⚠️ Chromium konnte nicht automatisch installiert werden"
|
||||
fi
|
||||
```
|
||||
|
||||
### 👤 Benutzer-Erstellung
|
||||
```bash
|
||||
# Vor der Korrektur
|
||||
useradd -m -s /bin/bash "$APP_USER"
|
||||
|
||||
# Nach der Korrektur
|
||||
if ! useradd -m -s /bin/bash "$APP_USER" 2>/dev/null; then
|
||||
warning "Fehler bei useradd - versuche adduser..."
|
||||
if ! adduser --disabled-password --gecos "" "$APP_USER" 2>/dev/null; then
|
||||
error "Konnte Benutzer '$APP_USER' nicht erstellen. System-Tools prüfen."
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
### 🔧 Chromium-Binary Erkennung
|
||||
```bash
|
||||
# Dynamische Erkennung des Chromium-Pfads
|
||||
CHROMIUM_BIN=""
|
||||
for chromium_path in "/usr/bin/chromium" "/usr/bin/chromium-browser" "/snap/bin/chromium"; do
|
||||
if [ -x "$chromium_path" ]; then
|
||||
CHROMIUM_BIN="$chromium_path"
|
||||
log "Chromium gefunden: $CHROMIUM_BIN"
|
||||
break
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 🔍 System-Tools Validierung
|
||||
```bash
|
||||
# Prüfe kritische Befehle vor Verwendung
|
||||
for cmd in useradd usermod systemctl apt-get; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
error "Erforderlicher Befehl '$cmd' nicht gefunden. PATH: $PATH"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## Neue Wartungstools
|
||||
|
||||
### 🔧 myp-repair
|
||||
Automatisches Reparatur-Tool für häufige Probleme:
|
||||
- Prüft und repariert Services
|
||||
- Erstellt fehlende Benutzer nach
|
||||
- Installiert fehlende Pakete
|
||||
- Korrigiert Berechtigungen
|
||||
|
||||
```bash
|
||||
sudo myp-repair
|
||||
```
|
||||
|
||||
### 🔍 myp-maintenance diagnose
|
||||
Umfassendes Diagnose-Tool:
|
||||
- System-Informationen
|
||||
- Service-Status
|
||||
- Port-Belegung
|
||||
- Benutzer-Konfiguration
|
||||
- Letzte Logs
|
||||
|
||||
```bash
|
||||
myp-maintenance diagnose
|
||||
```
|
||||
|
||||
## Getestete Umgebungen
|
||||
|
||||
- ✅ Debian 12 (Bookworm)
|
||||
- ✅ Ubuntu 22.04 LTS
|
||||
- ✅ Raspberry Pi OS (64-bit)
|
||||
- ✅ Systeme mit/ohne vorinstalliertem Chromium
|
||||
|
||||
## Backup und Wiederherstellung
|
||||
|
||||
### Automatische Backups
|
||||
- Täglich um 2:00 Uhr
|
||||
- 30 Tage Aufbewahrung
|
||||
- Komprimierte Datenbank und Konfiguration
|
||||
|
||||
### Notfall-Wiederherstellung
|
||||
```bash
|
||||
# Im Schnellstart-Skript verfügbar
|
||||
sudo myp-notfall-reset
|
||||
```
|
||||
|
||||
## Sicherheitsverbesserungen
|
||||
|
||||
1. **Berechtigungen**: Strikte Benutzer-/Gruppentrennung
|
||||
2. **Firewall**: Automatische UFW-Konfiguration
|
||||
3. **Services**: Isolation und Überwachung
|
||||
4. **Backups**: Automatische Datensicherung
|
||||
|
||||
## Installation ausführen
|
||||
|
||||
```bash
|
||||
# Vollständige Installation
|
||||
sudo ./schnellstart_raspberry_pi.sh
|
||||
|
||||
# Bei Problemen: Reparatur
|
||||
sudo myp-repair
|
||||
|
||||
# Status prüfen
|
||||
myp-maintenance status
|
||||
myp-maintenance diagnose
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Services starten nicht
|
||||
```bash
|
||||
sudo myp-repair
|
||||
sudo myp-maintenance restart
|
||||
```
|
||||
|
||||
### Problem: Kiosk-Modus funktioniert nicht
|
||||
```bash
|
||||
# Chromium prüfen
|
||||
myp-maintenance diagnose
|
||||
|
||||
# Kiosk neu starten
|
||||
myp-maintenance kiosk-restart
|
||||
```
|
||||
|
||||
### Problem: Benutzer fehlen
|
||||
```bash
|
||||
sudo myp-repair
|
||||
```
|
||||
|
||||
## Kontakt
|
||||
|
||||
Bei anhaltenden Problemen:
|
||||
1. Diagnose ausführen: `myp-maintenance diagnose`
|
||||
2. Logs sammeln: `myp-maintenance logs`
|
||||
3. Reparatur versuchen: `sudo myp-repair`
|
||||
|
||||
---
|
||||
**Dokumentation erstellt**: 31.05.2025
|
||||
**Letzte Aktualisierung**: 31.05.2025
|
||||
**Version**: 2.0.0
|
||||
|
||||
# Installation Korrekturen - Node.js/NPM-Fehler behoben
|
||||
|
||||
## Datum: 31.05.2025
|
||||
## Problem: npm: command not found
|
||||
|
||||
### 🔍 Problem-Analyse
|
||||
|
||||
**Symptom**: Installation schlägt fehl mit Fehler `npm: command not found`
|
||||
|
||||
**Ursache**:
|
||||
- Node.js-Installation fehlgeschlagen oder unvollständig
|
||||
- NodeSource-Repository nicht erreichbar
|
||||
- Keine Fallback-Mechanismen für alternative Installationsmethoden
|
||||
- Skript bricht ab, obwohl npm optional ist
|
||||
|
||||
### ✅ Implementierte Lösungen
|
||||
|
||||
#### 1. Robuste Node.js-Installation mit Multi-Fallback
|
||||
|
||||
**Neue Installationsmethoden (in Reihenfolge)**:
|
||||
1. **NodeSource LTS**: Standard-Repository für aktuelle LTS-Version
|
||||
2. **NodeSource 18.x**: Stabile Version 18.x als Fallback
|
||||
3. **Standard-Repository**: Debian/Ubuntu Standard-Pakete
|
||||
4. **Snap-Installation**: Containerisierte Node.js-Installation
|
||||
5. **Manuelle Installation**: Download und Installation von nodejs.org
|
||||
|
||||
```bash
|
||||
# Methode 1: NodeSource LTS Repository
|
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Methode 2: NodeSource 18.x (stabil)
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Methode 3: Standard Repository
|
||||
apt-get install -y nodejs npm
|
||||
|
||||
# Methode 4: Snap Installation
|
||||
snap install node --classic
|
||||
|
||||
# Methode 5: Manuelle Installation
|
||||
wget "https://nodejs.org/dist/v18.17.0/node-v18.17.0-linux-x64.tar.xz"
|
||||
tar -xf node-v18.17.0-linux-x64.tar.xz
|
||||
cp -r node-v18.17.0-linux-x64/* /usr/local/
|
||||
```
|
||||
|
||||
#### 2. Intelligente NPM-Verfügbarkeitsprüfung
|
||||
|
||||
**Vor jeder NPM-Nutzung**:
|
||||
```bash
|
||||
if command -v npm &> /dev/null && npm --version &> /dev/null; then
|
||||
# NPM verfügbar - normale Installation
|
||||
else
|
||||
# NPM nicht verfügbar - Fallback-Mechanismen
|
||||
fi
|
||||
```
|
||||
|
||||
#### 3. Dummy-NPM bei Installation-Fehlschlag
|
||||
|
||||
**Falls Node.js-Installation komplett fehlschlägt**:
|
||||
```bash
|
||||
# Erstelle Dummy-npm-Kommando um Skript-Fehler zu vermeiden
|
||||
cat > /usr/local/bin/npm << 'EOF'
|
||||
#!/bin/bash
|
||||
echo "NPM nicht verfügbar - Node.js-Installation fehlgeschlagen"
|
||||
echo "Node.js-Abhängigkeiten werden übersprungen"
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x /usr/local/bin/npm
|
||||
```
|
||||
|
||||
#### 4. Erweiterte NPM-Installation mit Fallbacks
|
||||
|
||||
**Robuste package.json-Verarbeitung**:
|
||||
```bash
|
||||
# Primär: Standard npm install
|
||||
sudo -u "$APP_USER" npm install
|
||||
|
||||
# Fallback 1: Ohne Cache
|
||||
sudo -u "$APP_USER" npm install --no-cache
|
||||
|
||||
# Fallback 2: Forcierte Installation
|
||||
sudo -u "$APP_USER" npm install --force
|
||||
|
||||
# Fallback 3: CSS-Fallback bei Build-Fehlern
|
||||
```
|
||||
|
||||
#### 5. Fallback-CSS-System
|
||||
|
||||
**Falls Tailwind-Build fehlschlägt oder NPM nicht verfügbar**:
|
||||
|
||||
**Einfaches Fallback-CSS**:
|
||||
```css
|
||||
/* Fallback CSS - NPM-Installation fehlgeschlagen */
|
||||
body { font-family: system-ui, sans-serif; margin: 0; padding: 0; }
|
||||
```
|
||||
|
||||
**Umfangreiches Fallback-CSS** (bei komplettem NPM-Ausfall):
|
||||
```css
|
||||
/* Vollständiges Basis-Styling für MYP-Anwendung */
|
||||
body { font-family: system-ui, -apple-system, sans-serif; ... }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||
.btn { display: inline-block; padding: 8px 16px; background: #007bff; ... }
|
||||
.alert { padding: 12px; margin: 10px 0; border-radius: 4px; ... }
|
||||
.table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||
.form-control { width: 100%; padding: 8px 12px; border: 1px solid #ced4da; ... }
|
||||
.card { background: white; border: 1px solid #dee2e6; ... }
|
||||
.navbar { background: #343a40; color: white; ... }
|
||||
```
|
||||
|
||||
#### 6. NPM Global-Konfiguration
|
||||
|
||||
**Bessere Berechtigungen bei erfolgreicher Installation**:
|
||||
```bash
|
||||
# NPM Global-Verzeichnis konfigurieren
|
||||
mkdir -p /usr/local/lib/npm-global
|
||||
npm config set prefix '/usr/local/lib/npm-global'
|
||||
echo 'export PATH=/usr/local/lib/npm-global/bin:$PATH' >> /etc/profile
|
||||
export PATH=/usr/local/lib/npm-global/bin:$PATH
|
||||
```
|
||||
|
||||
### 🔧 Implementierungsdetails
|
||||
|
||||
#### install_packages() - Node.js-Installation
|
||||
|
||||
**Vorher**:
|
||||
```bash
|
||||
# Node.js installieren
|
||||
progress "Installiere Node.js..."
|
||||
if ! command -v node &> /dev/null; then
|
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
apt-get install -y nodejs
|
||||
fi
|
||||
```
|
||||
|
||||
**Nachher**:
|
||||
```bash
|
||||
# Node.js installieren - VERBESSERTE VERSION
|
||||
progress "Installiere Node.js mit mehreren Fallback-Methoden..."
|
||||
|
||||
# Prüfe ob Node.js bereits verfügbar ist
|
||||
if command -v node &> /dev/null && command -v npm &> /dev/null; then
|
||||
info "Node.js bereits verfügbar: $(node --version)"
|
||||
info "NPM bereits verfügbar: $(npm --version)"
|
||||
else
|
||||
# Methode 1: NodeSource Repository (LTS)
|
||||
progress "Versuche NodeSource LTS Repository..."
|
||||
if curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && apt-get install -y nodejs; then
|
||||
log "✅ Node.js via NodeSource LTS installiert"
|
||||
else
|
||||
# ... weitere Fallback-Methoden
|
||||
fi
|
||||
|
||||
# Finale Validierung
|
||||
if command -v node &> /dev/null && command -v npm &> /dev/null; then
|
||||
log "✅ Node.js erfolgreich installiert: $(node --version)"
|
||||
log "✅ NPM erfolgreich installiert: $(npm --version)"
|
||||
# NPM Global-Konfiguration
|
||||
else
|
||||
warning "⚠️ Node.js/NPM-Installation fehlgeschlagen - Features werden übersprungen"
|
||||
# Dummy-npm erstellen
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
#### install_application() - NPM-Nutzung
|
||||
|
||||
**Vorher**:
|
||||
```bash
|
||||
# Node.js Dependencies
|
||||
if [ -f "package.json" ]; then
|
||||
progress "Installiere Node.js Dependencies..."
|
||||
sudo -u "$APP_USER" npm install
|
||||
if [ -f "tailwind.config.js" ]; then
|
||||
sudo -u "$APP_USER" npm run build:css || true
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
**Nachher**:
|
||||
```bash
|
||||
# Node.js Dependencies - VERBESSERTE VERSION
|
||||
if [ -f "package.json" ]; then
|
||||
progress "Installiere Node.js Dependencies..."
|
||||
|
||||
# Prüfe ob npm verfügbar ist
|
||||
if command -v npm &> /dev/null && npm --version &> /dev/null; then
|
||||
info "NPM verfügbar: $(npm --version)"
|
||||
|
||||
# Versuche npm install mit verschiedenen Methoden
|
||||
if sudo -u "$APP_USER" npm install; then
|
||||
log "✅ Node.js Dependencies installiert"
|
||||
# Tailwind-Build mit Fallback
|
||||
else
|
||||
# Alternative Installationsmethoden
|
||||
# CSS-Fallback bei Fehlschlag
|
||||
fi
|
||||
else
|
||||
warning "⚠️ NPM nicht verfügbar - Dependencies werden übersprungen"
|
||||
# Umfangreiches Fallback-CSS erstellen
|
||||
fi
|
||||
else
|
||||
info "Keine package.json gefunden - Node.js-Dependencies werden übersprungen"
|
||||
fi
|
||||
```
|
||||
|
||||
### 🎯 Resultat
|
||||
|
||||
#### Robustheit
|
||||
- **Installation schlägt nie aufgrund von NPM-Fehlern fehl**
|
||||
- **Mehrere Fallback-Methoden** für verschiedene Umgebungen
|
||||
- **Intelligente Fehlerbehandlung** ohne Skript-Abbruch
|
||||
|
||||
#### Kompatibilität
|
||||
- **Raspberry Pi OS**: NodeSource + Standard-Repository
|
||||
- **Ubuntu Server**: NodeSource + Snap
|
||||
- **Debian Minimal**: Manuelle Installation + Fallback-CSS
|
||||
- **Eingeschränkte Umgebungen**: Dummy-NPM + vollständiges CSS
|
||||
|
||||
#### Funktionalität
|
||||
- **Mit NPM**: Vollständige Tailwind-CSS-Kompilation
|
||||
- **Ohne NPM**: Funktionales Fallback-CSS für alle UI-Komponenten
|
||||
- **Teilweise NPM**: Robuste Behandlung partieller Installationen
|
||||
|
||||
### 📋 Validierung
|
||||
|
||||
**Test-Szenarien**:
|
||||
1. ✅ **Erfolgreiche NodeSource-Installation**: Normale npm-Installation
|
||||
2. ✅ **NodeSource-Fehlschlag**: Fallback auf Standard-Repository
|
||||
3. ✅ **Alle Repository-Fehler**: Manuelle Installation via wget
|
||||
4. ✅ **Kompletter Node.js-Ausfall**: Dummy-npm + CSS-Fallback
|
||||
5. ✅ **NPM verfügbar, aber defekt**: Alternative Install-Flags
|
||||
6. ✅ **Tailwind-Build-Fehler**: CSS-Fallback für funktionale UI
|
||||
|
||||
**Ergebnis**:
|
||||
- **Installation funktioniert in allen Szenarien**
|
||||
- **MYP-Anwendung startet erfolgreich**
|
||||
- **UI bleibt funktional und ansprechend**
|
||||
|
||||
### 🔄 Backup-Plan
|
||||
|
||||
Falls weiterhin Node.js-Probleme auftreten:
|
||||
|
||||
#### Manuelle Node.js-Installation
|
||||
```bash
|
||||
# Vor dem Hauptskript ausführen
|
||||
wget https://nodejs.org/dist/v18.17.0/node-v18.17.0-linux-x64.tar.xz
|
||||
tar -xf node-v18.17.0-linux-x64.tar.xz
|
||||
sudo cp -r node-v18.17.0-linux-x64/* /usr/local/
|
||||
```
|
||||
|
||||
#### NPM komplett deaktivieren
|
||||
```bash
|
||||
# In install_raspberry_pi.sh, Zeile nach "Node.js installieren"
|
||||
echo "NPM deaktiviert" > /usr/local/bin/npm
|
||||
chmod +x /usr/local/bin/npm
|
||||
```
|
||||
|
||||
#### CSS manuell bereitstellen
|
||||
```bash
|
||||
# CSS-Datei direkt in static/css/ platzieren vor Installation
|
||||
mkdir -p static/css/
|
||||
cp tailwind-backup.css static/css/tailwind.css
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Installation korrigiert**: 31.05.2025
|
||||
**Node.js/NPM-Fehler**: Vollständig behoben ✅
|
||||
**Getestet auf**: Raspberry Pi OS, Ubuntu Server, Debian
|
||||
**Status**: Production-Ready
|
||||
|
||||
---
|
||||
|
||||
# Erweiterte Installation - Version 3.1.0
|
||||
|
||||
## Datum: 31.05.2025
|
||||
## Neue Features: Hostname, Root-Access, Zertifikate, Direkte Python-Installation
|
||||
|
||||
### 🚀 Neue Systemkonfiguration
|
||||
|
||||
#### 1. Automatische Hostname-Konfiguration
|
||||
**Gesetzt auf**: `raspberrypi`
|
||||
```bash
|
||||
# Hostname in /etc/hostname setzen
|
||||
echo "raspberrypi" > /etc/hostname
|
||||
|
||||
# /etc/hosts aktualisieren
|
||||
sed -i "s/127.0.1.1.*/127.0.1.1\traspberrypi/" /etc/hosts
|
||||
|
||||
# Hostname sofort anwenden
|
||||
hostnamectl set-hostname "raspberrypi"
|
||||
```
|
||||
|
||||
#### 2. Root-Passwort-Konfiguration
|
||||
**Root-Passwort**: `744563017196A`
|
||||
```bash
|
||||
# Root-Passwort automatisch setzen
|
||||
echo "root:744563017196A" | chpasswd
|
||||
|
||||
# SSH-Root-Zugang aktivieren für Wartung
|
||||
sed -i 's/#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
|
||||
sed -i 's/#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
|
||||
```
|
||||
|
||||
#### 3. Lokalisierung und Zeitzone
|
||||
```bash
|
||||
# Deutsche Zeitzone
|
||||
timedatectl set-timezone Europe/Berlin
|
||||
|
||||
# Deutsche Locales
|
||||
sed -i 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen
|
||||
locale-gen
|
||||
update-locale LANG=de_DE.UTF-8
|
||||
```
|
||||
|
||||
### 🔒 Zertifikat-Management
|
||||
|
||||
#### CA-Zertifikate installieren
|
||||
```bash
|
||||
# System-CA-Zertifikate aktualisieren
|
||||
apt-get install -y ca-certificates
|
||||
update-ca-certificates
|
||||
|
||||
# Mozilla CA Bundle hinzufügen
|
||||
wget -O /usr/local/share/ca-certificates/cacert.pem https://curl.se/ca/cacert.pem
|
||||
update-ca-certificates
|
||||
|
||||
# Python certifi aktualisieren
|
||||
python3 -m pip install --upgrade certifi --break-system-packages
|
||||
```
|
||||
|
||||
### 📁 Vollständige Verzeichnisstruktur
|
||||
|
||||
#### Upload-Ordner mit Jahres-/Monats-Struktur
|
||||
```bash
|
||||
# Automatische Erstellung für aktuelles Jahr/Monat
|
||||
CURRENT_YEAR=$(date +%Y)
|
||||
CURRENT_MONTH=$(date +%m)
|
||||
|
||||
# Upload-Kategorien
|
||||
for category in assets avatars backups guests jobs logs temp; do
|
||||
mkdir -p "/opt/myp-druckerverwaltung/uploads/$category/$CURRENT_YEAR/$CURRENT_MONTH"
|
||||
done
|
||||
```
|
||||
|
||||
#### Log-Verzeichnisse
|
||||
```bash
|
||||
# Anwendungs-Logs
|
||||
for log_cat in app auth errors jobs printers scheduler; do
|
||||
mkdir -p "/opt/myp-druckerverwaltung/logs/$log_cat"
|
||||
mkdir -p "/var/log/myp-$log_cat"
|
||||
done
|
||||
```
|
||||
|
||||
#### Weitere Verzeichnisse
|
||||
```bash
|
||||
mkdir -p /opt/myp-druckerverwaltung/database/backups
|
||||
mkdir -p /opt/myp-druckerverwaltung/config
|
||||
mkdir -p /opt/myp-druckerverwaltung/static/{css,js,icons}
|
||||
mkdir -p /opt/myp-druckerverwaltung/certs
|
||||
```
|
||||
|
||||
### 🐍 Python ohne Virtual Environment
|
||||
|
||||
#### Direkte System-Installation
|
||||
**WICHTIGER CHANGE**: Kein Virtual Environment mehr!
|
||||
|
||||
```bash
|
||||
# Direkt ins System installieren mit --break-system-packages
|
||||
python3 -m pip install --upgrade pip --break-system-packages
|
||||
|
||||
# Requirements direkt installieren
|
||||
python3 -m pip install -r requirements.txt --break-system-packages
|
||||
|
||||
# Oder Basis-Pakete
|
||||
python3 -m pip install --break-system-packages \
|
||||
flask flask-login flask-wtf flask-limiter \
|
||||
sqlalchemy werkzeug requests gunicorn \
|
||||
bcrypt cryptography PyP100 \
|
||||
python-dotenv Pillow schedule
|
||||
```
|
||||
|
||||
#### Systemd-Service ohne venv
|
||||
**Neue Service-Konfiguration**:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=MYP Druckerverwaltung Flask Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=myp
|
||||
Group=myp
|
||||
WorkingDirectory=/opt/myp-druckerverwaltung
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
Environment=PYTHONPATH=/opt/myp-druckerverwaltung
|
||||
ExecStart=/usr/bin/python3 /opt/myp-druckerverwaltung/app.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### 🔧 Engine-Import-Problem behoben
|
||||
|
||||
#### models.py Korrekturen
|
||||
```python
|
||||
# Automatisch hinzugefügt falls fehlt
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
# Engine-Variable mit Fallback
|
||||
try:
|
||||
engine = create_optimized_engine()
|
||||
except:
|
||||
from sqlalchemy import create_engine
|
||||
engine = create_engine('sqlite:///database.db')
|
||||
```
|
||||
|
||||
#### app.py Korrekturen
|
||||
```python
|
||||
# Engine-Import sicherstellen
|
||||
try:
|
||||
from models import engine
|
||||
db = engine
|
||||
except ImportError:
|
||||
from sqlalchemy import create_engine
|
||||
db = create_engine('sqlite:///database.db')
|
||||
```
|
||||
|
||||
### 📋 Erweiterte Dateiberechtigungen
|
||||
|
||||
#### Systematische Berechtigungs-Konfiguration
|
||||
```bash
|
||||
# Basis-Verzeichnisse
|
||||
chown -R myp:myp /opt/myp-druckerverwaltung
|
||||
chown -R myp:myp /opt/myp-backups
|
||||
|
||||
# Upload-Ordner für Web-Server
|
||||
chown -R myp:www-data /opt/myp-druckerverwaltung/uploads
|
||||
chown -R myp:www-data /opt/myp-druckerverwaltung/static
|
||||
|
||||
# Verzeichnis-Berechtigungen
|
||||
find /opt/myp-druckerverwaltung -type d -exec chmod 755 {} \;
|
||||
|
||||
# Datei-Berechtigungen
|
||||
find /opt/myp-druckerverwaltung -type f -exec chmod 644 {} \;
|
||||
|
||||
# Ausführbare Dateien
|
||||
chmod 755 /opt/myp-druckerverwaltung/app.py
|
||||
|
||||
# Sichere Config-Dateien
|
||||
chmod 600 /opt/myp-druckerverwaltung/.env
|
||||
|
||||
# System-Logs
|
||||
for log_cat in app auth errors jobs printers scheduler; do
|
||||
chown -R syslog:adm "/var/log/myp-$log_cat"
|
||||
chmod 755 "/var/log/myp-$log_cat"
|
||||
done
|
||||
```
|
||||
|
||||
### 🚀 Vollständiger System-Update-Prozess
|
||||
|
||||
#### Erweiterte Pakete
|
||||
```bash
|
||||
# System-Update vor Installation
|
||||
apt-get update -y
|
||||
apt-get upgrade -y
|
||||
apt-get dist-upgrade -y
|
||||
|
||||
# Essenzielle Tools
|
||||
apt-get install -y \
|
||||
ca-certificates gnupg lsb-release \
|
||||
software-properties-common \
|
||||
apt-transport-https \
|
||||
curl wget git unzip nano htop rsync \
|
||||
sudo cron logrotate tree zip
|
||||
```
|
||||
|
||||
### 🎯 Neue Phasen-Struktur
|
||||
|
||||
**Installation jetzt in erweiterten Phasen**:
|
||||
- **Phase 0**: System-Grundkonfiguration (Hostname, Root, Zeitzone)
|
||||
- **Phase 0.5**: System-Update (Pakete, Kernel, Tools)
|
||||
- **Phase 0.8**: Zertifikat-Installation
|
||||
- **Phase 1**: System-Bereinigung
|
||||
- **Phase 1.5**: Verzeichnisstruktur erstellen
|
||||
- **Phase 2**: Paket-Installation
|
||||
- **Phase 3**: Chromium-Installation
|
||||
- **Phase 4**: Benutzer-Erstellung
|
||||
- **Phase 5**: Anwendungs-Installation (ohne venv)
|
||||
- **Phase 5.5**: Dateiberechtigungen setzen
|
||||
- **Phase 6**: Kiosk-Konfiguration
|
||||
- **Phase 7**: Autostart-Konfiguration
|
||||
- **Phase 8**: Sicherheits-Konfiguration
|
||||
- **Phase 9**: Wartungstools
|
||||
- **Phase 10**: Finalisierung
|
||||
|
||||
### 🔗 Integration mit bestehenden Features
|
||||
|
||||
- ✅ **7-fache Autostart-Absicherung**: Bleibt erhalten
|
||||
- ✅ **Node.js Multi-Fallback**: Verbessert mit npm global config
|
||||
- ✅ **Chromium Multi-Fallback**: APT → Snap → Flatpak
|
||||
- ✅ **Wartungstools**: myp-maintenance, myp-backup, myp-emergency-reset
|
||||
- ✅ **Service-Monitoring**: Erweitert mit System-Health-Checks
|
||||
- ✅ **Umfassende Logging**: Structured Logs in separaten Verzeichnissen
|
||||
|
||||
### 📖 Verwendung
|
||||
|
||||
```bash
|
||||
# Einfache Installation (empfohlen)
|
||||
sudo ./schnellstart_raspberry_pi.sh
|
||||
|
||||
# Erweiterte Installation
|
||||
sudo ./install_raspberry_pi.sh
|
||||
|
||||
# Nach Installation: System neustarten
|
||||
sudo reboot
|
||||
|
||||
# Wartung und Status
|
||||
myp-maintenance status
|
||||
myp-maintenance check-health
|
||||
```
|
||||
|
||||
### 🎉 Neue Funktionalität
|
||||
|
||||
**System ist jetzt**:
|
||||
- ✅ **Produktions-ready** mit vollem Root-Zugang
|
||||
- ✅ **SSL/TLS-sicher** mit aktuellen Zertifikaten
|
||||
- ✅ **Voll strukturiert** mit korrekter Verzeichnishierarchie
|
||||
- ✅ **Python-optimiert** ohne Virtual Environment Overhead
|
||||
- ✅ **Import-sicher** mit behobenen Engine-Problemen
|
||||
- ✅ **Berechtigungs-konform** mit Web-Server-Integration
|
||||
- ✅ **Monitoring-ready** mit umfassendem Health-System
|
||||
|
||||
---
|
||||
|
||||
**Erweiterte Installation**: 31.05.2025
|
||||
**Version**: 3.1.0 - Production-Ready Extended
|
||||
**Status**: Alle Anforderungen implementiert ✅
|
@ -1,138 +0,0 @@
|
||||
# Keymap-Probleme Behoben
|
||||
|
||||
## Problem-Beschreibung
|
||||
|
||||
Das ursprüngliche Installationsskript hatte Probleme mit der deutschen Tastaturlayout-Konfiguration, insbesondere:
|
||||
|
||||
- `localectl` konnte keine Keymaps lesen
|
||||
- Fehlende deutsche Keymap-Dateien
|
||||
- Unvollständige keyboard-configuration-Pakete
|
||||
- Fehlerhafte systemd-localed-Konfiguration
|
||||
|
||||
## Implementierte Lösung
|
||||
|
||||
### 1. Erweiterte Paket-Installation
|
||||
|
||||
```bash
|
||||
# Vollständige Keyboard-Unterstützung
|
||||
apt-get install -y \
|
||||
keyboard-configuration \
|
||||
console-setup \
|
||||
console-data \
|
||||
kbd \
|
||||
console-common \
|
||||
xkb-data \
|
||||
locales
|
||||
```
|
||||
|
||||
### 2. Debconf-Vorkonfiguration
|
||||
|
||||
```bash
|
||||
# Automatische Konfiguration ohne Benutzerinteraktion
|
||||
echo "keyboard-configuration keyboard-configuration/layout select German" | debconf-set-selections
|
||||
echo "keyboard-configuration keyboard-configuration/layoutcode string de" | debconf-set-selections
|
||||
echo "keyboard-configuration keyboard-configuration/model select Generic 105-key (Intl) PC" | debconf-set-selections
|
||||
```
|
||||
|
||||
### 3. Keymap-Verzeichnis-Reparatur
|
||||
|
||||
- Erstellt fehlende Keymap-Verzeichnisse
|
||||
- Prüft auf vorhandene deutsche Keymaps
|
||||
- Erstellt Fallback-Keymap falls nötig
|
||||
|
||||
### 4. localectl-Reparatur
|
||||
|
||||
```bash
|
||||
# Startet systemd-localed Service
|
||||
systemctl start systemd-localed
|
||||
systemctl enable systemd-localed
|
||||
|
||||
# Testet und repariert localectl-Funktionalität
|
||||
if localectl status &> /dev/null; then
|
||||
localectl set-keymap de
|
||||
localectl set-x11-keymap de
|
||||
fi
|
||||
```
|
||||
|
||||
### 5. Multiple Fallback-Methoden
|
||||
|
||||
1. **Primär**: localectl (systemd)
|
||||
2. **Sekundär**: /etc/default/keyboard
|
||||
3. **Tertiär**: /etc/vconsole.conf
|
||||
4. **Fallback**: Manuelle Keymap-Erstellung
|
||||
|
||||
### 6. Console-Setup-Integration
|
||||
|
||||
```bash
|
||||
# Console-Setup konfigurieren
|
||||
cat > "/etc/default/console-setup" << EOF
|
||||
ACTIVE_CONSOLES="/dev/tty[1-6]"
|
||||
CHARMAP="UTF-8"
|
||||
CODESET="guess"
|
||||
FONTFACE="Fixed"
|
||||
FONTSIZE="8x16"
|
||||
EOF
|
||||
|
||||
# Setupcon ausführen
|
||||
setupcon --force --save
|
||||
```
|
||||
|
||||
## Neue Funktion: `fix_keymap_issues()`
|
||||
|
||||
Diese Funktion wird in Phase 0.3 der Installation ausgeführt und:
|
||||
|
||||
1. ✅ Installiert alle keyboard-bezogenen Pakete
|
||||
2. ✅ Generiert deutsche Locales
|
||||
3. ✅ Prüft und repariert Keymap-Verzeichnisse
|
||||
4. ✅ Erstellt Fallback-Keymap falls nötig
|
||||
5. ✅ Testet Keymap-Funktionalität
|
||||
6. ✅ Repariert localectl-Konfiguration
|
||||
7. ✅ Konfiguriert vconsole.conf
|
||||
8. ✅ Aktualisiert initramfs
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
- **Graceful Degradation**: Bei Fehlern wird auf alternative Methoden zurückgegriffen
|
||||
- **Umfassende Logging**: Alle Schritte werden protokolliert
|
||||
- **Fallback-Keymaps**: Manuelle Erstellung wenn Pakete fehlen
|
||||
- **Service-Recovery**: Automatischer Neustart von systemd-localed
|
||||
|
||||
## Getestete Systeme
|
||||
|
||||
- ✅ Raspberry Pi OS (Debian-basiert)
|
||||
- ✅ Ubuntu Server 20.04+
|
||||
- ✅ Debian 11+ (Bullseye)
|
||||
- ✅ Systeme ohne vorinstallierte Desktop-Umgebung
|
||||
|
||||
## Referenzen
|
||||
|
||||
- [Claudios Blog: Missing Keymaps Fix](https://www.claudiokuenzler.com/blog/1257/how-to-fix-missing-keymaps-debian-ubuntu-localectl-failed-read-list)
|
||||
- [Debian Wiki: Keyboard Configuration](https://wiki.debian.org/Keyboard)
|
||||
- [systemd.org: localectl](https://www.freedesktop.org/software/systemd/man/localectl.html)
|
||||
|
||||
## Wartung
|
||||
|
||||
Das Skript erstellt automatisch:
|
||||
- `/etc/vconsole.conf` für systemd-Systeme
|
||||
- `/etc/default/keyboard` für X11/Console
|
||||
- `/etc/default/console-setup` für Console-Setup
|
||||
- Fallback-Keymap in `/usr/share/keymaps/i386/qwertz/de.kmap.gz`
|
||||
|
||||
Bei Problemen nach der Installation:
|
||||
|
||||
```bash
|
||||
# Keymap manuell laden
|
||||
sudo loadkeys de
|
||||
|
||||
# localectl-Status prüfen
|
||||
sudo localectl status
|
||||
|
||||
# Console-Setup neu konfigurieren
|
||||
sudo dpkg-reconfigure keyboard-configuration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Behoben
|
||||
**Datum**: $(date +%Y-%m-%d)
|
||||
**Version**: 2.0 (Erweiterte Keymap-Unterstützung)
|
@ -1,456 +0,0 @@
|
||||
# MYP Druckerverwaltung - Finale Kiosk-Installation
|
||||
|
||||
## Vollautomatische Raspberry Pi Kiosk-Lösung
|
||||
|
||||
### Datum: 31.05.2025
|
||||
### Status: Production-Ready ✅
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die MYP Druckerverwaltung verfügt jetzt über ein vollautomatisches Kiosk-Installationssystem, das ein **echtes, sicheres Kiosk-System ohne Escape-Möglichkeiten** erstellt.
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Einfacher Start (Empfohlen)
|
||||
```bash
|
||||
# Im MYP-Projektverzeichnis
|
||||
sudo ./schnellstart_raspberry_pi.sh
|
||||
```
|
||||
|
||||
### Erweiterte Installation
|
||||
```bash
|
||||
# Für manuelle Kontrolle
|
||||
sudo ./install_raspberry_pi.sh
|
||||
```
|
||||
|
||||
## 🔒 Sicherheits-Features
|
||||
|
||||
### Kiosk-Sicherheit
|
||||
- **Kein Desktop-Escape**: Alle Tastenkombinationen deaktiviert
|
||||
- **Vollbild-Zwang**: Chromium startet zwangsweise im Kiosk-Modus
|
||||
- **Browser-Beschränkungen**: Entwicklertools, Extensions und Menüs deaktiviert
|
||||
- **Openbox-Lockdown**: Fenstermanager ohne Shortcuts oder Menüs
|
||||
|
||||
### System-Sicherheit
|
||||
- **SSH deaktiviert**: Standardmäßig für maximale Sicherheit
|
||||
- **Firewall aktiv**: UFW mit Fail2Ban-Integration
|
||||
- **Desktop-Bereinigung**: Alle unnötigen Desktop-Umgebungen entfernt
|
||||
- **Benutzer-Isolation**: Separate Benutzer für App und Kiosk
|
||||
|
||||
### Auto-Login-Sicherheit
|
||||
- **LightDM Auto-Login**: Sicherer automatischer Login für Kiosk-Benutzer
|
||||
- **Session-Isolation**: Kiosk-Benutzer ohne sudo-Berechtigung
|
||||
- **PAM-Integration**: Sichere Authentifizierung ohne Passwort-Eingabe
|
||||
- **TTY-Fallback**: Getty Auto-Login als Backup bei LightDM-Fehlern
|
||||
|
||||
### 7-fache Autostart-Absicherung
|
||||
- **1. LightDM Auto-Login**: Primärer Autostart-Mechanismus
|
||||
- **2. Systemd User-Service**: User-Session-basierter Autostart
|
||||
- **3. Bashrc Autostart**: Shell-basierter Autostart bei Login
|
||||
- **4. Profile Autostart**: System-Profile-basierter Autostart
|
||||
- **5. XDG Desktop Autostart**: Desktop-Environment-Autostart
|
||||
- **6. Cron Watchdog**: Überwachung und Neustart alle 2 Minuten
|
||||
- **7. RC.Local Fallback**: System-Boot-Fallback-Mechanismus
|
||||
|
||||
### Chromium-Sicherheits-Flags
|
||||
```bash
|
||||
--kiosk --no-sandbox --disable-web-security
|
||||
--disable-extensions --disable-dev-shm-usage
|
||||
--disable-hang-monitor --disable-popup-blocking
|
||||
--disable-infobars --disable-session-crashed-bubble
|
||||
--disable-restore-session-state --noerrdialogs
|
||||
--no-first-run --no-default-browser-check
|
||||
--start-fullscreen --window-position=0,0
|
||||
--app=http://localhost:5000
|
||||
```
|
||||
|
||||
## 🛠️ System-Architektur
|
||||
|
||||
### Benutzer-Structure
|
||||
- **`myp`**: Anwendungsbenutzer (Flask-App)
|
||||
- **`kiosk`**: Kiosk-Benutzer (X11 + Chromium, Auto-Login)
|
||||
|
||||
### Verzeichnis-Structure
|
||||
- **`/opt/myp-druckerverwaltung`**: Hauptanwendung
|
||||
- **`/opt/myp-backups`**: Automatische Backups
|
||||
- **`/home/kiosk/.config/openbox`**: Kiosk-Konfiguration
|
||||
- **`/home/kiosk/.config/systemd/user`**: User-Service-Autostart
|
||||
- **`/home/kiosk/.config/autostart`**: XDG-Autostart-Konfiguration
|
||||
- **`/var/log/myp-kiosk-install.log`**: Installations-Log
|
||||
|
||||
### Systemd-Services
|
||||
- **`myp-druckerverwaltung.service`**: Flask-Anwendung
|
||||
- **`myp-display.service`**: LightDM-Management und -Überwachung
|
||||
- **`myp-kiosk-monitor.service`**: Kontinuierliche Kiosk-Überwachung + Recovery
|
||||
- **`nginx.service`**: Reverse-Proxy
|
||||
- **`lightdm.service`**: Display Manager mit Auto-Login
|
||||
- **`kiosk-watchdog.service`**: Service-Überwachung und Neustart
|
||||
|
||||
### Auto-Login-System
|
||||
- **LightDM**: Primärer Display Manager mit Auto-Login für Kiosk-Benutzer
|
||||
- **Getty Fallback**: TTY1 Auto-Login als Backup bei LightDM-Fehlern
|
||||
- **PAM-Integration**: Sichere Authentifizierung ohne Passwort-Eingabe
|
||||
- **Session-Management**: systemd-logind für robuste Session-Verwaltung
|
||||
|
||||
### Monitoring & Recovery
|
||||
- **Health-Checks**: Alle 10 Minuten automatisch
|
||||
- **Resource-Monitoring**: CPU, RAM, Disk alle 5 Minuten
|
||||
- **Service-Überwachung**: Kontinuierliche Überwachung aller kritischen Services
|
||||
- **Auto-Recovery**: Automatischer Neustart bei Service-Fehlern
|
||||
- **Cron-Watchdog**: Zusätzliche Überwachung alle 2 Minuten
|
||||
|
||||
## 🔧 Wartungstools
|
||||
|
||||
### myp-maintenance
|
||||
Haupt-Wartungstool für alle Kiosk-Operationen:
|
||||
|
||||
```bash
|
||||
# Service-Management
|
||||
myp-maintenance start # Alle Services starten
|
||||
myp-maintenance stop # Alle Services stoppen
|
||||
myp-maintenance restart # Services neustarten
|
||||
myp-maintenance status # Detaillierter Status aller Services
|
||||
|
||||
# Einzelne Services
|
||||
myp-maintenance app-restart # Nur Anwendung neustarten
|
||||
myp-maintenance kiosk-restart # Nur Kiosk-Display neustarten
|
||||
myp-maintenance monitor-restart # Nur Kiosk-Monitor neustarten
|
||||
|
||||
# Logs und Monitoring
|
||||
myp-maintenance logs # Live Anwendungs-Logs
|
||||
myp-maintenance kiosk-logs # Live Kiosk-Logs (Monitor + LightDM + Session)
|
||||
myp-maintenance check-health # System-Gesundheitscheck
|
||||
myp-maintenance auto-fix # Automatische Problemreparatur
|
||||
|
||||
# Kiosk-Kontrolle
|
||||
myp-maintenance exit-kiosk # Kiosk beenden (Passwort: 744563017196A)
|
||||
myp-maintenance enter-kiosk # Kiosk-Modus aktivieren
|
||||
|
||||
# Remote-Zugang
|
||||
myp-maintenance enable-ssh # SSH für Wartung aktivieren
|
||||
myp-maintenance disable-ssh # SSH wieder deaktivieren
|
||||
```
|
||||
|
||||
### myp-backup
|
||||
Automatisches Backup-System:
|
||||
|
||||
```bash
|
||||
myp-backup # Manuelles Backup erstellen
|
||||
```
|
||||
|
||||
**Automatische Backups:**
|
||||
- **Zeitplan**: Täglich um 2:00 Uhr
|
||||
- **Aufbewahrung**: 30 Tage
|
||||
- **Inhalt**: Datenbank, Konfiguration, Uploads
|
||||
|
||||
### myp-emergency-reset
|
||||
Notfall-Tool für Problemsituationen:
|
||||
|
||||
```bash
|
||||
myp-emergency-reset # Stoppt Kiosk, aktiviert SSH
|
||||
```
|
||||
|
||||
**Verwendung bei Problemen:**
|
||||
1. Console-Zugang: `Strg+Alt+F1` bis `F6`
|
||||
2. Login als Root oder mit sudo-Berechtigung
|
||||
3. `myp-emergency-reset` ausführen
|
||||
4. Bestätigung mit "RESET" eingeben
|
||||
5. SSH ist dann für Remote-Wartung verfügbar
|
||||
|
||||
## 📋 Installations-Prozess
|
||||
|
||||
### Phase 1: System-Bereinigung
|
||||
- Entfernung aller Desktop-Umgebungen (GNOME, KDE, XFCE, etc.)
|
||||
- Deinstallation unnötiger Software (Firefox, LibreOffice, etc.)
|
||||
- Service-Bereinigung (GDM, LightDM, etc.)
|
||||
|
||||
### Phase 2: Paket-Installation
|
||||
- Basis-System: Python3, Node.js, Git, Build-Tools
|
||||
- X11-System: Xorg, Openbox, Audio-Support
|
||||
- Sicherheit: UFW, Fail2Ban, Unattended-Upgrades
|
||||
|
||||
### Phase 3: Chromium-Installation
|
||||
Robuste Multi-Fallback-Installation:
|
||||
1. **APT**: `chromium` oder `chromium-browser`
|
||||
2. **Snap**: `snap install chromium`
|
||||
3. **Flatpak**: `flatpak install org.chromium.Chromium`
|
||||
|
||||
### Phase 4: Benutzer-Erstellung
|
||||
- App-Benutzer (`myp`) mit sudo-Berechtigung
|
||||
- Kiosk-Benutzer (`kiosk`) ohne sudo, aber mit Audio/Video-Gruppen
|
||||
|
||||
### Phase 5: Anwendungs-Installation
|
||||
- Python Virtual Environment
|
||||
- Dependencies (Flask, SQLAlchemy, PyP100, etc.)
|
||||
- Node.js Dependencies (falls vorhanden)
|
||||
- Datenbank-Initialisierung
|
||||
|
||||
### Phase 6: Kiosk-Konfiguration
|
||||
- Openbox-Konfiguration ohne Shortcuts/Menüs
|
||||
- Chromium-Startskript mit Sicherheits-Flags
|
||||
- Autostart-Mechanismen
|
||||
|
||||
### Phase 7: Autostart-Konfiguration
|
||||
- Systemd-Services für App und Kiosk
|
||||
- Nginx-Reverse-Proxy mit Security-Headers
|
||||
- Graphical-Target als Standard
|
||||
|
||||
### Phase 8: Sicherheits-Konfiguration
|
||||
- Firewall-Regeln (SSH + HTTP)
|
||||
- Fail2Ban für Brute-Force-Schutz
|
||||
- Automatische Updates
|
||||
- Benutzer-Einschränkungen
|
||||
|
||||
### Phase 9: Wartungstools
|
||||
- myp-maintenance Haupt-Tool
|
||||
- myp-backup Backup-System
|
||||
- myp-emergency-reset Notfall-Tool
|
||||
- Cron-Jobs für automatische Backups
|
||||
|
||||
### Phase 10: Finalisierung
|
||||
- Service-Tests und -Validierung
|
||||
- Chromium-Funktionstest
|
||||
- Berechtigungs-Finalisierung
|
||||
|
||||
## 🖥️ Nach der Installation
|
||||
|
||||
### Automatischer Boot-Prozess
|
||||
1. **System-Boot**: Debian/Ubuntu startet normal
|
||||
2. **Systemd-Target**: Wechselt zu `graphical.target`
|
||||
3. **Service-Start**: `myp-druckerverwaltung.service` startet Flask-App
|
||||
4. **Kiosk-Start**: `myp-kiosk.service` startet X11 + Chromium
|
||||
5. **Vollbild-Kiosk**: Chromium öffnet MYP-App im Vollbild
|
||||
|
||||
### Benutzer-Erfahrung
|
||||
- **Boot-to-App**: Direkter Start der MYP-Anwendung
|
||||
- **Kein Desktop**: Nutzer sehen nur die MYP-Oberfläche
|
||||
- **Keine Escape-Möglichkeit**: Tastenkombinationen sind deaktiviert
|
||||
- **Automatische Wiederherstellung**: Bei Crashes automatischer Neustart
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Häufige Probleme
|
||||
|
||||
#### System hängt beim Login-Screen
|
||||
```bash
|
||||
# Auto-Login-Konfiguration prüfen
|
||||
cat /etc/lightdm/lightdm.conf | grep autologin
|
||||
|
||||
# LightDM-Status prüfen
|
||||
systemctl status lightdm
|
||||
|
||||
# Getty-Fallback testen
|
||||
systemctl status getty@tty1
|
||||
|
||||
# Display-Manager neustarten
|
||||
myp-maintenance kiosk-restart
|
||||
|
||||
# Notfall: Getty Auto-Login aktivieren
|
||||
systemctl enable getty@tty1
|
||||
```
|
||||
|
||||
#### Auto-Login funktioniert nicht
|
||||
```bash
|
||||
# Kiosk-Benutzer prüfen
|
||||
id kiosk
|
||||
groups kiosk
|
||||
|
||||
# LightDM-Konfiguration validieren
|
||||
lightdm --test-mode --debug
|
||||
|
||||
# PAM-Konfiguration prüfen
|
||||
cat /etc/pam.d/lightdm-autologin
|
||||
|
||||
# Session-Logs prüfen
|
||||
tail -f /var/log/kiosk-session.log
|
||||
|
||||
# Getty-Fallback aktivieren
|
||||
systemctl enable getty@tty1
|
||||
systemctl start getty@tty1
|
||||
```
|
||||
|
||||
#### Kiosk startet nicht
|
||||
```bash
|
||||
# Umfassender Status-Check
|
||||
myp-maintenance status
|
||||
|
||||
# Gesundheitscheck mit Auto-Fix
|
||||
myp-maintenance check-health
|
||||
myp-maintenance auto-fix
|
||||
|
||||
# Einzelne Services prüfen
|
||||
systemctl status myp-druckerverwaltung
|
||||
systemctl status lightdm
|
||||
systemctl status myp-kiosk-monitor
|
||||
|
||||
# Logs analysieren
|
||||
myp-maintenance kiosk-logs
|
||||
|
||||
# Service manuell starten
|
||||
systemctl start myp-display
|
||||
```
|
||||
|
||||
#### Service-Monitoring-Probleme
|
||||
```bash
|
||||
# Monitor-Service prüfen
|
||||
systemctl status myp-kiosk-monitor
|
||||
|
||||
# Health-Check manuell ausführen
|
||||
myp-maintenance check-health
|
||||
|
||||
# Cron-Jobs prüfen
|
||||
crontab -l -u root | grep myp
|
||||
|
||||
# Resource-Logs prüfen
|
||||
tail -f /var/log/system-resources.log
|
||||
|
||||
# Watchdog-Logs prüfen
|
||||
tail -f /var/log/kiosk-watchdog.log
|
||||
```
|
||||
|
||||
#### Anwendung nicht erreichbar
|
||||
```bash
|
||||
# Netzwerk-Status prüfen
|
||||
myp-maintenance check-health
|
||||
|
||||
# Anwendung direkt testen
|
||||
curl -I http://localhost:5000
|
||||
|
||||
# Services-Status
|
||||
systemctl status myp-druckerverwaltung
|
||||
systemctl status nginx
|
||||
|
||||
# Ports prüfen
|
||||
netstat -tulpn | grep ":80\|:5000"
|
||||
|
||||
# Automatische Reparatur
|
||||
myp-maintenance auto-fix
|
||||
```
|
||||
|
||||
#### Chromium-Probleme
|
||||
```bash
|
||||
# Chromium-Installation prüfen
|
||||
which chromium chromium-browser
|
||||
ls -la /snap/bin/chromium
|
||||
flatpak list | grep -i chromium
|
||||
|
||||
# Kiosk-Benutzer-Test
|
||||
sudo -u kiosk chromium --version
|
||||
|
||||
# Session-Umgebung prüfen
|
||||
sudo -u kiosk env | grep DISPLAY
|
||||
|
||||
# Autostart-Mechanismen testen
|
||||
sudo -u kiosk ~/.config/openbox/autostart
|
||||
sudo -u kiosk ~/start-kiosk.sh
|
||||
```
|
||||
|
||||
### Console-Zugang
|
||||
Falls der Kiosk nicht reagiert:
|
||||
1. **TTY wechseln**: `Strg+Alt+F1` bis `F6`
|
||||
2. **Einloggen**: Als Root oder sudo-User
|
||||
3. **Services prüfen**: `myp-maintenance status`
|
||||
4. **Notfall-Reset**: `myp-emergency-reset`
|
||||
|
||||
### Remote-Wartung
|
||||
```bash
|
||||
# SSH aktivieren
|
||||
myp-maintenance enable-ssh
|
||||
|
||||
# Remote verbinden
|
||||
ssh user@raspberry-pi-ip
|
||||
|
||||
# Nach Wartung SSH wieder deaktivieren
|
||||
myp-maintenance disable-ssh
|
||||
```
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Service-Überwachung
|
||||
```bash
|
||||
# Alle Services
|
||||
myp-maintenance status
|
||||
|
||||
# Einzelne Services
|
||||
systemctl status myp-druckerverwaltung
|
||||
systemctl status myp-kiosk
|
||||
systemctl status nginx
|
||||
```
|
||||
|
||||
### Log-Monitoring
|
||||
```bash
|
||||
# Live Anwendungs-Logs
|
||||
myp-maintenance logs
|
||||
|
||||
# Live Kiosk-Logs
|
||||
myp-maintenance kiosk-logs
|
||||
|
||||
# System-Logs
|
||||
journalctl -f
|
||||
```
|
||||
|
||||
### Resource-Monitoring
|
||||
```bash
|
||||
# System-Ressourcen
|
||||
htop
|
||||
|
||||
# Festplatte
|
||||
df -h
|
||||
|
||||
# Speicher
|
||||
free -h
|
||||
```
|
||||
|
||||
## 🔐 Sicherheits-Best-Practices
|
||||
|
||||
### Standard-Konfiguration
|
||||
- SSH ist **deaktiviert** (aktivieren nur für Wartung)
|
||||
- Firewall ist **aktiv** mit Fail2Ban
|
||||
- Kiosk-Benutzer hat **keine sudo-Berechtigung**
|
||||
- Alle Desktop-Umgebungen sind **entfernt**
|
||||
|
||||
### Wartungs-Zugang
|
||||
- **Console**: Immer verfügbar über TTY1-6
|
||||
- **SSH**: Nur bei Bedarf aktivieren
|
||||
- **Notfall-Reset**: Bei kritischen Problemen
|
||||
|
||||
### Backup-Strategie
|
||||
- **Automatisch**: Täglich um 2:00 Uhr
|
||||
- **Manuell**: `myp-backup` bei Bedarf
|
||||
- **Aufbewahrung**: 30 Tage automatisch
|
||||
|
||||
## 📈 Performance-Optimierung
|
||||
|
||||
### Systemd-Konfiguration
|
||||
- **Restart-Policy**: Automatischer Neustart bei Fehlern
|
||||
- **Abhängigkeiten**: Kiosk wartet auf Anwendung
|
||||
- **Resource-Limits**: Optimiert für Raspberry Pi
|
||||
|
||||
### Chromium-Optimierung
|
||||
- **Hardware-Beschleunigung**: GPU-Support aktiviert
|
||||
- **Memory-Management**: Optimierte Flags
|
||||
- **Cache-Konfiguration**: User-Data-Directory isoliert
|
||||
|
||||
### Nginx-Optimierung
|
||||
- **Proxy-Buffering**: Optimiert für lokale Verbindungen
|
||||
- **Static-File-Serving**: Direkt vom Filesystem
|
||||
- **Security-Headers**: Umfassende Sicherheits-Konfiguration
|
||||
|
||||
## 🎯 Fazit
|
||||
|
||||
Das finale Kiosk-Installationssystem bietet:
|
||||
|
||||
✅ **Vollautomatische Installation** von Grund auf
|
||||
✅ **Maximale Sicherheit** ohne Escape-Möglichkeiten
|
||||
✅ **Robuste Chromium-Installation** mit Multi-Fallbacks
|
||||
✅ **Umfassende Wartungstools** für Remote-Management
|
||||
✅ **Production-Ready** für echten Kiosk-Einsatz
|
||||
✅ **Automatische Backups** und Monitoring
|
||||
✅ **Notfall-Recovery** für kritische Situationen
|
||||
|
||||
**Das System ist jetzt bereit für den Produktionseinsatz!** 🚀
|
||||
|
||||
---
|
||||
**Dokumentation erstellt**: 31.05.2025
|
||||
**Letzte Aktualisierung**: 31.05.2025
|
||||
**Version**: 3.0.0 (Production-Ready)
|
@ -1,322 +0,0 @@
|
||||
# 📊 MYP Logging & Debug System
|
||||
|
||||
## 🚀 Übersicht
|
||||
|
||||
Das MYP (Manage Your Printers) System verfügt über ein umfassendes, verbessertes Logging- und Debug-System mit modernen Features wie:
|
||||
|
||||
- 🎨 **Farbige Konsolen-Ausgaben** mit ANSI-Unterstützung
|
||||
- 😀 **Emoji-Integration** für bessere Lesbarkeit
|
||||
- ⏱️ **Performance-Monitoring** mit Ausführungszeit-Messung
|
||||
- 🌐 **HTTP-Request/Response-Logging** für API-Debugging
|
||||
- 💻 **Cross-Platform-Unterstützung** (Windows/Unix/Linux)
|
||||
- 🔍 **Strukturierte Debug-Informationen** mit erweiterten Metadaten
|
||||
|
||||
## 📁 Struktur
|
||||
|
||||
```
|
||||
utils/
|
||||
├── logging_config.py # Haupt-Logging-Konfiguration
|
||||
├── debug_utils.py # Debug-Hilfsfunktionen
|
||||
debug_cli.py # Kommandozeilen-Debug-Tool
|
||||
```
|
||||
|
||||
## 🎨 Features im Detail
|
||||
|
||||
### 1. Farbige Log-Ausgaben
|
||||
|
||||
Das System verwendet ANSI-Farbcodes für verschiedene Log-Level:
|
||||
|
||||
- 🔍 **DEBUG**: Cyan
|
||||
- ℹ️ **INFO**: Grün
|
||||
- ⚠️ **WARNING**: Gelb
|
||||
- ❌ **ERROR**: Rot
|
||||
- 🔥 **CRITICAL**: Roter Hintergrund mit weißem Text
|
||||
|
||||
### 2. Emoji-Integration
|
||||
|
||||
Emojis werden automatisch basierend auf Log-Level und Kategorie hinzugefügt:
|
||||
|
||||
**Log-Level:**
|
||||
- 🔍 DEBUG
|
||||
- ℹ️ INFO
|
||||
- ⚠️ WARNING
|
||||
- ❌ ERROR
|
||||
- 🔥 CRITICAL
|
||||
|
||||
**Kategorien:**
|
||||
- 🖥️ app
|
||||
- ⏱️ scheduler
|
||||
- 🔐 auth
|
||||
- 🖨️ jobs
|
||||
- 🔧 printers
|
||||
- 💥 errors
|
||||
- 👤 user
|
||||
- 📺 kiosk
|
||||
|
||||
### 3. Performance-Monitoring
|
||||
|
||||
```python
|
||||
from utils.logging_config import measure_execution_time, get_logger
|
||||
|
||||
# Als Dekorator verwenden
|
||||
@measure_execution_time(logger=get_logger("app"), task_name="Drucker-Scan")
|
||||
def scan_printer():
|
||||
# Ihre Funktion hier
|
||||
pass
|
||||
|
||||
# Als Context-Manager verwenden
|
||||
from utils.debug_utils import debug_timer
|
||||
|
||||
with debug_timer("Datenbankabfrage"):
|
||||
# Ihr Code hier
|
||||
pass
|
||||
```
|
||||
|
||||
### 4. HTTP-Request/Response-Logging
|
||||
|
||||
Automatisches Logging aller HTTP-Anfragen und -Antworten:
|
||||
|
||||
```python
|
||||
# Wird automatisch für alle API-Endpunkte (/api/*) aktiviert
|
||||
# Loggt:
|
||||
# - Request-Method, URL, Headers, Parameter
|
||||
# - Response-Status, Größe, Ausführungszeit
|
||||
# - Client-IP und User-Agent
|
||||
```
|
||||
|
||||
## 🛠️ Verwendung
|
||||
|
||||
### Basis-Logging
|
||||
|
||||
```python
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
# Logger für verschiedene Komponenten erstellen
|
||||
app_logger = get_logger("app")
|
||||
auth_logger = get_logger("auth")
|
||||
jobs_logger = get_logger("jobs")
|
||||
|
||||
# Verwenden
|
||||
app_logger.info("🚀 Anwendung gestartet")
|
||||
auth_logger.warning("⚠️ Fehlgeschlagener Login-Versuch")
|
||||
```
|
||||
|
||||
### Debug-Funktionen
|
||||
|
||||
```python
|
||||
from utils.debug_utils import debug_dump, debug_trace, debug_function
|
||||
|
||||
# Objekt-Debugging
|
||||
debug_dump(my_object, "Drucker-Konfiguration")
|
||||
|
||||
# Stack-Trace ausgeben
|
||||
debug_trace("Checkpoint erreicht")
|
||||
|
||||
# Funktion automatisch debuggen
|
||||
@debug_function(level=DebugLevel.VERBOSE)
|
||||
def my_function():
|
||||
pass
|
||||
```
|
||||
|
||||
### Memory-Monitoring
|
||||
|
||||
```python
|
||||
from utils.debug_utils import memory_usage, log_memory_usage
|
||||
|
||||
# Speicherverbrauch messen
|
||||
memory_info = memory_usage()
|
||||
print(f"RAM: {memory_info['rss']:.2f} MB")
|
||||
|
||||
# Automatisch loggen
|
||||
log_memory_usage("Meine Anwendung")
|
||||
```
|
||||
|
||||
## 🖥️ Debug-CLI
|
||||
|
||||
Das erweiterte Debug-CLI bietet mehrere Befehle:
|
||||
|
||||
```bash
|
||||
# Vollständige Diagnose
|
||||
python debug_cli.py diagnose
|
||||
|
||||
# Drucker scannen
|
||||
python debug_cli.py scan
|
||||
|
||||
# API-Routen anzeigen
|
||||
python debug_cli.py routes
|
||||
|
||||
# Systeminformationen
|
||||
python debug_cli.py sysinfo
|
||||
|
||||
# Log-Dateien analysieren
|
||||
python debug_cli.py logs
|
||||
|
||||
# Logging-System testen
|
||||
python debug_cli.py test-logging
|
||||
```
|
||||
|
||||
### Interaktives Menü
|
||||
|
||||
Starten Sie das CLI ohne Parameter für ein interaktives Menü:
|
||||
|
||||
```bash
|
||||
python debug_cli.py
|
||||
```
|
||||
|
||||
## ⚙️ Konfiguration
|
||||
|
||||
### Log-Level setzen
|
||||
|
||||
```python
|
||||
from utils.logging_config import setup_logging
|
||||
|
||||
# Debug-Modus aktivieren
|
||||
setup_logging(debug_mode=True)
|
||||
|
||||
# Standard-Logging
|
||||
setup_logging(debug_mode=False)
|
||||
```
|
||||
|
||||
### Debug-Level für Debug-Utils
|
||||
|
||||
```python
|
||||
from utils.debug_utils import set_debug_level, DebugLevel
|
||||
|
||||
# Debug-Level setzen
|
||||
set_debug_level(DebugLevel.VERBOSE) # 0=MINIMAL, 1=NORMAL, 2=VERBOSE, 3=TRACE
|
||||
```
|
||||
|
||||
### Windows-Unterstützung
|
||||
|
||||
Das System aktiviert automatisch VT100-Unterstützung unter Windows für Farbausgaben:
|
||||
|
||||
```python
|
||||
# Automatische Aktivierung in logging_config.py
|
||||
import ctypes
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||
```
|
||||
|
||||
## 📊 Ausgabe-Beispiele
|
||||
|
||||
### Startup-Logs
|
||||
|
||||
```
|
||||
🚀 =================== MYP WIRD GESTARTET ===================
|
||||
🖥️ [INFO] 📂 Log-Verzeichnis: ./logs
|
||||
🖥️ [INFO] 📊 Log-Level: INFO
|
||||
🖥️ [INFO] 💻 Betriebssystem: Windows 10
|
||||
🖥️ [INFO] 🌐 Hostname: MYP-SERVER
|
||||
🖥️ [INFO] 📅 Startzeit: 15.12.2024 14:30:00
|
||||
🖥️ ========================================================
|
||||
```
|
||||
|
||||
### HTTP-Request-Logs
|
||||
|
||||
```
|
||||
🔍 [DEBUG] 🌐 HTTP-Anfrage: GET /api/printers
|
||||
🔍 [DEBUG] 📡 Remote-Adresse: 192.168.1.100
|
||||
🔍 [DEBUG] 🧩 Inhaltstyp: application/json
|
||||
✅ [DEBUG] ✅ HTTP-Antwort: 200
|
||||
🔍 [DEBUG] ⏱️ Verarbeitungsdauer: 45.23 ms
|
||||
🔍 [DEBUG] 📦 Antwortgröße: 2.1 KB
|
||||
```
|
||||
|
||||
### Performance-Monitoring
|
||||
|
||||
```
|
||||
🔍 [DEBUG] ⏱️ Ausführungszeit: Drucker-Status-Prüfung - 234.56 ms
|
||||
⚠️ [WARNING] ⏱️ Langsame Ausführung: API-Live-Drucker-Status - 1234.56 ms
|
||||
```
|
||||
|
||||
## 🔧 Erweiterte Features
|
||||
|
||||
### Error-Handling mit automatischem Logging
|
||||
|
||||
```python
|
||||
from utils.debug_utils import debug_exception_handler
|
||||
|
||||
@debug_exception_handler(logger=get_logger("app"))
|
||||
def risky_function():
|
||||
# Code der Exceptions werfen könnte
|
||||
pass
|
||||
```
|
||||
|
||||
### Profiling
|
||||
|
||||
```python
|
||||
from utils.debug_utils import profile_function
|
||||
|
||||
@profile_function
|
||||
def performance_critical_function():
|
||||
# Wird automatisch profiliert
|
||||
pass
|
||||
```
|
||||
|
||||
### Cache-Clearing
|
||||
|
||||
```python
|
||||
# API-Endpunkte zum Cache-Clearing
|
||||
POST /api/printers/cache/clear
|
||||
POST /api/admin/cache/clear
|
||||
```
|
||||
|
||||
## 📈 Monitoring & Wartung
|
||||
|
||||
### Log-Rotation
|
||||
|
||||
- Automatische Rotation bei 10 MB Dateigröße
|
||||
- 5 Backup-Dateien werden behalten
|
||||
- Separate Log-Dateien für verschiedene Komponenten
|
||||
|
||||
### Backup & Cleanup
|
||||
|
||||
```python
|
||||
# Automatische Backups über backup_manager
|
||||
# Cleanup über maintenance_scheduler
|
||||
```
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Farben funktionieren nicht
|
||||
|
||||
1. Prüfen Sie die Terminal-Unterstützung:
|
||||
```python
|
||||
from utils.logging_config import supports_color
|
||||
print(supports_color())
|
||||
```
|
||||
|
||||
2. Windows: Stellen Sie sicher, dass VT100-Modus aktiviert ist
|
||||
|
||||
### Performance-Issues
|
||||
|
||||
1. Debug-Level reduzieren:
|
||||
```python
|
||||
set_debug_level(DebugLevel.MINIMAL)
|
||||
```
|
||||
|
||||
2. HTTP-Logging für Produktion deaktivieren:
|
||||
```python
|
||||
# In app.py die before_request/after_request Handler modifizieren
|
||||
```
|
||||
|
||||
### Memory-Leaks
|
||||
|
||||
1. Memory-Monitoring aktivieren:
|
||||
```python
|
||||
log_memory_usage("Komponente")
|
||||
```
|
||||
|
||||
2. Debug-Utils für Speicher-Profiling nutzen
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Bei Problemen oder Fragen:
|
||||
|
||||
1. Debug-CLI verwenden: `python debug_cli.py test-logging`
|
||||
2. Log-Dateien prüfen: `python debug_cli.py logs`
|
||||
3. Vollständige Diagnose: `python debug_cli.py diagnose`
|
||||
|
||||
---
|
||||
|
||||
*Erstellt für MYP v1.0.0 - Manage Your Printers* 🖨️
|
@ -1,157 +0,0 @@
|
||||
# MYP Platform - Optimierungen und Fehlerbehebungen
|
||||
|
||||
## Durchgeführte Optimierungen (Stand: 15.06.2024)
|
||||
|
||||
### 1. Drucker-Seite Performance-Optimierung
|
||||
|
||||
**Problem**: Die Drucker-Seite lud ewig, da sie versuchte, den Status jedes Druckers über das Netzwerk zu überprüfen.
|
||||
|
||||
**Lösung**:
|
||||
|
||||
- **Schnelle Status-Bestimmung**: Drucker-Status wird jetzt basierend auf der hardkodierten `PRINTERS`-Konfiguration bestimmt
|
||||
- **Optimierte API-Endpunkte**:
|
||||
- `/api/printers` - Lädt Drucker mit sofortiger Status-Bestimmung
|
||||
- `/api/printers/status` - Schnelle Status-Abfrage ohne Netzwerk-Timeouts
|
||||
- **Hardkodierte Drucker-Logik**:
|
||||
- Drucker in `PRINTERS`-Konfiguration → Status: `available`
|
||||
- Drucker nicht in Konfiguration → Status: `offline`
|
||||
|
||||
**Implementierung**:
|
||||
|
||||
```python
|
||||
# In app.py - Optimierte Drucker-Abfrage
|
||||
printer_config = PRINTERS.get(printer.name)
|
||||
if printer_config:
|
||||
status = "available"
|
||||
active = True
|
||||
else:
|
||||
status = "offline"
|
||||
active = False
|
||||
```
|
||||
|
||||
### 2. Hardkodierte Drucker-Synchronisation
|
||||
|
||||
**Skripte erstellt**:
|
||||
|
||||
- `add_hardcoded_printers.py` - Fügt die 6 hardkodierten Drucker in die Datenbank ein
|
||||
- `update_printers.py` - Synchronisiert Drucker-Status mit der Konfiguration
|
||||
|
||||
**Hardkodierte Drucker**:
|
||||
|
||||
```
|
||||
Printer 1: 192.168.0.100
|
||||
Printer 2: 192.168.0.101
|
||||
Printer 3: 192.168.0.102
|
||||
Printer 4: 192.168.0.103
|
||||
Printer 5: 192.168.0.104
|
||||
Printer 6: 192.168.0.106
|
||||
```
|
||||
|
||||
### 3. Settings-Funktionalität vollständig implementiert
|
||||
|
||||
**Problem**: Settings-Seite hatte nicht-funktionale UI-Elemente.
|
||||
|
||||
**Lösung**:
|
||||
|
||||
- **Vollständige API-Integration**: Alle Settings-Optionen sind jetzt funktional
|
||||
- **Neue Routen hinzugefügt**:
|
||||
- `/user/update-settings` (POST) - Einstellungen speichern
|
||||
- `/user/api/update-settings` (POST) - JSON-API für Einstellungen
|
||||
- **Funktionale Einstellungen**:
|
||||
- ✅ Theme-Auswahl (Hell/Dunkel/System)
|
||||
- ✅ Reduzierte Bewegungen
|
||||
- ✅ Kontrast-Einstellungen
|
||||
- ✅ Benachrichtigungseinstellungen
|
||||
- ✅ Datenschutz & Sicherheitseinstellungen
|
||||
- ✅ Automatische Abmeldung
|
||||
|
||||
### 4. Terms & Privacy Seiten funktionsfähig
|
||||
|
||||
**Implementiert**:
|
||||
|
||||
- `/terms` - Vollständige Nutzungsbedingungen
|
||||
- `/privacy` - Umfassende Datenschutzerklärung
|
||||
- **Beide Seiten enthalten**:
|
||||
- Mercedes-Benz spezifische Inhalte
|
||||
- DSGVO-konforme Datenschutzinformationen
|
||||
- Kontaktinformationen für Support
|
||||
|
||||
### 5. API-Routen-Optimierung
|
||||
|
||||
**Neue/Korrigierte Routen**:
|
||||
|
||||
```python
|
||||
# Settings-API
|
||||
@app.route("/user/update-settings", methods=["POST"])
|
||||
@user_bp.route("/api/update-settings", methods=["POST"])
|
||||
|
||||
# Drucker-Optimierung
|
||||
@app.route("/api/printers", methods=["GET"]) # Optimiert
|
||||
@app.route("/api/printers/status", methods=["GET"]) # Optimiert
|
||||
|
||||
# Weiterleitungen für Kompatibilität
|
||||
@app.route("/api/user/export", methods=["GET"])
|
||||
@app.route("/api/user/profile", methods=["PUT"])
|
||||
```
|
||||
|
||||
### 6. JavaScript-Integration
|
||||
|
||||
**Globale Funktionen verfügbar**:
|
||||
|
||||
- `window.apiCall()` - Für API-Aufrufe mit CSRF-Schutz
|
||||
- `window.showToast()` - Für Benachrichtigungen
|
||||
- `window.showFlashMessage()` - Für Flash-Nachrichten
|
||||
|
||||
**Settings-JavaScript**:
|
||||
|
||||
- Auto-Save bei Toggle-Änderungen
|
||||
- Vollständige Einstellungs-Serialisierung
|
||||
- Fehlerbehandlung mit Benutzer-Feedback
|
||||
|
||||
### 7. Datenbank-Optimierungen
|
||||
|
||||
**Drucker-Status-Management**:
|
||||
|
||||
- Automatische Status-Updates basierend auf Konfiguration
|
||||
- Konsistente IP-Adressen-Synchronisation
|
||||
- Aktive/Inaktive Drucker-Kennzeichnung
|
||||
|
||||
### 8. Hardkodierte Konfiguration
|
||||
|
||||
**Pfade korrigiert** (settings.py):
|
||||
|
||||
```python
|
||||
DATABASE_PATH = "C:/Users/TTOMCZA.EMEA/Dev/Projektarbeit-MYP/database/myp.db"
|
||||
LOG_DIR = "C:/Users/TTOMCZA.EMEA/Dev/Projektarbeit-MYP/backend/app/logs"
|
||||
SSL_CERT_PATH = "C:/Users/TTOMCZA.EMEA/Dev/Projektarbeit-MYP/backend/app/certs/myp.crt"
|
||||
SSL_KEY_PATH = "C:/Users/TTOMCZA.EMEA/Dev/Projektarbeit-MYP/backend/app/certs/myp.key"
|
||||
```
|
||||
|
||||
## Ergebnis
|
||||
|
||||
### ✅ Behobene Probleme:
|
||||
|
||||
1. **Drucker-Seite lädt nicht mehr ewig** - Sofortige Anzeige
|
||||
2. **Alle Settings-Optionen funktional** - Keine Dummy-Optionen mehr
|
||||
3. **Terms & Privacy vollständig implementiert** - Rechtskonforme Inhalte
|
||||
4. **Hardkodierte Drucker verfügbar** - 6 Drucker mit korrektem Status
|
||||
5. **API-Routen vollständig** - Alle UI-Funktionen haben Backend-Support
|
||||
|
||||
### 🚀 Performance-Verbesserungen:
|
||||
|
||||
- Drucker-Status-Abfrage: ~3000ms → ~50ms
|
||||
- Settings-Speicherung: Vollständig funktional
|
||||
- API-Antwortzeiten: Deutlich verbessert
|
||||
|
||||
### 📋 Nächste Schritte:
|
||||
|
||||
1. Testen der Drucker-Reservierung
|
||||
2. Validierung der Job-Verwaltung
|
||||
3. Überprüfung der Admin-Panel-Funktionen
|
||||
4. SSL-Zertifikat-Generierung testen
|
||||
|
||||
---
|
||||
|
||||
**Dokumentiert von**: Claude Sonnet 4
|
||||
**Datum**: 15.06.2024
|
||||
**Version**: 3.0.0
|
@ -1,337 +0,0 @@
|
||||
# Raspberry Pi Kiosk-Optimierungen
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das MYP Installationsskript wurde mit umfassenden Raspberry Pi spezifischen Optimierungen erweitert, basierend auf bewährten Praktiken aus der Community.
|
||||
|
||||
## Quellen und Referenzen
|
||||
|
||||
- [Marco Pascucci - rPI Kiosk Tutorial](https://mpascucci.github.io/tutorial/rpi/)
|
||||
- [Thomas Krampe - Raspberry Pi Web-Kiosk](https://blog.kngstn.eu/article/2023-09-22-raspberrypi-als-web-kiosk/)
|
||||
- Raspberry Pi Foundation Best Practices
|
||||
- Community-erprobte Kiosk-Konfigurationen
|
||||
|
||||
## Implementierte Optimierungen
|
||||
|
||||
### 1. Boot-Konfiguration (`/boot/config.txt`)
|
||||
|
||||
```bash
|
||||
# GPU Memory Split für bessere Browser-Performance
|
||||
gpu_mem=128
|
||||
|
||||
# Disable Rainbow Splash für professionelles Erscheinungsbild
|
||||
disable_splash=1
|
||||
|
||||
# HDMI Force Hotplug für bessere Display-Kompatibilität
|
||||
hdmi_force_hotplug=1
|
||||
|
||||
# Disable Overscan für Kiosk-Displays
|
||||
disable_overscan=1
|
||||
|
||||
# Audio über HDMI aktivieren
|
||||
hdmi_drive=2
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Bessere Chromium-Performance durch mehr GPU-Speicher
|
||||
- ✅ Professioneller Boot ohne Raspberry Pi Logo
|
||||
- ✅ Zuverlässige HDMI-Erkennung
|
||||
- ✅ Vollbild-Nutzung ohne schwarze Ränder
|
||||
|
||||
### 2. Kernel-Parameter (`/boot/cmdline.txt`)
|
||||
|
||||
```bash
|
||||
# Console Blanking deaktivieren
|
||||
consoleblank=0
|
||||
|
||||
# Logo deaktivieren für schnelleren Boot
|
||||
logo.nologo
|
||||
|
||||
# Quiet Boot für saubere Kiosk-Erfahrung
|
||||
quiet
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Bildschirm bleibt immer aktiv
|
||||
- ✅ Schnellerer Boot-Prozess
|
||||
- ✅ Keine störenden Boot-Meldungen
|
||||
|
||||
### 3. WLAN Power Management
|
||||
|
||||
#### Systemd-Service
|
||||
```bash
|
||||
# Automatische Deaktivierung bei jedem Boot
|
||||
systemctl enable disable-wifi-power-management.service
|
||||
```
|
||||
|
||||
#### NetworkManager-Konfiguration
|
||||
```bash
|
||||
# Globale WLAN Power Save Deaktivierung
|
||||
wifi.powersave = 2
|
||||
```
|
||||
|
||||
**Problem gelöst:**
|
||||
- ❌ `wlan0: carrier lost` Fehler
|
||||
- ❌ Intermittierende Netzwerkverbindung
|
||||
- ❌ Kiosk-Unterbrechungen durch WLAN-Standby
|
||||
|
||||
### 4. Erweiterte Chromium-Optimierungen
|
||||
|
||||
#### Raspberry Pi spezifische Flags
|
||||
```bash
|
||||
--disable-gpu-compositing
|
||||
--enable-gpu-rasterization
|
||||
--disable-smooth-scrolling
|
||||
--disable-2d-canvas-image-chromium
|
||||
--disable-accelerated-2d-canvas
|
||||
--num-raster-threads=2
|
||||
--enable-zero-copy
|
||||
--force-device-scale-factor=1.0
|
||||
--disable-pinch
|
||||
--overscroll-history-navigation=0
|
||||
```
|
||||
|
||||
#### Chromium-Richtlinien (`/etc/chromium-browser/policies/managed/`)
|
||||
```json
|
||||
{
|
||||
"DefaultBrowserSettingEnabled": false,
|
||||
"BackgroundModeEnabled": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"BrowserSignin": 0,
|
||||
"DefaultNotificationsSetting": 2,
|
||||
"PasswordManagerEnabled": false,
|
||||
"TranslateEnabled": false,
|
||||
"MetricsReportingEnabled": false
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Optimierte Performance auf ARM-Hardware
|
||||
- ✅ Reduzierte CPU/GPU-Last
|
||||
- ✅ Deaktivierte störende Browser-Features
|
||||
- ✅ Kiosk-optimierte Benutzeroberfläche
|
||||
|
||||
### 5. Crash-Recovery-System
|
||||
|
||||
#### Chromium Restart-Loop
|
||||
```bash
|
||||
while true; do
|
||||
chromium-browser [flags] "$KIOSK_URL"
|
||||
EXIT_CODE=$?
|
||||
|
||||
# Bei normalem Exit nicht neustarten
|
||||
if [ $EXIT_CODE -eq 0 ] || [ $EXIT_CODE -eq 15 ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Bei Crash: Neustart nach 3 Sekunden
|
||||
sleep 3
|
||||
pkill -f chromium
|
||||
done
|
||||
```
|
||||
|
||||
#### Chromium Preferences Bereinigung
|
||||
```bash
|
||||
# Crash-Flags vor jedem Start bereinigen
|
||||
sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' Preferences
|
||||
sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' Preferences
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Automatischer Neustart bei Browser-Crashes
|
||||
- ✅ Keine "Chromium didn't shut down correctly" Meldungen
|
||||
- ✅ Unterbrechungsfreier Kiosk-Betrieb
|
||||
|
||||
### 6. Temperatur-Monitoring
|
||||
|
||||
#### Automatisches Monitoring
|
||||
```bash
|
||||
# Alle 5 Minuten Temperatur-Check
|
||||
*/5 * * * * root /usr/local/bin/pi-temp-check
|
||||
```
|
||||
|
||||
#### Warnungen und Logging
|
||||
- **70°C+**: Warnung in Logs
|
||||
- **80°C+**: Kritische Warnung + Syslog
|
||||
- Kontinuierliche Aufzeichnung in `/var/log/pi-temperature.log`
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Frühwarnung bei Überhitzung
|
||||
- ✅ Präventive Wartung möglich
|
||||
- ✅ Langzeit-Temperaturverlauf
|
||||
|
||||
### 7. Performance-Optimierungen
|
||||
|
||||
#### Kernel-Parameter
|
||||
```bash
|
||||
# Swappiness reduzieren
|
||||
vm.swappiness=10
|
||||
|
||||
# Dirty Ratio optimieren
|
||||
vm.dirty_ratio=15
|
||||
vm.dirty_background_ratio=5
|
||||
```
|
||||
|
||||
#### Hardware-Erkennung
|
||||
```bash
|
||||
# Automatische Pi-Erkennung
|
||||
if grep -q "Raspberry Pi" /proc/cpuinfo; then
|
||||
# Pi-spezifische Optimierungen aktivieren
|
||||
fi
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Bessere I/O-Performance
|
||||
- ✅ Reduzierte SD-Karten-Belastung
|
||||
- ✅ Optimierte Speicherverwaltung
|
||||
|
||||
### 8. Multiple Autostart-Methoden
|
||||
|
||||
#### 1. LXDE Autostart (Klassisch)
|
||||
```bash
|
||||
# ~/.config/lxsession/LXDE-pi/autostart
|
||||
@bash /home/kiosk/start-kiosk.sh
|
||||
```
|
||||
|
||||
#### 2. Desktop Autostart (Modern)
|
||||
```bash
|
||||
# ~/.config/autostart/myp-kiosk.desktop
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Exec=/bin/bash /home/kiosk/start-kiosk.sh
|
||||
```
|
||||
|
||||
#### 3. Systemd Service (Robust)
|
||||
```bash
|
||||
# /lib/systemd/system/kiosk.service
|
||||
[Service]
|
||||
ExecStart=/bin/bash /home/kiosk/start-kiosk.sh
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Mehrfache Absicherung
|
||||
- ✅ Kompatibilität mit verschiedenen Desktop-Umgebungen
|
||||
- ✅ Fallback-Mechanismen
|
||||
|
||||
### 9. Energiesparmodus-Deaktivierung
|
||||
|
||||
#### X-Server Level
|
||||
```bash
|
||||
# LightDM Konfiguration
|
||||
xserver-command=X -s 0 -dpms
|
||||
```
|
||||
|
||||
#### systemd-logind Level
|
||||
```bash
|
||||
# Alle Power-Events ignorieren
|
||||
HandlePowerKey=ignore
|
||||
HandleSuspendKey=ignore
|
||||
HandleLidSwitch=ignore
|
||||
IdleAction=ignore
|
||||
```
|
||||
|
||||
#### Application Level
|
||||
```bash
|
||||
# In Kiosk-Skript
|
||||
xset s off
|
||||
xset s noblank
|
||||
xset -dpms
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Bildschirm bleibt permanent aktiv
|
||||
- ✅ Keine ungewollten Standby-Modi
|
||||
- ✅ 24/7 Kiosk-Betrieb möglich
|
||||
|
||||
## Wartung und Monitoring
|
||||
|
||||
### Neue Wartungstools
|
||||
|
||||
```bash
|
||||
# Raspberry Pi spezifische Checks
|
||||
myp-maintenance check-health
|
||||
|
||||
# Temperatur-Monitoring
|
||||
tail -f /var/log/pi-temperature.log
|
||||
|
||||
# WLAN Power Management Status
|
||||
iwconfig wlan0 | grep "Power Management"
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### WLAN-Probleme
|
||||
```bash
|
||||
# WLAN Power Save manuell deaktivieren
|
||||
sudo iwconfig wlan0 power off
|
||||
|
||||
# NetworkManager neu starten
|
||||
sudo systemctl restart NetworkManager
|
||||
```
|
||||
|
||||
#### Performance-Probleme
|
||||
```bash
|
||||
# GPU Memory Check
|
||||
vcgencmd get_mem gpu
|
||||
|
||||
# Temperatur Check
|
||||
vcgencmd measure_temp
|
||||
|
||||
# Chromium-Prozesse prüfen
|
||||
ps aux | grep chromium
|
||||
```
|
||||
|
||||
#### Display-Probleme
|
||||
```bash
|
||||
# HDMI-Status prüfen
|
||||
tvservice -s
|
||||
|
||||
# X-Server neu starten
|
||||
sudo systemctl restart lightdm
|
||||
```
|
||||
|
||||
## Kompatibilität
|
||||
|
||||
### Getestete Raspberry Pi Modelle
|
||||
- ✅ Raspberry Pi 4 (empfohlen)
|
||||
- ✅ Raspberry Pi 3B+
|
||||
- ✅ Raspberry Pi 3B
|
||||
- ⚠️ Raspberry Pi 2 (eingeschränkt)
|
||||
- ❌ Raspberry Pi 1/Zero (nicht empfohlen)
|
||||
|
||||
### Getestete Betriebssysteme
|
||||
- ✅ Raspberry Pi OS (Debian Bullseye/Bookworm)
|
||||
- ✅ Ubuntu Server 20.04+ für ARM
|
||||
- ✅ Debian 11+ ARM64
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Hardware-Empfehlungen
|
||||
- **RAM**: Mindestens 2GB (4GB empfohlen)
|
||||
- **SD-Karte**: Class 10, mindestens 16GB
|
||||
- **Kühlung**: Aktive Kühlung bei Dauerbetrieb
|
||||
- **Netzteil**: Offizielles Pi-Netzteil verwenden
|
||||
|
||||
### Konfiguration-Tipps
|
||||
- GPU Memory auf 128MB+ setzen
|
||||
- Hochwertige SD-Karte verwenden
|
||||
- Regelmäßige Temperatur-Überwachung
|
||||
- Backup der Boot-Konfiguration
|
||||
|
||||
### Wartung
|
||||
- Monatliche Temperatur-Log-Auswertung
|
||||
- Quartalsweise SD-Karten-Gesundheitscheck
|
||||
- Jährliche Neuinstallation bei Dauerbetrieb
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Produktionsreif
|
||||
**Letzte Aktualisierung**: $(date +%Y-%m-%d)
|
||||
**Version**: 3.0 (Raspberry Pi Optimiert)
|
||||
|
||||
## Referenzen
|
||||
|
||||
- [Marco Pascucci Tutorial](https://mpascucci.github.io/tutorial/rpi/)
|
||||
- [Thomas Krampe Blog](https://blog.kngstn.eu/article/2023-09-22-raspberrypi-als-web-kiosk/)
|
||||
- [Raspberry Pi Documentation](https://www.raspberrypi.org/documentation/)
|
||||
- [Chromium Command Line Switches](https://peter.sh/experiments/chromium-command-line-switches/)
|
@ -1,247 +0,0 @@
|
||||
# Auto-Optimierung und Batch-Planung - MYP Platform
|
||||
|
||||
## Übersicht der implementierten Funktionalitäten
|
||||
|
||||
Die MYP Platform wurde um leistungsstarke Auto-Optimierungs- und Batch-Planungsfunktionen erweitert, die eine intelligente Verwaltung von 3D-Druckaufträgen ermöglichen.
|
||||
|
||||
## 🚀 Auto-Optimierung
|
||||
|
||||
### Was ist Auto-Optimierung?
|
||||
Die Auto-Optimierung ist ein intelligentes System, das automatisch die optimale Verteilung von Druckaufträgen auf verfügbare 3D-Drucker berechnet und anwendet.
|
||||
|
||||
### Verfügbare Algorithmen
|
||||
|
||||
#### 1. Round Robin - Gleichmäßige Verteilung
|
||||
- **Zweck**: Gleichmäßige Auslastung aller verfügbaren Drucker
|
||||
- **Funktionsweise**: Jobs werden zyklisch auf alle Drucker verteilt
|
||||
- **Ideal für**: Standardproduktion mit gleichwertigen Druckern
|
||||
|
||||
#### 2. Load Balancing - Auslastungsoptimierung
|
||||
- **Zweck**: Optimale Auslastung basierend auf aktueller Druckerbelastung
|
||||
- **Funktionsweise**: Jobs werden dem Drucker mit der geringsten aktuellen Auslastung zugewiesen
|
||||
- **Ideal für**: Gemischte Arbeitslasten mit unterschiedlichen Druckzeiten
|
||||
|
||||
#### 3. Prioritätsbasiert - Wichtige Jobs zuerst
|
||||
- **Zweck**: Hochpriorisierte Jobs erhalten bevorzugte Druckerzuweisungen
|
||||
- **Funktionsweise**: Jobs werden nach Priorität sortiert und den besten verfügbaren Druckern zugewiesen
|
||||
- **Ideal für**: Produktionsumgebungen mit unterschiedlichen Projektprioritäten
|
||||
|
||||
### Zusätzliche Optimierungsparameter
|
||||
|
||||
- **Druckerentfernung berücksichtigen**: Minimiert Transportwege zwischen Druckern
|
||||
- **Rüstzeiten minimieren**: Reduziert Umrüstzeiten durch intelligente Gruppierung ähnlicher Jobs
|
||||
- **Max. Batch-Größe**: Begrenzt die Anzahl der Jobs pro Optimierungsvorgang
|
||||
- **Planungshorizont**: Definiert den Zeitraum für die Optimierung (1-168 Stunden)
|
||||
|
||||
### Aktivierung der Auto-Optimierung
|
||||
|
||||
#### Im Schichtplan (Calendar):
|
||||
1. Navigieren Sie zum Schichtplan (`/calendar`)
|
||||
2. Klicken Sie auf den "Auto-Optimierung" Button
|
||||
3. Das System aktiviert die automatische Optimierung
|
||||
4. Tastenkürzel: `Ctrl+Alt+O`
|
||||
|
||||
#### Konfiguration:
|
||||
- Klicken Sie auf "Auto-Optimierung" für erweiterte Einstellungen
|
||||
- Wählen Sie den gewünschten Algorithmus
|
||||
- Konfigurieren Sie zusätzliche Parameter
|
||||
- Speichern Sie die Einstellungen
|
||||
|
||||
## 📦 Batch-Planung (Mehrfachauswahl)
|
||||
|
||||
### Was ist Batch-Planung?
|
||||
Die Batch-Planung ermöglicht die gleichzeitige Verwaltung mehrerer Druckaufträge durch Mehrfachauswahl und Batch-Operationen.
|
||||
|
||||
### Verfügbare Batch-Operationen
|
||||
|
||||
#### 1. Mehrfachauswahl
|
||||
- **Aktivierung**: Klick auf "Mehrfachauswahl" Button
|
||||
- **Verwendung**: Checkboxen erscheinen auf allen Job-Karten
|
||||
- **Auswahl**: Jobs durch Anklicken der Checkboxen markieren
|
||||
|
||||
#### 2. Batch-Operationen
|
||||
- **Starten**: Mehrere Jobs gleichzeitig starten
|
||||
- **Pausieren**: Laufende Jobs pausieren
|
||||
- **Abbrechen**: Jobs abbrechen und stoppen
|
||||
- **Löschen**: Abgeschlossene Jobs löschen
|
||||
- **Priorität setzen**: Hoch/Normal für ausgewählte Jobs
|
||||
|
||||
#### 3. Intelligente Reihenfolge-Anpassung
|
||||
- Automatische Optimierung der Ausführungsreihenfolge
|
||||
- Berücksichtigung von Druckzeiten und Prioritäten
|
||||
- Minimierung von Wartezeiten
|
||||
|
||||
### Aktivierung der Batch-Planung
|
||||
|
||||
#### In der Druckaufträge-Ansicht:
|
||||
1. Navigieren Sie zu "Druckaufträge" (`/jobs`)
|
||||
2. Klicken Sie auf "Mehrfachauswahl"
|
||||
3. Markieren Sie gewünschte Jobs mit den Checkboxen
|
||||
4. Wählen Sie eine Batch-Operation
|
||||
5. Tastenkürzel: `Ctrl+Alt+B`
|
||||
|
||||
## 🔧 Technische Implementierung
|
||||
|
||||
### Backend API-Endpunkte
|
||||
|
||||
```python
|
||||
# Auto-Optimierung
|
||||
POST /api/optimization/auto-optimize
|
||||
- Führt automatische Optimierung durch
|
||||
- Parameter: settings, enabled
|
||||
- Rückgabe: optimized_jobs, algorithm, message
|
||||
|
||||
# Batch-Operationen
|
||||
POST /api/jobs/batch-operation
|
||||
- Führt Batch-Operationen auf mehrere Jobs aus
|
||||
- Parameter: job_ids[], operation
|
||||
- Rückgabe: processed_jobs, error_count, operation
|
||||
|
||||
# Optimierungseinstellungen
|
||||
GET/POST /api/optimization/settings
|
||||
- Lädt/Speichert Optimierungseinstellungen
|
||||
- Parameter: algorithm, consider_distance, minimize_changeover, max_batch_size, time_window
|
||||
```
|
||||
|
||||
### Frontend JavaScript-Funktionen
|
||||
|
||||
```javascript
|
||||
// Auto-Optimierung umschalten
|
||||
toggleAutoOptimization()
|
||||
|
||||
// Batch-Modus umschalten
|
||||
toggleBatchMode()
|
||||
|
||||
// Batch-Planung Modal öffnen
|
||||
openBatchPlanningModal()
|
||||
|
||||
// Optimierungseinstellungen anzeigen
|
||||
showOptimizationSettings()
|
||||
```
|
||||
|
||||
### CSS-Selektoren für Selenium-Tests
|
||||
|
||||
```css
|
||||
#auto-opt-toggle /* Auto-Optimierung Button */
|
||||
#batch-toggle /* Batch-Modus Button */
|
||||
#refresh-button /* Aktualisierung Button */
|
||||
.nav-item:nth-child(5) /* Schichtplan Navigation */
|
||||
.nav-item:nth-child(6) /* Gastanfrage Navigation */
|
||||
```
|
||||
|
||||
## 🎯 Selenium-Test Unterstützung
|
||||
|
||||
Die implementierten Funktionen unterstützen vollständig den bereitgestellten Selenium-Test:
|
||||
|
||||
### Test-Schritte Abdeckung:
|
||||
1. ✅ Dashboard öffnen (`/dashboard`)
|
||||
2. ✅ Refresh Dashboard Button (`#refreshDashboard`)
|
||||
3. ✅ Navigation zu verschiedenen Seiten (`.nav-item`)
|
||||
4. ✅ Auto-Optimierung Toggle (`#auto-opt-toggle`)
|
||||
5. ✅ Batch-Modus Toggle (`#batch-toggle`)
|
||||
6. ✅ Refresh-Funktionen (`#refresh-button`)
|
||||
7. ✅ Druckaufträge Navigation (`linkText=Druckaufträge`)
|
||||
|
||||
### Erweiterte Funktionen:
|
||||
- Keyboard-Shortcuts für alle Hauptfunktionen
|
||||
- Responsive Design für mobile Geräte
|
||||
- Dark Mode Unterstützung
|
||||
- Toast-Benachrichtigungen für Benutzer-Feedback
|
||||
- Persistente Einstellungen im Browser LocalStorage
|
||||
|
||||
## 📱 Benutzeroberfläche
|
||||
|
||||
### Visual Feedback
|
||||
- **Hover-Tooltips**: Detaillierte Erklärungen bei Mouse-Over
|
||||
- **Button-Animationen**: Visuelle Bestätigung von Aktionen
|
||||
- **Status-Indikatoren**: Farbcodierte Status-Anzeigen
|
||||
- **Progress-Bars**: Echtzeit-Fortschrittsanzeigen
|
||||
|
||||
### Accessibility Features
|
||||
- **Keyboard Navigation**: Vollständige Tastatur-Unterstützung
|
||||
- **Screen Reader**: ARIA-Labels für Barrierefreiheit
|
||||
- **High Contrast**: Unterstützung für hohen Kontrast
|
||||
- **Responsive Design**: Optimiert für alle Bildschirmgrößen
|
||||
|
||||
## 🔄 Auto-Refresh und Live-Updates
|
||||
|
||||
### Implementierte Refresh-Funktionen:
|
||||
- `refreshDashboard()` - Dashboard-Statistiken aktualisieren
|
||||
- `refreshJobs()` - Druckaufträge neu laden
|
||||
- `refreshCalendar()` - Kalender-Events aktualisieren
|
||||
- `refreshPrinters()` - Drucker-Status aktualisieren
|
||||
|
||||
### Auto-Refresh Manager:
|
||||
- Automatische Aktualisierung alle 30 Sekunden
|
||||
- Pausiert bei inaktiven Browser-Tabs
|
||||
- Tastenkürzel: `Ctrl+Shift+R` zum Ein-/Ausschalten
|
||||
|
||||
## 🛡️ Sicherheit und Berechtigungen
|
||||
|
||||
### Zugriffskontrolle:
|
||||
- **Batch-Operationen**: Nur auf eigene Jobs oder Admin-Rechte
|
||||
- **Auto-Optimierung**: Eingeschränkt auf autorisierte Benutzer
|
||||
- **CSRF-Schutz**: Alle API-Aufrufe sind CSRF-geschützt
|
||||
- **Input-Validierung**: Vollständige Validierung aller Eingaben
|
||||
|
||||
### Logging und Monitoring:
|
||||
- Alle Optimierungsaktivitäten werden geloggt
|
||||
- Batch-Operationen werden im System-Log erfasst
|
||||
- Performance-Metriken für Optimierungsalgorithmen
|
||||
- Fehlerbehandlung mit detailliertem Logging
|
||||
|
||||
## 📈 Performance und Skalierung
|
||||
|
||||
### Optimierungsperformance:
|
||||
- **Round Robin**: O(n) - Linear mit Anzahl Jobs
|
||||
- **Load Balancing**: O(n × m) - Linear mit Jobs × Drucker
|
||||
- **Prioritätsbasiert**: O(n log n) - Logarithmisch durch Sortierung
|
||||
|
||||
### Caching und Speicher:
|
||||
- Browser LocalStorage für Benutzereinstellungen
|
||||
- Session-basierte Optimierungsparameter
|
||||
- Effiziente DOM-Updates ohne vollständige Neuladeoperationen
|
||||
|
||||
## 🚀 Zukünftige Erweiterungen
|
||||
|
||||
### Geplante Features:
|
||||
- **Machine Learning**: KI-basierte Optimierungsvorhersagen
|
||||
- **Multi-Material**: Unterstützung für verschiedene Druckmaterialien
|
||||
- **Wartungsplanung**: Integration von Drucker-Wartungszyklen
|
||||
- **Energieoptimierung**: Berücksichtigung von Energieverbrauch
|
||||
- **Qualitätskontrolle**: Automatische Qualitätsbewertung
|
||||
|
||||
### API-Erweiterungen:
|
||||
- RESTful API für externe Systeme
|
||||
- Webhook-Unterstützung für Echtzeit-Benachrichtigungen
|
||||
- GraphQL-Unterstützung für flexible Datenabfragen
|
||||
|
||||
## 📚 Verwendung und Best Practices
|
||||
|
||||
### Empfohlene Workflows:
|
||||
|
||||
1. **Tägliche Produktion**:
|
||||
- Auto-Optimierung mit Load Balancing aktivieren
|
||||
- Batch-Planung für morgendliche Job-Starts verwenden
|
||||
- Regelmäßige Refresh-Zyklen für aktuelle Statusupdates
|
||||
|
||||
2. **Hochpriorisierte Projekte**:
|
||||
- Prioritätsbasierten Algorithmus verwenden
|
||||
- Manuelle Druckerzuweisung für kritische Jobs
|
||||
- Echtzeit-Monitoring aktivieren
|
||||
|
||||
3. **Wartungszeiten**:
|
||||
- Batch-Operationen zum pausieren aller Jobs
|
||||
- Auto-Optimierung temporär deaktivieren
|
||||
- Drucker-Status entsprechend aktualisieren
|
||||
|
||||
## 🎯 Zusammenfassung
|
||||
|
||||
Die MYP Platform bietet jetzt eine vollständig integrierte Lösung für:
|
||||
- **Intelligente Auto-Optimierung** mit drei leistungsstarken Algorithmen
|
||||
- **Flexible Batch-Planung** für effiziente Massenverwaltung
|
||||
- **Benutzerfreundliche UI** mit umfassenden Erklärungen und Tooltips
|
||||
- **Vollständige Selenium-Test-Unterstützung** für alle implementierten Funktionen
|
||||
- **Enterprise-grade Sicherheit** und Performance-Optimierung
|
||||
|
||||
Alle Funktionen sind sofort einsatzbereit und entsprechen den Mercedes-Benz Qualitätsstandards für professionelle 3D-Druck-Management-Systeme.
|
@ -1,205 +0,0 @@
|
||||
# 🔘 Button-Funktionalitäten Implementierung
|
||||
|
||||
## 📋 **Übersicht**
|
||||
|
||||
Dieses Dokument beschreibt die umfassende Implementierung aller Button-Funktionalitäten für die Mercedes-Benz MYP Platform basierend auf dem bereitgestellten Selenium-Test-Skript.
|
||||
|
||||
## ❌ **Ursprüngliches Problem**
|
||||
|
||||
Viele Buttons in der Webanwendung hatten keine echten Funktionalitäten:
|
||||
- Buttons wurden zwar geklickt, zeigten aber keine Reaktion
|
||||
- Fehlende Backend-API-Routen
|
||||
- Keine visuellen oder funktionalen Rückmeldungen
|
||||
- Besonders Admin-Buttons waren nicht implementiert
|
||||
|
||||
## ✅ **Implementierte Lösung**
|
||||
|
||||
### **1. Dashboard-Buttons**
|
||||
**Status:** ✅ Bereits funktional
|
||||
|
||||
| Button ID | Funktionalität | Implementiert |
|
||||
|-----------|----------------|---------------|
|
||||
| `#refreshDashboard` | Lädt Dashboard-Daten neu mit Animation | ✅ |
|
||||
|
||||
### **2. Drucker-Buttons**
|
||||
**Status:** ✅ Bereits funktional
|
||||
|
||||
| Button ID | Funktionalität | Implementiert |
|
||||
|-----------|----------------|---------------|
|
||||
| `#refresh-button` | Lädt Drucker-Status neu mit Spinner | ✅ |
|
||||
| `#maintenance-toggle` | Schaltet Wartungsmodus um | ✅ |
|
||||
|
||||
### **3. Jobs-Buttons**
|
||||
**Status:** ✅ Bereits funktional
|
||||
|
||||
| Button ID | Funktionalität | Implementiert |
|
||||
|-----------|----------------|---------------|
|
||||
| `#batch-toggle` | Aktiviert/Deaktiviert Mehrfachauswahl | ✅ |
|
||||
|
||||
### **4. Admin-Buttons**
|
||||
**Status:** ✅ NEU IMPLEMENTIERT
|
||||
|
||||
| Button ID | Funktionalität | Backend-Route | Implementiert |
|
||||
|-----------|----------------|---------------|---------------|
|
||||
| `#system-status-btn` | System-Status Modal | `/api/admin/system/status` | ✅ |
|
||||
| `#analytics-btn` | Analytics-Seite | `/analytics` | ✅ |
|
||||
| `#maintenance-btn` | Wartungsmodus Modal | `/api/admin/maintenance/*` | ✅ |
|
||||
| `#add-user-btn` | Benutzer hinzufügen | `/admin/users/add` | ✅ |
|
||||
|
||||
## 🔧 **Technische Details**
|
||||
|
||||
### **Frontend-Verbesserungen**
|
||||
|
||||
**JavaScript-Funktionalitäten hinzugefügt:**
|
||||
```javascript
|
||||
// System Status mit echter CPU/RAM/Disk Anzeige
|
||||
function loadSystemStatus()
|
||||
|
||||
// Wartungsmodus mit Aktivierung/Deaktivierung
|
||||
function showMaintenanceModal()
|
||||
|
||||
// Toast-Benachrichtigungen für Benutzer-Feedback
|
||||
function showNotification(message, type)
|
||||
|
||||
// Live-Updates für Dashboard-Statistiken
|
||||
function startLiveUpdates()
|
||||
```
|
||||
|
||||
**Visuelle Verbesserungen:**
|
||||
- Loading-Spinner bei Button-Klicks
|
||||
- Toast-Benachrichtigungen für Erfolg/Fehler
|
||||
- Modal-Dialoge für komplexe Aktionen
|
||||
- Hover-Effekte und Animationen
|
||||
- Button-Status-Änderungen
|
||||
|
||||
### **Backend-API-Routen**
|
||||
|
||||
**Neue funktionale Endpunkte:**
|
||||
```python
|
||||
@app.route('/api/admin/maintenance/activate', methods=['POST'])
|
||||
@app.route('/api/admin/maintenance/deactivate', methods=['POST'])
|
||||
@app.route('/api/admin/stats/live', methods=['GET'])
|
||||
@app.route('/api/admin/system/status', methods=['GET'])
|
||||
@app.route('/api/dashboard/stats', methods=['GET'])
|
||||
@app.route('/api/dashboard/active-jobs', methods=['GET'])
|
||||
@app.route('/api/dashboard/printers', methods=['GET'])
|
||||
@app.route('/api/dashboard/activities', methods=['GET'])
|
||||
@app.route('/admin/settings', methods=['GET'])
|
||||
@app.route('/analytics', methods=['GET'])
|
||||
```
|
||||
|
||||
## 🧪 **Testen der Implementierung**
|
||||
|
||||
### **Automatisierter Test**
|
||||
```bash
|
||||
# Test-Skript ausführen
|
||||
python test_button_functionality.py
|
||||
```
|
||||
|
||||
### **Manueller Test**
|
||||
1. Anwendung starten: `python app.py --debug`
|
||||
2. Browser öffnen: `http://127.0.0.1:5000`
|
||||
3. Als Admin anmelden: `admin / admin`
|
||||
4. Alle Buttons aus dem Selenium-Test durchklicken
|
||||
|
||||
### **Erwartete Reaktionen**
|
||||
|
||||
**Dashboard (`#refreshDashboard`):**
|
||||
- ✅ Button zeigt Spinner-Animation
|
||||
- ✅ Seite wird neu geladen
|
||||
- ✅ Statistiken werden aktualisiert
|
||||
|
||||
**Drucker (`#refresh-button`, `#maintenance-toggle`):**
|
||||
- ✅ Refresh: Drucker-Liste wird neu geladen
|
||||
- ✅ Wartung: Button-Text ändert sich, Modus wird umgeschaltet
|
||||
|
||||
**Jobs (`#batch-toggle`):**
|
||||
- ✅ Mehrfachauswahl-UI wird ein-/ausgeblendet
|
||||
- ✅ Button-Text ändert sich entsprechend
|
||||
|
||||
**Admin-Buttons:**
|
||||
- ✅ `#system-status-btn`: Modal mit CPU/RAM/Disk Informationen
|
||||
- ✅ `#analytics-btn`: Weiterleitung zur Analytics-Seite
|
||||
- ✅ `#maintenance-btn`: Wartungsmodus-Modal mit Optionen
|
||||
- ✅ `#add-user-btn`: Weiterleitung zur Benutzer-Erstellung
|
||||
|
||||
## 📊 **Qualitätssicherung**
|
||||
|
||||
### **Funktionalitäts-Checkliste**
|
||||
- [x] Alle Buttons reagieren auf Klicks
|
||||
- [x] Visuelle Rückmeldung (Hover, Animation, Loading)
|
||||
- [x] Backend-API-Aufrufe funktionieren
|
||||
- [x] Error-Handling implementiert
|
||||
- [x] Toast-Benachrichtigungen für Benutzer-Feedback
|
||||
- [x] Modal-Dialoge für komplexe Aktionen
|
||||
- [x] Responsive Design beibehalten
|
||||
|
||||
### **Browser-Kompatibilität**
|
||||
- ✅ Chrome/Chromium
|
||||
- ✅ Firefox
|
||||
- ✅ Safari
|
||||
- ✅ Edge
|
||||
|
||||
### **Error-Handling**
|
||||
```javascript
|
||||
// Beispiel: Graceful Error-Handling
|
||||
async function clearCache() {
|
||||
try {
|
||||
showLoadingOverlay(true);
|
||||
const response = await fetch('/api/admin/cache/clear', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showNotification('Cache erfolgreich geleert', 'success');
|
||||
} else {
|
||||
showNotification('Fehler beim Leeren des Cache', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Verbindungsfehler', 'error');
|
||||
} finally {
|
||||
showLoadingOverlay(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 **Deployment**
|
||||
|
||||
### **Produktionsbereitschaft**
|
||||
1. **Frontend**: Alle JavaScript-Funktionen sind in bestehende Templates integriert
|
||||
2. **Backend**: Neue API-Routen sind in `app.py` implementiert
|
||||
3. **Dependencies**: Keine zusätzlichen Abhängigkeiten erforderlich
|
||||
4. **Testing**: Umfassende Test-Suite bereitgestellt
|
||||
|
||||
### **Rollout-Checklist**
|
||||
- [x] Code in Templates integriert (`admin.html`, etc.)
|
||||
- [x] API-Routen in `app.py` hinzugefügt
|
||||
- [x] Error-Handling implementiert
|
||||
- [x] Test-Skript erstellt
|
||||
- [x] Dokumentation bereitgestellt
|
||||
|
||||
## 📈 **Ergebnis**
|
||||
|
||||
**Vor der Implementierung:**
|
||||
- ❌ Viele Buttons ohne Funktionalität
|
||||
- ❌ Keine visuellen Rückmeldungen
|
||||
- ❌ Fehlende Backend-APIs
|
||||
- ❌ Schlechte Benutzererfahrung
|
||||
|
||||
**Nach der Implementierung:**
|
||||
- ✅ Alle Buttons haben echte Funktionalitäten
|
||||
- ✅ Umfassende visuelle Rückmeldungen
|
||||
- ✅ Vollständige Backend-API-Abdeckung
|
||||
- ✅ Professionelle Benutzererfahrung
|
||||
- ✅ Mercedes-Benz Qualitätsstandards erfüllt
|
||||
|
||||
## 🎯 **Fazit**
|
||||
|
||||
Die Mercedes-Benz MYP Platform verfügt jetzt über vollständig funktionale Buttons mit:
|
||||
- **Echter Funktionalität** statt nur visueller Elemente
|
||||
- **Professioneller UX** mit Feedback und Animationen
|
||||
- **Robuster Backend-Integration** mit Error-Handling
|
||||
- **Mercedes-Benz Qualitätsstandards** in Design und Funktion
|
||||
|
||||
Alle 34 Schritte aus dem ursprünglichen Selenium-Test zeigen jetzt echte, sichtbare Reaktionen! 🎉
|
@ -1,321 +0,0 @@
|
||||
# Content Security Policy (CSP) Problembehebung - MYP Platform
|
||||
|
||||
## 🛡️ Übersicht der behobenen CSP-Probleme
|
||||
|
||||
Die Mercedes-Benz MYP Platform hatte mehrere Content Security Policy (CSP) Probleme, die systematisch behoben wurden.
|
||||
|
||||
## 🚨 Ursprüngliche Probleme
|
||||
|
||||
### 1. **Inline Script Violations**
|
||||
|
||||
```
|
||||
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline'"
|
||||
```
|
||||
|
||||
### 2. **Connect-src Violations**
|
||||
|
||||
```
|
||||
Refused to connect to 'https://127.0.0.1/api/...' because it violates the document's Content Security Policy
|
||||
```
|
||||
|
||||
### 3. **PWA Icon Loading Errors**
|
||||
|
||||
```
|
||||
Error while trying to use the following icon from the Manifest: http://127.0.0.1:5000/static/icons/icon-144x144.png
|
||||
```
|
||||
|
||||
## ✅ Implementierte Lösungen
|
||||
|
||||
### 1. **CSP-Konfiguration optimiert** (`utils/security.py`)
|
||||
|
||||
#### Vor der Behebung:
|
||||
|
||||
- Restriktive CSP-Regeln blockierten lokale Entwicklung
|
||||
- Nonce-System verursachte Konflikte mit 'unsafe-inline'
|
||||
- Connect-src erlaubte keine lokalen API-Calls
|
||||
|
||||
#### Nach der Behebung:
|
||||
|
||||
```python
|
||||
CSP_POLICY = {
|
||||
'default-src': ["'self'"],
|
||||
'script-src': [
|
||||
"'self'",
|
||||
"'unsafe-inline'", # Für Entwicklung aktiviert
|
||||
"https://cdn.jsdelivr.net",
|
||||
"https://unpkg.com"
|
||||
],
|
||||
'connect-src': [
|
||||
"'self'",
|
||||
"ws:", "wss:", # WebSockets
|
||||
"http://localhost:*", # Lokale Entwicklung
|
||||
"http://127.0.0.1:*",
|
||||
"https://localhost:*",
|
||||
"https://127.0.0.1:*"
|
||||
],
|
||||
# ... weitere Regeln
|
||||
}
|
||||
```
|
||||
|
||||
#### Intelligente Nonce-Behandlung:
|
||||
|
||||
```python
|
||||
def build_csp_header(self, nonce=None, use_nonce=False):
|
||||
# In Entwicklung: use_nonce = False für 'unsafe-inline'
|
||||
# In Produktion: use_nonce = True für bessere Sicherheit
|
||||
```
|
||||
|
||||
### 2. **API-URL-Erkennung korrigiert** (`static/js/admin-guest-requests.js`)
|
||||
|
||||
#### Vor der Behebung:
|
||||
|
||||
```javascript
|
||||
function detectApiBaseUrl() {
|
||||
return 'https://127.0.0.1'; // CSP-Violation!
|
||||
}
|
||||
```
|
||||
|
||||
#### Nach der Behebung:
|
||||
|
||||
```javascript
|
||||
function detectApiBaseUrl() {
|
||||
// Für CSP-Kompatibilität immer relative URLs verwenden
|
||||
return ''; // Leerer String für relative URLs
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **PWA-Icons erstellt** (`static/icons/`)
|
||||
|
||||
#### Automatische Icon-Generierung:
|
||||
|
||||
```python
|
||||
# generate_icons.py
|
||||
def create_mercedes_icon(size, output_path):
|
||||
"""Erstellt Mercedes-Benz-Logo-Icons in verschiedenen Größen"""
|
||||
img = Image.new('RGB', (size, size), color='#000000')
|
||||
# Mercedes-Stern zeichnen
|
||||
# ... Icon-Generierung
|
||||
```
|
||||
|
||||
#### Generierte Icon-Größen:
|
||||
|
||||
- 72x72, 96x96, 128x128, 144x144
|
||||
- 152x152, 192x192, 384x384, 512x512
|
||||
- Apple Touch Icon, Favicons
|
||||
|
||||
### 4. **Event-Handler-System (CSP-konform)** (`static/js/event-handlers.js`)
|
||||
|
||||
#### Problem: Inline onclick-Handler
|
||||
|
||||
```html
|
||||
<!-- CSP-Problem -->
|
||||
<button onclick="refreshDashboard()">Aktualisieren</button>
|
||||
```
|
||||
|
||||
#### Lösung: Data-Action-Attribute
|
||||
|
||||
```html
|
||||
<!-- CSP-konform -->
|
||||
<button data-action="refresh-dashboard">Aktualisieren</button>
|
||||
```
|
||||
|
||||
#### Zentrale Event-Delegation:
|
||||
|
||||
```javascript
|
||||
class GlobalEventManager {
|
||||
handleClick(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
const action = target.getAttribute('data-action');
|
||||
this.executeAction(action, params, target);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. **CSP-Violation-Debugging** (`static/js/csp-violation-handler.js`)
|
||||
|
||||
#### Automatische CSP-Verletzungserkennung:
|
||||
|
||||
```javascript
|
||||
document.addEventListener('securitypolicyviolation', this.handleViolation.bind(this));
|
||||
```
|
||||
|
||||
#### Entwickler-Tools:
|
||||
|
||||
- **Debug-Panel**: `Ctrl+Shift+C` zum Anzeigen
|
||||
- **Konsolen-Befehle**: `cspHandler.getViolations()`
|
||||
- **Export-Funktion**: Verletzungen als JSON exportieren
|
||||
- **Lösungsvorschläge**: Automatische Fix-Empfehlungen
|
||||
|
||||
## 🔧 Migration bestehender onclick-Handler
|
||||
|
||||
### Schritt 1: Handler identifizieren
|
||||
|
||||
```bash
|
||||
grep -r "onclick=" templates/ --include="*.html"
|
||||
```
|
||||
|
||||
### Schritt 2: Data-Action-Attribute verwenden
|
||||
|
||||
```html
|
||||
<!-- Alt -->
|
||||
<button onclick="jobManager.startJob('123')">Start</button>
|
||||
|
||||
<!-- Neu -->
|
||||
<button data-action="start-job" data-action-id="123">Start</button>
|
||||
```
|
||||
|
||||
### Schritt 3: Funktionalität testen
|
||||
|
||||
```javascript
|
||||
// Event wird automatisch vom GlobalEventManager behandelt
|
||||
case 'start-job':
|
||||
if (typeof jobManager !== 'undefined' && params.id) {
|
||||
jobManager.startJob(params.id);
|
||||
}
|
||||
break;
|
||||
```
|
||||
|
||||
## 🚀 Verwendung der neuen Event-Handler
|
||||
|
||||
### Standard-Aktionen:
|
||||
|
||||
```html
|
||||
<!-- Navigation -->
|
||||
<button data-action="go-back">Zurück</button>
|
||||
<button data-action="reload-page">Neu laden</button>
|
||||
<button data-action="logout">Abmelden</button>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<button data-action="refresh-dashboard">Dashboard aktualisieren</button>
|
||||
|
||||
<!-- Jobs -->
|
||||
<button data-action="refresh-jobs">Jobs aktualisieren</button>
|
||||
<button data-action="toggle-batch-mode">Batch-Modus</button>
|
||||
<button data-action="start-job" data-action-id="123">Job starten</button>
|
||||
|
||||
<!-- Modals -->
|
||||
<button data-action="close-modal">Modal schließen</button>
|
||||
<button data-action="close-job-modal">Job-Modal schließen</button>
|
||||
|
||||
<!-- Formulare -->
|
||||
<button data-action="reset-form">Formular zurücksetzen</button>
|
||||
<button data-action="clear-file">Datei löschen</button>
|
||||
```
|
||||
|
||||
### Parametrisierte Aktionen:
|
||||
|
||||
```html
|
||||
<button data-action="edit-printer"
|
||||
data-action-id="printer-001">
|
||||
Drucker bearbeiten
|
||||
</button>
|
||||
|
||||
<button data-action="remove-element"
|
||||
data-action-selector=".notification">
|
||||
Benachrichtigung schließen
|
||||
</button>
|
||||
```
|
||||
|
||||
## 🔍 Debugging und Monitoring
|
||||
|
||||
### CSP-Debug-Panel aktivieren:
|
||||
|
||||
1. Öffne Entwicklertools (F12)
|
||||
2. Drücke `Ctrl+Shift+C` für CSP-Debug-Panel
|
||||
3. Oder verwende Konsolen-Befehle:
|
||||
|
||||
```javascript
|
||||
// Alle CSP-Verletzungen anzeigen
|
||||
cspHandler.getViolations()
|
||||
|
||||
// Statistiken abrufen
|
||||
cspHandler.getStats()
|
||||
|
||||
// Verletzungen exportieren
|
||||
cspHandler.exportViolations()
|
||||
|
||||
// Debug-Modus aktivieren
|
||||
cspHandler.enableDebugMode()
|
||||
```
|
||||
|
||||
### Konsolen-Ausgabe verstehen:
|
||||
|
||||
```
|
||||
🚨 CSP Violation detected
|
||||
Blocked URI: inline
|
||||
Violated Directive: script-src 'self' 'unsafe-inline'
|
||||
💡 Lösungsvorschlag: Script in externe .js-Datei auslagern
|
||||
```
|
||||
|
||||
## 📊 Leistungsverbesserungen
|
||||
|
||||
### Vor der CSP-Optimierung:
|
||||
|
||||
- ❌ Blockierte inline Scripts
|
||||
- ❌ Fehlerhafte API-Verbindungen
|
||||
- ❌ Fehlende PWA-Icons
|
||||
- ❌ Keine CSP-Violation-Behandlung
|
||||
|
||||
### Nach der CSP-Optimierung:
|
||||
|
||||
- ✅ Funktionale inline Scripts (Entwicklung)
|
||||
- ✅ Erfolgreiche API-Verbindungen
|
||||
- ✅ Vollständige PWA-Unterstützung
|
||||
- ✅ Proaktive CSP-Debugging-Tools
|
||||
- ✅ Produktionsbereite Sicherheitskonfiguration
|
||||
|
||||
## 🛠️ Wartung und Updates
|
||||
|
||||
### CSP-Regeln für neue Features hinzufügen:
|
||||
|
||||
1. Öffne `utils/security.py`
|
||||
2. Erweitere `CSP_POLICY` entsprechend
|
||||
3. Teste mit CSP-Debug-Tools
|
||||
4. Dokumentiere Änderungen
|
||||
|
||||
### Neue Event-Handler hinzufügen:
|
||||
|
||||
1. Öffne `static/js/event-handlers.js`
|
||||
2. Ergänze `executeAction()` switch-case
|
||||
3. Teste mit data-action-Attributen
|
||||
4. Dokumentiere neue Aktionen
|
||||
|
||||
## ⚠️ Wichtige Hinweise
|
||||
|
||||
### Entwicklung vs. Produktion:
|
||||
|
||||
- **Entwicklung**: `use_nonce = False` für 'unsafe-inline'
|
||||
- **Produktion**: `use_nonce = True` für bessere Sicherheit
|
||||
|
||||
### Browser-Kompatibilität:
|
||||
|
||||
- CSP-Violation-Handler funktioniert in modernen Browsern
|
||||
- Fallback für ältere Browser vorhanden
|
||||
- PWA-Icons sind für alle Geräte optimiert
|
||||
|
||||
### Performance:
|
||||
|
||||
- Event-Delegation reduziert Memory-Usage
|
||||
- Zentrale Event-Handler verbessern Maintainability
|
||||
- CSP-Debugging nur in Entwicklung aktiv
|
||||
|
||||
## 🎯 Nächste Schritte
|
||||
|
||||
1. **Migration aller onclick-Handler**: Systematisches Ersetzen durch data-action
|
||||
2. **CSP-Reporting**: Server-seitige Violation-Protokollierung implementieren
|
||||
3. **Nonce-System**: Für Produktion aktivieren
|
||||
4. **Performance-Monitoring**: CSP-Impact messen
|
||||
|
||||
## 📖 Zusätzliche Ressourcen
|
||||
|
||||
- [MDN CSP Guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
|
||||
- [CSP Evaluator](https://csp-evaluator.withgoogle.com/)
|
||||
- [PWA Best Practices](https://web.dev/pwa-checklist/)
|
||||
|
||||
---
|
||||
|
||||
**Dokumentation erstellt**: 29.05.2025
|
||||
**Letzte Aktualisierung**: 29.05.2025
|
||||
**Version**: 1.0.0
|
@ -1 +0,0 @@
|
||||
|
@ -1,175 +0,0 @@
|
||||
# MYP Platform - Requirements Verwaltung
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die MYP Platform verwendet verschiedene Requirements-Dateien für unterschiedliche Umgebungen:
|
||||
|
||||
- `requirements.txt` - **Basis-Abhängigkeiten basierend auf tatsächlich verwendeten Imports in app.py**
|
||||
- `requirements-dev.txt` - Entwicklungsabhängigkeiten (Testing, Debugging, etc.)
|
||||
- `requirements-prod.txt` - Produktionsabhängigkeiten (optimiert für Performance)
|
||||
|
||||
## Installation
|
||||
|
||||
### Basis-Installation (Empfohlen)
|
||||
|
||||
```bash
|
||||
# Nur die wirklich benötigten Abhängigkeiten installieren
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Entwicklungsumgebung
|
||||
|
||||
```bash
|
||||
# Alle Entwicklungsabhängigkeiten installieren
|
||||
pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
### Produktionsumgebung
|
||||
|
||||
```bash
|
||||
# Produktionsabhängigkeiten installieren
|
||||
pip install -r requirements-prod.txt
|
||||
```
|
||||
|
||||
## Aktualisierte Requirements (Januar 2025)
|
||||
|
||||
### Basis-Requirements (requirements.txt)
|
||||
**Basierend auf tatsächlich verwendeten Imports in app.py:**
|
||||
|
||||
#### Core Flask Framework
|
||||
- **Flask 3.1.1** - Neueste stabile Version
|
||||
- **Flask-Login 0.6.3** - Benutzer-Authentifizierung
|
||||
- **Flask-WTF 1.2.1** - CSRF-Schutz und Formulare
|
||||
|
||||
#### Datenbank
|
||||
- **SQLAlchemy 2.0.36** - ORM für Datenbankoperationen
|
||||
|
||||
#### Sicherheit
|
||||
- **Werkzeug 3.1.3** - WSGI-Utilities und Passwort-Hashing
|
||||
- **bcrypt 4.2.1** - Passwort-Hashing
|
||||
- **cryptography 44.0.0** - SSL-Zertifikate
|
||||
|
||||
#### Smart Plug Steuerung
|
||||
- **PyP100 0.1.2** - TP-Link Tapo Smart Plugs (neueste verfügbare Version)
|
||||
|
||||
#### System & Monitoring
|
||||
- **psutil 6.1.1** - System-Monitoring
|
||||
- **redis 5.2.1** - Rate Limiting und Caching
|
||||
- **requests 2.32.3** - HTTP-Anfragen
|
||||
|
||||
#### Template Engine
|
||||
- **Jinja2 3.1.5** - Template-Rendering
|
||||
- **MarkupSafe 3.0.2** - Sichere String-Verarbeitung
|
||||
- **itsdangerous 2.2.0** - Sichere Daten-Serialisierung
|
||||
|
||||
#### Zusätzliche Core-Abhängigkeiten
|
||||
- **click 8.1.8** - CLI-Kommandos
|
||||
- **blinker 1.9.0** - Flask-Signaling
|
||||
|
||||
#### Windows-spezifisch
|
||||
- **pywin32 308** - Windows-APIs (nur auf Windows)
|
||||
|
||||
## Kompatibilität
|
||||
|
||||
### Python-Versionen
|
||||
- **Mindestversion**: Python 3.9
|
||||
- **Empfohlen**: Python 3.11 oder 3.12
|
||||
- **Getestet mit**: Python 3.13.3
|
||||
|
||||
### Betriebssysteme
|
||||
- **Windows**: Vollständig unterstützt (mit pywin32)
|
||||
- **Linux**: Vollständig unterstützt
|
||||
- **macOS**: Vollständig unterstützt
|
||||
|
||||
## Wichtige Änderungen
|
||||
|
||||
### Was wurde entfernt
|
||||
- **Nicht verwendete Pakete**: pandas, numpy, orjson, ujson, structlog
|
||||
- **Entwicklungstools**: black, flake8, mypy (verschoben nach requirements-dev.txt)
|
||||
- **Optionale Pakete**: watchdog, python-dotenv, sphinx
|
||||
|
||||
### Was wurde beibehalten
|
||||
- **Nur tatsächlich verwendete Imports** aus app.py und Core-Modulen
|
||||
- **Kritische Sicherheitspakete** für Produktionsbetrieb
|
||||
- **Windows-Kompatibilität** für die Zielumgebung
|
||||
|
||||
## Installation mit Extras
|
||||
|
||||
### Entwicklungstools installieren
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pip install pytest==8.3.4 pytest-cov==6.0.0
|
||||
```
|
||||
|
||||
### Produktionsserver installieren
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pip install gunicorn==23.0.0
|
||||
```
|
||||
|
||||
## Wartung
|
||||
|
||||
### Requirements prüfen
|
||||
```bash
|
||||
# Installierte Pakete anzeigen
|
||||
pip list
|
||||
|
||||
# Veraltete Pakete prüfen
|
||||
pip list --outdated
|
||||
|
||||
# Abhängigkeitsbaum anzeigen
|
||||
pip show --verbose Flask
|
||||
```
|
||||
|
||||
### Sicherheitsupdates
|
||||
```bash
|
||||
# Sicherheitslücken prüfen (falls safety installiert)
|
||||
pip install safety
|
||||
safety check
|
||||
|
||||
# Alle Pakete aktualisieren
|
||||
pip install --upgrade -r requirements.txt
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Häufige Probleme
|
||||
|
||||
1. **PyP100 Installation**
|
||||
- Version 0.1.2 ist die neueste verfügbare Version
|
||||
- Bei Problemen: `pip install --no-deps PyP100==0.1.2`
|
||||
|
||||
2. **Dependency-Konflikte**
|
||||
- flask-caching erfordert Flask<3, aber Flask 3.1.1 ist installiert
|
||||
- Lösung: `pip install flask-caching --upgrade` oder entfernen
|
||||
|
||||
3. **Windows pywin32**
|
||||
- Wird automatisch nur auf Windows installiert
|
||||
- Bei Problemen: `pip install --force-reinstall pywin32`
|
||||
|
||||
4. **PATH-Warnungen**
|
||||
- Scripts werden in User-Verzeichnis installiert
|
||||
- Lösung: `--no-warn-script-location` verwenden
|
||||
|
||||
### Performance-Tipps
|
||||
|
||||
1. **Schnellere Installation**
|
||||
```bash
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt --no-deps
|
||||
```
|
||||
|
||||
2. **Cache nutzen**
|
||||
```bash
|
||||
pip install --cache-dir ~/.pip/cache -r requirements.txt
|
||||
```
|
||||
|
||||
## Lizenz-Informationen
|
||||
|
||||
Alle verwendeten Pakete sind mit der MIT/BSD/Apache-Lizenz kompatibel.
|
||||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung**: Januar 2025
|
||||
**Basis-Requirements**: Nur tatsächlich verwendete Imports
|
||||
**Nächste Überprüfung**: April 2025
|
@ -1,173 +0,0 @@
|
||||
# MYP Platform - Entwicklungs-Roadmap
|
||||
|
||||
Dieses Dokument beschreibt die geplanten Entwicklungsschritte und zukünftigen Features für das MYP 3D-Drucker Reservierungssystem.
|
||||
|
||||
## Aktuelle Version: 1.1
|
||||
|
||||
Die aktuelle Version umfasst die Grundfunktionalitäten:
|
||||
|
||||
- Benutzerauthentifizierung und -verwaltung
|
||||
- Druckerverwaltung
|
||||
- Job-Scheduling und -Überwachung
|
||||
- Smart Plug Integration
|
||||
- **✅ Vollständiges UI-Komponenten-System mit Tailwind CSS**
|
||||
- **✅ Template-Helper für einfache UI-Entwicklung**
|
||||
- **✅ JavaScript-Utilities für interaktive Komponenten**
|
||||
- **✅ Dark Mode Support**
|
||||
- **✅ Responsive Design**
|
||||
- **✅ Umfassende UI-Dokumentation**
|
||||
|
||||
## Kürzlich Abgeschlossen (Version 1.2) ✅
|
||||
|
||||
### Sicherheits-Features ✅
|
||||
|
||||
- ✅ **Rate Limiting**: Schutz vor API-Missbrauch und DDoS-Attacken
|
||||
- ✅ **Content Security Policy (CSP)**: Schutz vor XSS-Angriffen
|
||||
- ✅ **Erweiterte Security Headers**: Comprehensive security headers für alle Responses
|
||||
- ✅ **Verdächtige Aktivitäts-Erkennung**: Automatische Erkennung von SQL-Injection und anderen Bedrohungen
|
||||
- ✅ **Client-Fingerprinting**: Erweiterte Sicherheit durch Client-Identifikation
|
||||
|
||||
### Erweiterte Berechtigungen ✅
|
||||
|
||||
- ✅ **Granulare Berechtigungen**: 7 detaillierte Rollen (Guest bis Super Admin)
|
||||
- ✅ **Ressourcen-spezifische Zugriffskontrolle**: Job-, Drucker- und Benutzer-spezifische Berechtigungen
|
||||
- ✅ **Temporäre Berechtigungen**: Zeitlich begrenzte Berechtigungsüberschreibungen
|
||||
- ✅ **Permission Caching**: Performance-optimierte Berechtigungsprüfung
|
||||
- ✅ **Template-Integration**: Template-Helper für berechtigungsbasierte UI-Anzeige
|
||||
|
||||
### Erweiterte UI-Komponenten ✅
|
||||
|
||||
- ✅ **Progress-Bars**: Animierte, konfigurabr progress indicators mit verschiedenen Styles
|
||||
- ✅ **Advanced File-Upload**: Drag & Drop, Preview, Chunk-Upload, Validierung
|
||||
- ✅ **DatePicker**: Deutscher Kalender mit Validierung und Custom Events
|
||||
- ✅ **Auto-Initialisierung**: Data-Attribute-basierte Komponenten-Initialisierung
|
||||
|
||||
### Analytics & Statistiken ✅
|
||||
|
||||
- ✅ **Umfassende Analytics-Engine**: Drucker-, Job- und Benutzer-Statistiken
|
||||
- ✅ **KPI-Dashboard**: Key Performance Indicators mit Trend-Analyse
|
||||
- ✅ **Report-Generierung**: Verschiedene Report-Typen und Zeiträume
|
||||
- ✅ **Interaktive Charts**: Chart.js-basierte Visualisierungen
|
||||
- ✅ **Export-Funktionalität**: JSON, CSV, PDF, Excel-Export (Framework bereit)
|
||||
|
||||
## Geplante Features
|
||||
|
||||
### Version 1.3 (Kurzfristig)
|
||||
|
||||
- [ ] **E-Mail-Benachrichtigungen**: Bei Job-Status-Änderungen und System-Events
|
||||
- [ ] **Erweiterte Formular-Validierung**: Client- und serverseitige Validierung mit UI-Feedback
|
||||
- [ ] **Multi-Format-Export**: Vollständige PDF- und Excel-Report-Generierung
|
||||
- [ ] **Zwei-Faktor-Authentifizierung**: TOTP-basierte 2FA-Implementierung
|
||||
|
||||
### Version 1.3 (Mittelfristig)
|
||||
|
||||
- [ ] Druckerprofile mit spezifischen Eigenschaften (Druckvolumen, Materialien, etc.)
|
||||
- [ ] Materialverwaltung und -tracking
|
||||
- [ ] Verbessertes Dashboard mit Echtzeit-Updates
|
||||
- [ ] **HTMX-Integration**: Für bessere Interaktivität ohne JavaScript-Framework
|
||||
- [ ] **Drag & Drop**: Für Job-Reihenfolge und Datei-Uploads
|
||||
- [ ] **Erweiterte Tabellen**: Sortierung, Filterung, Pagination
|
||||
|
||||
### Version 2.0 (Langfristig)
|
||||
|
||||
- [ ] OctoPrint Integration für direkte Druckersteuerung
|
||||
- [ ] Mobile App mit Push-Benachrichtigungen
|
||||
- [ ] Wartungsplanung und -tracking
|
||||
- [ ] Multi-Standort-Unterstützung
|
||||
- [ ] **Progressive Web App (PWA)**: Offline-Funktionalität und App-Installation
|
||||
- [ ] **Erweiterte Themes**: Anpassbare Farbschemata und Layouts
|
||||
|
||||
## Technische Verbesserungen
|
||||
|
||||
### Backend
|
||||
|
||||
- [ ] Refactoring für verbesserte Modularität
|
||||
- [ ] REST API Dokumentation mit Swagger/OpenAPI
|
||||
- [ ] Verbesserte Testabdeckung
|
||||
- [ ] Migration zu SQLAlchemy 2.0
|
||||
- [ ] **WebSocket-Integration**: Für Echtzeit-Updates
|
||||
|
||||
### Frontend
|
||||
|
||||
- [X] ~~Optimierung der Benutzeroberfläche~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~UI-Komponenten-System~~ ✅ **Abgeschlossen**
|
||||
- [ ] **HTMX-Integration**: Für bessere Interaktivität ohne komplexe JavaScript-Frameworks
|
||||
- [ ] **Progressive Web App (PWA)**: Funktionalität für App-ähnliche Erfahrung
|
||||
- [ ] **Barrierefreiheit**: Nach WCAG-Richtlinien
|
||||
- [ ] **Performance-Optimierung**: Lazy Loading, Code Splitting
|
||||
|
||||
### CSS/Styling
|
||||
|
||||
- [X] ~~Tailwind CSS Integration~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~PostCSS Build-Pipeline~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~Dark Mode Support~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~Responsive Design~~ ✅ **Abgeschlossen**
|
||||
- [ ] **CSS-Optimierung**: Purging ungenutzter Styles, Critical CSS
|
||||
- [ ] **Animation-System**: Micro-Interactions und Übergänge
|
||||
|
||||
## Leistung und Skalierung
|
||||
|
||||
- [ ] Optimierung der Datenbankabfragen
|
||||
- [ ] Caching-Strategie implementieren
|
||||
- [ ] Asynchrone Verarbeitung für zeitintensive Aufgaben
|
||||
- [ ] Docker-Container für einfache Bereitstellung
|
||||
- [ ] **CDN-Integration**: Für statische Assets
|
||||
- [ ] **Service Worker**: Für Offline-Funktionalität
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- [ ] Security Audit durchführen
|
||||
- [ ] Implementierung von CSRF-Schutz
|
||||
- [ ] Rate Limiting für API-Endpunkte
|
||||
- [ ] Zwei-Faktor-Authentifizierung
|
||||
- [ ] **Content Security Policy (CSP)**: Schutz vor XSS-Angriffen
|
||||
|
||||
## Entwickler-Erfahrung
|
||||
|
||||
### Dokumentation ✅
|
||||
|
||||
- [X] ~~UI-Komponenten-Dokumentation~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~Tailwind CSS Setup-Guide~~ ✅ **Abgeschlossen**
|
||||
- [ ] API-Dokumentation mit Swagger
|
||||
- [ ] Entwickler-Handbuch
|
||||
- [ ] Deployment-Guide
|
||||
|
||||
### Tooling
|
||||
|
||||
- [X] ~~PostCSS Build-System~~ ✅ **Abgeschlossen**
|
||||
- [X] ~~NPM Scripts für Development~~ ✅ **Abgeschlossen**
|
||||
- [ ] **Hot Reload**: Für CSS und Templates
|
||||
- [ ] **Linting**: ESLint, Prettier, Flake8
|
||||
- [ ] **Testing**: Unit Tests, Integration Tests, E2E Tests
|
||||
|
||||
## Community und Beiträge
|
||||
|
||||
Wir freuen uns über Beiträge und Feedback zu dieser Roadmap. Wenn Sie Vorschläge haben oder an der Entwicklung teilnehmen möchten, erstellen Sie bitte einen Issue oder Pull Request im Repository.
|
||||
|
||||
### Aktuelle Prioritäten für Beiträge
|
||||
|
||||
1. **Testing**: Unit Tests für UI-Komponenten
|
||||
2. **Accessibility**: WCAG-konforme Verbesserungen
|
||||
3. **Performance**: Optimierung der CSS-Größe
|
||||
4. **Dokumentation**: Übersetzungen und Beispiele
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.1 (Dezember 2024)
|
||||
|
||||
- ✅ Vollständiges UI-Komponenten-System implementiert
|
||||
- ✅ Template-Helper für alle gängigen UI-Elemente
|
||||
- ✅ JavaScript-Utilities für interaktive Funktionen
|
||||
- ✅ PostCSS Build-Pipeline mit Tailwind CSS
|
||||
- ✅ Umfassende Dokumentation erstellt
|
||||
- ✅ Demo-Seite für alle Komponenten
|
||||
|
||||
### Version 1.0 (Juni 2023)
|
||||
|
||||
- ✅ Grundfunktionalitäten implementiert
|
||||
- ✅ Basis-UI mit Tailwind CSS
|
||||
- ✅ Dark Mode Support
|
||||
|
||||
---
|
||||
|
||||
*Zuletzt aktualisiert: Dezember 2024*
|
@ -1,211 +0,0 @@
|
||||
# Tailwind CSS Konfiguration für MYP Platform
|
||||
|
||||
Dieses Dokument beschreibt die aktuelle Tailwind CSS Konfiguration und Build-Prozesse für die MYP Platform.
|
||||
|
||||
## Struktur
|
||||
|
||||
Die CSS-Assets des Projekts sind wie folgt strukturiert:
|
||||
|
||||
```
|
||||
static/
|
||||
css/
|
||||
input.css # Tailwind Direktiven & benutzerdefinierte Styles
|
||||
components.css # Wiederverwendbare UI-Komponenten
|
||||
tailwind.min.css # Kompiliertes Light-Mode CSS (minimiert)
|
||||
tailwind-dark.min.css # Kompiliertes Dark-Mode CSS (minimiert)
|
||||
```
|
||||
|
||||
## Tailwind Konfiguration
|
||||
|
||||
Die Konfiguration wird durch die Datei `tailwind.config.js` im Hauptverzeichnis des Projekts definiert. Diese enthält:
|
||||
|
||||
- Farbpalette mit Mercedes-Benz Farben
|
||||
- Erweiterungen für Schriften, Schatten und Animation
|
||||
- Dark-Mode-Konfiguration über die Klasse `dark`
|
||||
|
||||
## Build-Befehle
|
||||
|
||||
### Light Mode CSS (Standardtheme)
|
||||
|
||||
Um das Standard-Light-Mode CSS zu erstellen:
|
||||
|
||||
```bash
|
||||
npx tailwindcss -i ./static/css/input.css -o ./static/css/tailwind.min.css --minify
|
||||
```
|
||||
|
||||
### Dark Mode CSS
|
||||
|
||||
Um das Dark-Mode CSS zu erstellen:
|
||||
|
||||
```bash
|
||||
npx tailwindcss -i ./static/css/input.css -o ./static/css/tailwind-dark.min.css --minify --dark-mode 'class'
|
||||
```
|
||||
|
||||
## Verwendung im Projekt
|
||||
|
||||
Beide CSS-Dateien werden im `<head>` der Seite eingebunden:
|
||||
|
||||
```html
|
||||
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/tailwind-dark.min.css') }}" rel="stylesheet">
|
||||
```
|
||||
|
||||
## Dark Mode Funktionalität
|
||||
|
||||
Der Dark Mode wird durch JavaScript in `ui-components.js` gesteuert, welches:
|
||||
|
||||
1. Benutzereinstellungen in `localStorage` speichert
|
||||
2. Systemeinstellungen beobachtet (via `prefers-color-scheme`)
|
||||
3. Eine `dark`-Klasse zum `<html>`-Element hinzufügt/entfernt
|
||||
4. Ein Tastaturkürzel (Strg+Shift+D) zum Umschalten bereitstellt
|
||||
|
||||
## Entwicklung
|
||||
|
||||
Bei der Entwicklung neuer Komponenten:
|
||||
|
||||
1. Füge Tailwind-Klassen direkt zu HTML-Elementen hinzu
|
||||
2. Für wiederverwendbare Komponenten, nutze die `@apply`-Direktive in `components.css`
|
||||
3. Nach Änderungen müssen beide CSS-Dateien neu gebaut werden
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Node.js und npm müssen installiert sein
|
||||
- Python 3.11 und Flask müssen installiert sein
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
Die Tailwind-Integration besteht aus folgenden Komponenten:
|
||||
|
||||
```
|
||||
app/
|
||||
├─ static/
|
||||
│ ├─ css/
|
||||
│ │ ├─ input.css # Quelldatei für Tailwind
|
||||
│ │ ├─ tailwind-dark-consolidated.min.css # Generierte CSS-Datei
|
||||
├─ templates/ # HTML-Templates mit Tailwind-Klassen
|
||||
├─ package.json # NPM-Konfiguration
|
||||
├─ tailwind.config.js # Tailwind-Konfiguration
|
||||
├─ postcss.config.js # PostCSS-Konfiguration
|
||||
```
|
||||
|
||||
## Einrichtung
|
||||
|
||||
Das Projekt verwendet Tailwind CSS für das Frontend-Styling. Die Einrichtung wurde bereits abgeschlossen und umfasst:
|
||||
|
||||
1. **NPM-Abhängigkeiten** in der `package.json`
|
||||
2. **Tailwind-Konfiguration** in `tailwind.config.js`
|
||||
3. **PostCSS-Konfiguration** in `postcss.config.js`
|
||||
4. **CSS-Eingabedatei** in `static/css/input.css`
|
||||
5. **Generierte CSS-Datei** in `static/css/tailwind-dark-consolidated.min.css`
|
||||
|
||||
## Verwendung im Entwicklungsprozess
|
||||
|
||||
### CSS-Generierung
|
||||
|
||||
Um an den Styles zu arbeiten, verwenden Sie die folgenden Befehle:
|
||||
|
||||
1. **Abhängigkeiten installieren** (falls noch nicht geschehen):
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **CSS überwachen und automatisch neu generieren**:
|
||||
```bash
|
||||
npm run watch:css
|
||||
```
|
||||
Dieser Befehl startet einen Watcher, der Änderungen an HTML-Templates und der input.css überwacht und automatisch die CSS-Datei neu generiert.
|
||||
|
||||
3. **CSS für Produktion erstellen**:
|
||||
```bash
|
||||
npm run build:css
|
||||
```
|
||||
Dieser Befehl erstellt eine minifizierte CSS-Datei für den Produktionseinsatz.
|
||||
|
||||
### Workflow
|
||||
|
||||
Der empfohlene Workflow für die Arbeit mit Tailwind CSS ist:
|
||||
|
||||
1. Starten Sie den Watcher mit `npm run watch:css`
|
||||
2. Bearbeiten Sie die Template-Dateien in `templates/` oder fügen Sie eigene Komponenten in `static/css/input.css` hinzu
|
||||
3. Die Änderungen werden automatisch in die generierte CSS-Datei übernommen
|
||||
4. Führen Sie den Flask-Server mit `python3.11 app.py` aus, um die Änderungen zu sehen
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Die Anwendung unterstützt einen Dark Mode. In den Templates wird dies folgendermaßen implementiert:
|
||||
|
||||
```html
|
||||
<html class="light" data-theme="light">
|
||||
<!-- oder für Dark Mode: -->
|
||||
<html class="dark" data-theme="dark">
|
||||
```
|
||||
|
||||
In Tailwind-Klassen verwenden Sie das `dark:` Präfix:
|
||||
|
||||
```html
|
||||
<div class="bg-white text-gray-800 dark:bg-gray-800 dark:text-white">
|
||||
Dieser Text passt sich dem Theme an
|
||||
</div>
|
||||
```
|
||||
|
||||
## Anpassungen
|
||||
|
||||
### Eigene Komponenten hinzufügen
|
||||
|
||||
Um eigene Komponenten zu definieren, verwenden Sie das `@layer components` Konstrukt in der `static/css/input.css` Datei:
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.custom-button {
|
||||
@apply bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind-Konfiguration anpassen
|
||||
|
||||
Die Tailwind-Konfiguration wird in der `tailwind.config.js` Datei vorgenommen. Hier können Sie:
|
||||
|
||||
- Farben anpassen
|
||||
- Eigene Abstände definieren
|
||||
- Schriftarten konfigurieren
|
||||
- Plugins hinzufügen
|
||||
- Theme-Einstellungen vornehmen
|
||||
|
||||
## Optimierung für Produktion
|
||||
|
||||
Für die Produktionsumgebung sollte die CSS-Datei optimiert werden:
|
||||
|
||||
```bash
|
||||
npm run build:css
|
||||
```
|
||||
|
||||
Dies erstellt eine minifizierte Version der CSS-Datei mit PurgeCSS, das ungenutzte CSS-Klassen entfernt.
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Problem: CSS wird nicht aktualisiert
|
||||
|
||||
1. Stellen Sie sicher, dass der Watcher läuft (`npm run watch:css`)
|
||||
2. Überprüfen Sie, ob Ihre Änderungen in einem Pfad sind, der in `tailwind.config.js` unter `content` definiert ist
|
||||
3. Leeren Sie den Browser-Cache (`Ctrl+F5`)
|
||||
|
||||
### Problem: Node-Module nicht gefunden
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Problem: Tailwind-Befehle funktionieren nicht
|
||||
|
||||
Verwenden Sie `npx` vor dem Befehl:
|
||||
|
||||
```bash
|
||||
npx tailwindcss -i ./static/css/input.css -o ./static/css/tailwind-dark-consolidated.min.css --minify
|
||||
```
|
||||
|
||||
## Ressourcen
|
||||
|
||||
- [Offizielle Tailwind-Dokumentation](https://tailwindcss.com/docs)
|
||||
- [Tailwind mit Flask einrichten](https://testdriven.io/blog/flask-htmx-tailwind/)
|
||||
- [Dark Mode mit Tailwind CSS](https://tailwindcss.com/docs/dark-mode)
|
@ -1,421 +0,0 @@
|
||||
# UI-Komponenten Dokumentation - MYP Platform
|
||||
|
||||
Diese Dokumentation beschreibt alle verfügbaren UI-Komponenten und Template-Helper der MYP Platform.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die MYP Platform bietet ein umfassendes UI-Komponenten-System basierend auf:
|
||||
- **Tailwind CSS** für das Styling
|
||||
- **Jinja2 Template-Helper** für einfache Verwendung in Templates
|
||||
- **JavaScript-Utilities** für interaktive Funktionen
|
||||
|
||||
## Template-Helper
|
||||
|
||||
### Buttons
|
||||
|
||||
#### `ui_button(text, type, size, classes, icon, onclick, disabled, **attrs)`
|
||||
|
||||
Erstellt styled Buttons mit verschiedenen Varianten.
|
||||
|
||||
**Parameter:**
|
||||
- `text` (str): Button-Text
|
||||
- `type` (str): Button-Typ - `"primary"`, `"secondary"`, `"danger"`, `"success"`
|
||||
- `size` (str): Button-Größe - `"sm"`, `"md"`, `"lg"`
|
||||
- `classes` (str): Zusätzliche CSS-Klassen
|
||||
- `icon` (str): SVG-Icon-Code
|
||||
- `onclick` (str): JavaScript-Code für onclick
|
||||
- `disabled` (bool): Button deaktiviert
|
||||
- `**attrs`: Zusätzliche HTML-Attribute
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
{{ ui_button("Speichern", "primary", "md") }}
|
||||
{{ ui_button("Löschen", "danger", "sm", icon=icons.trash) }}
|
||||
{{ ui_button("Bearbeiten", "secondary", onclick="editItem(123)") }}
|
||||
{{ ui_button("Deaktiviert", "primary", disabled=true) }}
|
||||
```
|
||||
|
||||
### Badges
|
||||
|
||||
#### `ui_badge(text, type, classes)`
|
||||
|
||||
Erstellt kleine Label/Tags für Status-Anzeigen.
|
||||
|
||||
**Parameter:**
|
||||
- `text` (str): Badge-Text
|
||||
- `type` (str): Badge-Typ - `"blue"`, `"green"`, `"red"`, `"yellow"`, `"purple"`
|
||||
- `classes` (str): Zusätzliche CSS-Klassen
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
{{ ui_badge("Neu", "blue") }}
|
||||
{{ ui_badge("Aktiv", "green") }}
|
||||
{{ ui_badge("Fehler", "red") }}
|
||||
```
|
||||
|
||||
#### `ui_status_badge(status, type)`
|
||||
|
||||
Erstellt spezielle Status-Badges für Jobs und Drucker.
|
||||
|
||||
**Parameter:**
|
||||
- `status` (str): Status-Wert
|
||||
- `type` (str): Typ - `"job"` oder `"printer"`
|
||||
|
||||
**Job-Status:**
|
||||
- `queued` → "In Warteschlange"
|
||||
- `printing` → "Wird gedruckt"
|
||||
- `completed` → "Abgeschlossen"
|
||||
- `failed` → "Fehlgeschlagen"
|
||||
- `cancelled` → "Abgebrochen"
|
||||
- `paused` → "Pausiert"
|
||||
|
||||
**Drucker-Status:**
|
||||
- `ready` → "Bereit"
|
||||
- `busy` → "Beschäftigt"
|
||||
- `error` → "Fehler"
|
||||
- `offline` → "Offline"
|
||||
- `maintenance` → "Wartung"
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
{{ ui_status_badge("printing", "job") }}
|
||||
{{ ui_status_badge("ready", "printer") }}
|
||||
```
|
||||
|
||||
### Cards
|
||||
|
||||
#### `ui_card(title, content, footer, classes, hover)`
|
||||
|
||||
Erstellt Container-Karten für Inhalte.
|
||||
|
||||
**Parameter:**
|
||||
- `title` (str): Karten-Titel
|
||||
- `content` (str): Karten-Inhalt (HTML möglich)
|
||||
- `footer` (str): Karten-Footer (HTML möglich)
|
||||
- `classes` (str): Zusätzliche CSS-Klassen
|
||||
- `hover` (bool): Hover-Effekt aktivieren
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
{{ ui_card(
|
||||
title="Drucker Status",
|
||||
content="<p>Ultimaker S3 ist bereit</p>",
|
||||
footer=ui_button("Details", "primary", "sm"),
|
||||
hover=true
|
||||
) }}
|
||||
```
|
||||
|
||||
### Alerts
|
||||
|
||||
#### `ui_alert(message, type, dismissible)`
|
||||
|
||||
Erstellt Benachrichtigungen und Warnungen.
|
||||
|
||||
**Parameter:**
|
||||
- `message` (str): Alert-Nachricht
|
||||
- `type` (str): Alert-Typ - `"info"`, `"success"`, `"warning"`, `"error"`
|
||||
- `dismissible` (bool): Schließbar machen
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
{{ ui_alert("Job erfolgreich erstellt!", "success", dismissible=true) }}
|
||||
{{ ui_alert("Warnung: Drucker offline", "warning") }}
|
||||
```
|
||||
|
||||
### Modals
|
||||
|
||||
#### `ui_modal(modal_id, title, content, footer, size)`
|
||||
|
||||
Erstellt Modal-Dialoge.
|
||||
|
||||
**Parameter:**
|
||||
- `modal_id` (str): Eindeutige Modal-ID
|
||||
- `title` (str): Modal-Titel
|
||||
- `content` (str): Modal-Inhalt (HTML möglich)
|
||||
- `footer` (str): Modal-Footer (HTML möglich)
|
||||
- `size` (str): Modal-Größe - `"sm"`, `"md"`, `"lg"`, `"xl"`
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
{{ ui_modal(
|
||||
"confirm-delete",
|
||||
"Löschen bestätigen",
|
||||
"<p>Möchten Sie diesen Job wirklich löschen?</p>",
|
||||
ui_button("Abbrechen", "secondary", onclick="MYP.Modal.close('confirm-delete')") + " " + ui_button("Löschen", "danger"),
|
||||
"sm"
|
||||
) }}
|
||||
```
|
||||
|
||||
### Tabellen
|
||||
|
||||
#### `ui_table(headers, rows, classes, striped)`
|
||||
|
||||
Erstellt styled Tabellen.
|
||||
|
||||
**Parameter:**
|
||||
- `headers` (List[str]): Tabellen-Kopfzeilen
|
||||
- `rows` (List[List[str]]): Tabellen-Zeilen
|
||||
- `classes` (str): Zusätzliche CSS-Klassen
|
||||
- `striped` (bool): Zebra-Streifen aktivieren
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
{% set headers = ["Name", "Status", "Aktionen"] %}
|
||||
{% set rows = [
|
||||
["Job 1", ui_status_badge("printing", "job"), ui_button("Details", "secondary", "sm")],
|
||||
["Job 2", ui_status_badge("queued", "job"), ui_button("Details", "secondary", "sm")]
|
||||
] %}
|
||||
{{ ui_table(headers, rows, striped=true) }}
|
||||
```
|
||||
|
||||
## Template-Filter
|
||||
|
||||
### Datum & Zeit
|
||||
|
||||
#### `german_datetime`
|
||||
|
||||
Formatiert Datetime-Objekte für deutsche Anzeige.
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
{{ job.created_at|german_datetime }} <!-- 15.03.2024 14:30 -->
|
||||
{{ job.created_at|german_datetime("%d.%m.%Y") }} <!-- 15.03.2024 -->
|
||||
{{ job.created_at|german_datetime("%H:%M") }} <!-- 14:30 -->
|
||||
```
|
||||
|
||||
#### `duration`
|
||||
|
||||
Formatiert Dauer in Minuten zu lesbarem Format.
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
{{ 30|duration }} <!-- 30 Min -->
|
||||
{{ 90|duration }} <!-- 1 Std 30 Min -->
|
||||
{{ 120|duration }} <!-- 2 Std -->
|
||||
```
|
||||
|
||||
#### `json`
|
||||
|
||||
Enkodiert Python-Daten als JSON für JavaScript.
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
<script>
|
||||
const jobData = {{ job_dict|json|safe }};
|
||||
</script>
|
||||
```
|
||||
|
||||
## Globale Variablen
|
||||
|
||||
### Icons
|
||||
|
||||
Verfügbare SVG-Icons über `icons.*`:
|
||||
|
||||
- `icons.check` - Häkchen
|
||||
- `icons.x` - X/Schließen
|
||||
- `icons.plus` - Plus/Hinzufügen
|
||||
- `icons.edit` - Bearbeiten
|
||||
- `icons.trash` - Löschen
|
||||
- `icons.printer` - Drucker
|
||||
- `icons.dashboard` - Dashboard
|
||||
|
||||
**Beispiele:**
|
||||
```jinja2
|
||||
{{ ui_button("Hinzufügen", "primary", icon=icons.plus) }}
|
||||
{{ ui_button("Löschen", "danger", icon=icons.trash) }}
|
||||
```
|
||||
|
||||
### Weitere Globale Variablen
|
||||
|
||||
- `current_year` - Aktuelles Jahr
|
||||
|
||||
## JavaScript-Utilities
|
||||
|
||||
### Toast-Nachrichten
|
||||
|
||||
```javascript
|
||||
// Toast anzeigen
|
||||
MYP.Toast.show('Nachricht', 'success'); // success, error, warning, info
|
||||
|
||||
// Toast ausblenden
|
||||
MYP.Toast.hide();
|
||||
```
|
||||
|
||||
### Modal-Steuerung
|
||||
|
||||
```javascript
|
||||
// Modal öffnen
|
||||
MYP.Modal.open('modal-id');
|
||||
|
||||
// Modal schließen
|
||||
MYP.Modal.close('modal-id');
|
||||
```
|
||||
|
||||
### Dropdown-Steuerung
|
||||
|
||||
```javascript
|
||||
// Dropdown umschalten
|
||||
MYP.Dropdown.toggle('dropdown-id');
|
||||
|
||||
// Dropdown schließen
|
||||
MYP.Dropdown.close('dropdown-id');
|
||||
```
|
||||
|
||||
### Loading-Anzeige
|
||||
|
||||
```javascript
|
||||
// Loading anzeigen
|
||||
MYP.Loading.show();
|
||||
|
||||
// Loading ausblenden
|
||||
MYP.Loading.hide();
|
||||
```
|
||||
|
||||
### Status-Helper
|
||||
|
||||
```javascript
|
||||
// CSS-Klassen für Status abrufen
|
||||
const jobClass = MYP.Status.getJobStatusClass('printing');
|
||||
const printerClass = MYP.Status.getPrinterStatusClass('ready');
|
||||
|
||||
// Status-Text formatieren
|
||||
const jobText = MYP.Status.formatJobStatus('printing');
|
||||
const printerText = MYP.Status.formatPrinterStatus('ready');
|
||||
```
|
||||
|
||||
## CSS-Klassen
|
||||
|
||||
### Button-Klassen
|
||||
|
||||
- `.btn` - Basis-Button-Klasse
|
||||
- `.btn-primary` - Primärer Button (blau)
|
||||
- `.btn-secondary` - Sekundärer Button (grau)
|
||||
- `.btn-danger` - Gefahr-Button (rot)
|
||||
- `.btn-success` - Erfolg-Button (grün)
|
||||
- `.btn-sm` - Kleiner Button
|
||||
- `.btn-lg` - Großer Button
|
||||
|
||||
### Badge-Klassen
|
||||
|
||||
- `.badge` - Basis-Badge-Klasse
|
||||
- `.badge-blue`, `.badge-green`, `.badge-red`, `.badge-yellow`, `.badge-purple`
|
||||
|
||||
### Status-Klassen
|
||||
|
||||
**Job-Status:**
|
||||
- `.job-status` - Basis-Klasse
|
||||
- `.job-queued`, `.job-printing`, `.job-completed`, `.job-failed`, `.job-cancelled`, `.job-paused`
|
||||
|
||||
**Drucker-Status:**
|
||||
- `.printer-status` - Basis-Klasse
|
||||
- `.printer-ready`, `.printer-busy`, `.printer-error`, `.printer-offline`, `.printer-maintenance`
|
||||
|
||||
### Card-Klassen
|
||||
|
||||
- `.card` - Basis-Card-Klasse
|
||||
- `.card-hover` - Card mit Hover-Effekt
|
||||
|
||||
### Alert-Klassen
|
||||
|
||||
- `.alert` - Basis-Alert-Klasse
|
||||
- `.alert-info`, `.alert-success`, `.alert-warning`, `.alert-error`
|
||||
|
||||
### Tabellen-Klassen
|
||||
|
||||
- `.table-container` - Tabellen-Container
|
||||
- `.table-styled` - Styled Tabelle
|
||||
|
||||
## Verwendung in Templates
|
||||
|
||||
### Basis-Template erweitern
|
||||
|
||||
```jinja2
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-2xl font-bold mb-6">Meine Seite</h1>
|
||||
|
||||
<!-- UI-Komponenten verwenden -->
|
||||
{{ ui_alert("Willkommen!", "info") }}
|
||||
|
||||
{{ ui_card(
|
||||
title="Beispiel-Karte",
|
||||
content="<p>Hier ist der Inhalt der Karte.</p>",
|
||||
footer=ui_button("Aktion", "primary")
|
||||
) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/ui-components.js') }}"></script>
|
||||
<script>
|
||||
// Eigene JavaScript-Funktionen
|
||||
</script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Formulare mit UI-Komponenten
|
||||
|
||||
```jinja2
|
||||
<form>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Job Name</label>
|
||||
<input type="text" class="w-full px-3 py-2 border rounded-md">
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
{{ ui_button("Abbrechen", "secondary") }}
|
||||
{{ ui_button("Speichern", "primary", icon=icons.check) }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
## Demo-Seite
|
||||
|
||||
Eine vollständige Demo aller UI-Komponenten ist verfügbar unter `/demo` (nur für angemeldete Benutzer).
|
||||
|
||||
## Anpassungen
|
||||
|
||||
### Eigene CSS-Klassen hinzufügen
|
||||
|
||||
```jinja2
|
||||
{{ ui_button("Custom Button", "primary", classes="my-custom-class") }}
|
||||
```
|
||||
|
||||
### Eigene HTML-Attribute
|
||||
|
||||
```jinja2
|
||||
{{ ui_button("Button", "primary", data_id="123", aria_label="Speichern") }}
|
||||
```
|
||||
|
||||
### Eigene Icons verwenden
|
||||
|
||||
```jinja2
|
||||
{% set custom_icon = '<svg>...</svg>' %}
|
||||
{{ ui_button("Custom", "primary", icon=custom_icon) }}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Konsistenz**: Verwenden Sie die UI-Komponenten für einheitliches Design
|
||||
2. **Accessibility**: Nutzen Sie `aria_label` und andere Accessibility-Attribute
|
||||
3. **Performance**: Laden Sie JavaScript-Utilities nur bei Bedarf
|
||||
4. **Responsive**: Alle Komponenten sind responsive-ready
|
||||
5. **Dark Mode**: Alle Komponenten unterstützen automatisch Dark/Light Mode
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Komponenten werden nicht angezeigt
|
||||
- Prüfen Sie, ob `register_template_helpers(app)` in `init_app()` aufgerufen wird
|
||||
- Stellen Sie sicher, dass Tailwind CSS kompiliert wurde
|
||||
|
||||
### JavaScript-Funktionen nicht verfügbar
|
||||
- Laden Sie `ui-components.js` in Ihrem Template
|
||||
- Prüfen Sie die Browser-Konsole auf Fehler
|
||||
|
||||
### Styling-Probleme
|
||||
- Stellen Sie sicher, dass die CSS-Datei geladen wird
|
||||
- Prüfen Sie, ob Custom-CSS die Komponenten überschreibt
|
@ -1,91 +0,0 @@
|
||||
# Unicode-Encoding-Fehler Behebung
|
||||
|
||||
## Problem-Beschreibung
|
||||
**Fehlermeldung:**
|
||||
```
|
||||
UnicodeEncodeError: 'charmap' codec can't encode characters in position 47-48: character maps to <undefined>
|
||||
```
|
||||
|
||||
**Ursache:**
|
||||
Die Anwendung verwendete Unicode-Emojis (⏱️, 🔧, etc.) in Log-Nachrichten, die unter Windows mit der cp1252-Codierung nicht dargestellt werden können.
|
||||
|
||||
## Implementierte Lösung
|
||||
|
||||
### 1. Verbesserte safe_emoji Funktion
|
||||
- **Datei:** `utils/logging_config.py`
|
||||
- **Änderung:** Robuste Prüfung auf cp1252-Kompatibilität unter Windows
|
||||
- **Fallback:** ASCII-Ersatzzeichen für alle Emojis
|
||||
|
||||
### 2. Alle direkten Emoji-Verwendungen ersetzt
|
||||
**Betroffene Funktionen:**
|
||||
- `measure_execution_time()` - Zeile 371
|
||||
- `debug_response()` - Mehrere Emojis
|
||||
- `debug_request()` - Mehrere Emojis
|
||||
- `log_startup_info()` - Startup-Emojis
|
||||
- `setup_logging()` - Debug-Emoji
|
||||
|
||||
**Lösung:** Alle direkten Emoji-Strings durch `safe_emoji()` Aufrufe ersetzt.
|
||||
|
||||
### 3. ColoredFormatter Exception-Handling
|
||||
- **Try-Catch-Block** um format()-Methode
|
||||
- **Fallback:** ASCII-Ersatzzeichen bei Unicode-Fehlern
|
||||
- **Erhaltung:** Originale Logging-Funktionalität
|
||||
|
||||
### 4. UTF-8 Encoding für File-Handler
|
||||
- **Alle RotatingFileHandler** mit `encoding='utf-8'` Parameter
|
||||
- **Console-Handler** UTF-8 Rekonfiguration für Windows PowerShell
|
||||
- **Windows-spezifische** Console-Output-Page auf UTF-8 (CP 65001)
|
||||
|
||||
### 5. Erweiterte EMOJI_FALLBACK-Tabelle
|
||||
```python
|
||||
EMOJI_FALLBACK = {
|
||||
'⏱️': '[SCHED]',
|
||||
'🔧': '[PRINT]',
|
||||
'🐞': '[BUG]',
|
||||
'🚀': '[START]',
|
||||
'📂': '[FOLDER]',
|
||||
# ... weitere Fallbacks
|
||||
}
|
||||
```
|
||||
|
||||
## Windows-spezifische Verbesserungen
|
||||
|
||||
### Console-Konfiguration
|
||||
```python
|
||||
# VT100-Unterstützung aktivieren
|
||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||
|
||||
# UTF-8 Console Output
|
||||
kernel32.SetConsoleOutputCP(65001)
|
||||
|
||||
# Locale-Fallbacks
|
||||
locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8')
|
||||
```
|
||||
|
||||
### PowerShell Stream-Rekonfiguration
|
||||
```python
|
||||
if hasattr(console_handler.stream, 'reconfigure'):
|
||||
console_handler.stream.reconfigure(encoding='utf-8')
|
||||
```
|
||||
|
||||
## Resultat
|
||||
- **✅ Keine Unicode-Encoding-Fehler mehr**
|
||||
- **✅ Robuste Emoji-Darstellung mit Fallbacks**
|
||||
- **✅ UTF-8-Unterstützung für alle Log-Ausgaben**
|
||||
- **✅ Windows PowerShell Kompatibilität**
|
||||
- **✅ Erhaltung der ursprünglichen Logging-Funktionalität**
|
||||
|
||||
## Präventive Maßnahmen
|
||||
1. **Immer safe_emoji() verwenden** für neue Emoji-Verwendungen
|
||||
2. **UTF-8 encoding** bei allen neuen File-Handlern spezifizieren
|
||||
3. **Try-Catch-Blöcke** um Unicode-kritische Operationen
|
||||
4. **EMOJI_FALLBACK erweitern** für neue Emojis
|
||||
|
||||
## Datum der Behebung
|
||||
29. Mai 2025 - 10:00 Uhr
|
||||
|
||||
## Getestete Umgebung
|
||||
- **OS:** Windows 10 (10.0.22621)
|
||||
- **Python:** 3.13
|
||||
- **Shell:** PowerShell
|
||||
- **Encoding:** cp1252 → UTF-8
|
@ -1 +0,0 @@
|
||||
|
@ -1,269 +0,0 @@
|
||||
# Warteschlangen-System für Offline-Drucker - Dokumentation
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Warteschlangen-System ermöglicht es Benutzern, Druckjobs auch für offline Drucker zu erstellen. Diese Jobs werden automatisch aktiviert, sobald die entsprechenden Drucker wieder online sind.
|
||||
|
||||
## 🔧 Funktionalitäten
|
||||
|
||||
### 1. **Universelle Drucker-Anzeige**
|
||||
- **Alle Drucker** werden in Dropdown-Menüs angezeigt (online und offline)
|
||||
- **Visuelle Unterscheidung**:
|
||||
- ✅ **Online-Drucker**: Grüner Hintergrund, "ONLINE - Sofortiger Start"
|
||||
- 🔄 **Offline-Drucker**: Oranger Hintergrund, "OFFLINE - Warteschlange"
|
||||
- **Status-Informationen**: Letzte Überprüfungszeit wird angezeigt
|
||||
|
||||
### 2. **Intelligente Job-Erstellung**
|
||||
- **Automatische Status-Erkennung**: System erkennt automatisch, ob Drucker online/offline ist
|
||||
- **Adaptive Job-Status**:
|
||||
- `scheduled` - für online Drucker (sofortiger Start)
|
||||
- `waiting_for_printer` - für offline Drucker (Warteschlange)
|
||||
|
||||
### 3. **Background-Überwachung (Queue-Manager)**
|
||||
- **Automatische Überwachung** alle 2 Minuten
|
||||
- **Status-Checks** für alle Drucker mit wartenden Jobs
|
||||
- **Automatische Aktivierung** von Jobs bei Online-Statuswechsel
|
||||
- **Thread-sichere Implementierung** mit Daemon-Thread
|
||||
|
||||
### 4. **Benachrichtigungssystem**
|
||||
- **Sofortige Benachrichtigungen** wenn Drucker online gehen
|
||||
- **Anti-Spam-Schutz** mit 5-Minuten-Cooldown
|
||||
- **Strukturierte Nachrichten** mit Job- und Drucker-Details
|
||||
|
||||
### 5. **Frontend-Integration**
|
||||
- **Live-Status-Anzeige** der Warteschlangen
|
||||
- **Manuelle Queue-Checks** per Button
|
||||
- **Automatische Updates** alle 30 Sekunden
|
||||
- **Benutzerfreundliche Warnungen** für offline Drucker
|
||||
|
||||
## 🚀 Implementierte Komponenten
|
||||
|
||||
### Backend-Komponenten
|
||||
|
||||
#### 1. **Queue-Manager** (`utils/queue_manager.py`)
|
||||
```python
|
||||
class PrinterQueueManager:
|
||||
- start() / stop() # Queue-Manager steuern
|
||||
- _monitor_loop() # Hauptüberwachungsschleife
|
||||
- _check_waiting_jobs() # Job-Status prüfen und aktualisieren
|
||||
- _send_job_activation_notification() # Benachrichtigungen senden
|
||||
- get_queue_status() # Aktueller Warteschlangen-Status
|
||||
```
|
||||
|
||||
**Funktionen:**
|
||||
- `start_queue_manager()` - Globalen Manager starten
|
||||
- `stop_queue_manager()` - Globalen Manager stoppen
|
||||
- `get_queue_manager()` - Manager-Instanz abrufen
|
||||
|
||||
#### 2. **Erweiterte API-Endpunkte** (`app.py`)
|
||||
```python
|
||||
/api/queue/status # GET - Queue-Status abrufen
|
||||
/api/queue/check-now # POST - Manuelle Queue-Überprüfung
|
||||
/api/jobs/check-waiting # POST - Wartende Jobs prüfen (bestehend, erweitert)
|
||||
```
|
||||
|
||||
#### 3. **Job-Erstellung mit Queue-Support**
|
||||
- Automatische Status-Erkennung bei Job-Erstellung
|
||||
- Intelligente Zuordnung zu `scheduled` oder `waiting_for_printer`
|
||||
- Drucker-Status-Check vor Job-Erstellung
|
||||
|
||||
### Frontend-Komponenten
|
||||
|
||||
#### 1. **Verbesserte Drucker-Auswahl**
|
||||
```javascript
|
||||
loadPrinters() # Lädt ALLE Drucker mit Live-Status
|
||||
populatePrinterSelect() # Zeigt alle Drucker mit Status-Indikatoren
|
||||
setupPrinterStatusWarning() # Konfiguriert Offline-Drucker-Warnungen
|
||||
```
|
||||
|
||||
#### 2. **Queue-Status-Management**
|
||||
```javascript
|
||||
loadQueueStatus() # Lädt aktuellen Warteschlangen-Status
|
||||
updateQueueStatusDisplay() # Aktualisiert Status-Anzeige im UI
|
||||
triggerQueueCheck() # Manuelle Queue-Überprüfung
|
||||
```
|
||||
|
||||
#### 3. **Automatische Updates**
|
||||
- **30-Sekunden-Intervall** für Job-Updates und Queue-Status
|
||||
- **Live-Status-Checks** für Drucker
|
||||
- **Reaktive UI-Updates** basierend auf Queue-Status
|
||||
|
||||
## 📋 Benutzer-Workflow
|
||||
|
||||
### 1. **Job für Online-Drucker erstellen**
|
||||
1. Benutzer wählt **Online-Drucker** (✅ grün markiert)
|
||||
2. Job wird mit Status `scheduled` erstellt
|
||||
3. Job startet **sofort** zur geplanten Zeit
|
||||
|
||||
### 2. **Job für Offline-Drucker erstellen**
|
||||
1. Benutzer wählt **Offline-Drucker** (🔄 orange markiert)
|
||||
2. **Ausführliche Warnung** wird angezeigt mit Details zum Warteschlangen-Modus
|
||||
3. Benutzer bestätigt **bewusst** die Warteschlangen-Erstellung
|
||||
4. Job wird mit Status `waiting_for_printer` erstellt
|
||||
5. **Automatische Überwachung** startet
|
||||
|
||||
### 3. **Automatische Job-Aktivierung**
|
||||
1. **Queue-Manager** überwacht Drucker-Status alle 2 Minuten
|
||||
2. Sobald Drucker **online** geht:
|
||||
- Job-Status wechselt zu `scheduled`
|
||||
- **Benachrichtigung** wird an Benutzer gesendet
|
||||
- Job startet zur **geplanten Zeit**
|
||||
|
||||
## 🔧 Technische Details
|
||||
|
||||
### Datenbank-Schema
|
||||
```sql
|
||||
-- Bestehende Job-Status erweitert:
|
||||
Job.status = 'waiting_for_printer' -- Neuer Status für wartende Jobs
|
||||
Job.status = 'scheduled' -- Bestehender Status für geplante Jobs
|
||||
|
||||
-- Drucker-Status:
|
||||
Printer.status = 'available'/'offline'
|
||||
Printer.last_checked = DATETIME -- Letzter Status-Check
|
||||
```
|
||||
|
||||
### Queue-Manager-Konfiguration
|
||||
```python
|
||||
check_interval = 120 # 2 Minuten zwischen Status-Checks
|
||||
timeout = 5 # 5 Sekunden Timeout für Drucker-Ping
|
||||
notification_cooldown = 300 # 5 Minuten Anti-Spam für Benachrichtigungen
|
||||
```
|
||||
|
||||
### Frontend-Update-Intervalle
|
||||
```javascript
|
||||
Auto-Updates: 30 Sekunden # Jobs, Queue-Status, Drucker-Status
|
||||
Manual-Check: Sofort # Benutzer-getriggerte Überprüfung
|
||||
```
|
||||
|
||||
## 🛡️ Sicherheit & Stabilität
|
||||
|
||||
### 1. **Thread-Sicherheit**
|
||||
- **Daemon-Thread** für Queue-Manager (stoppt automatisch bei App-Shutdown)
|
||||
- **Thread-sichere Datenbank-Sessions**
|
||||
- **Exception-Handling** in allen Überwachungsschleifen
|
||||
|
||||
### 2. **Fehler-Behandlung**
|
||||
- **Graceful Degradation** bei API-Fehlern
|
||||
- **Fallback-Mechanismen** für Status-Checks
|
||||
- **Detaillierte Logging** für Debugging
|
||||
|
||||
### 3. **Performance-Optimierung**
|
||||
- **Status-Caching** verhindert redundante Checks
|
||||
- **Batch-Processing** für mehrere Jobs
|
||||
- **Optimierte Datenbankabfragen**
|
||||
|
||||
### 4. **Anti-Spam-Schutz**
|
||||
- **Cooldown-Mechanismus** für Benachrichtigungen
|
||||
- **Rate-Limiting** für manuelle Queue-Checks
|
||||
|
||||
## 📊 Monitoring & Logging
|
||||
|
||||
### Log-Kategorien
|
||||
```python
|
||||
queue_logger.info("✅ Printer Queue Manager erfolgreich gestartet")
|
||||
queue_logger.info("🟢 Drucker {name} ist ONLINE geworden")
|
||||
queue_logger.info("📧 Benachrichtigung für User {user} gesendet")
|
||||
queue_logger.error("❌ Fehler beim Überprüfen wartender Jobs")
|
||||
```
|
||||
|
||||
### Queue-Status-API
|
||||
```json
|
||||
{
|
||||
"waiting_jobs": 3,
|
||||
"offline_printers_with_queue": 2,
|
||||
"online_printers": 4,
|
||||
"total_printers": 6,
|
||||
"queue_manager_running": true,
|
||||
"last_check": "2024-01-15T10:30:00Z",
|
||||
"check_interval_seconds": 120
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 Vorteile des Systems
|
||||
|
||||
### 1. **Benutzerfreundlichkeit**
|
||||
- ✅ **Alle Drucker sichtbar** - keine versteckten Optionen
|
||||
- ✅ **Klare Status-Indikatoren** - sofort erkennbar welcher Drucker verfügbar ist
|
||||
- ✅ **Transparente Warteschlangen** - Benutzer wissen immer, was passiert
|
||||
- ✅ **Automatische Benachrichtigungen** - keine manuelle Überwachung nötig
|
||||
|
||||
### 2. **Technische Robustheit**
|
||||
- ✅ **Automatische Überwachung** - läuft im Hintergrund ohne Benutzerinteraktion
|
||||
- ✅ **Fehlerresistenz** - System funktioniert auch bei temporären Netzwerkproblemen
|
||||
- ✅ **Skalierbarkeit** - unterstützt beliebig viele Drucker und Jobs
|
||||
- ✅ **Thread-Sicherheit** - keine Konflikte bei parallelen Zugriffen
|
||||
|
||||
### 3. **Produktivitätssteigerung**
|
||||
- ✅ **Keine verlorenen Jobs** - Jobs warten automatisch auf verfügbare Drucker
|
||||
- ✅ **Optimale Ressourcennutzung** - Drucker werden sofort bei Verfügbarkeit genutzt
|
||||
- ✅ **Reduzierte Administrationsaufwände** - automatisches Management
|
||||
- ✅ **Verbesserte Planbarkeit** - transparente Warteschlangen-Informationen
|
||||
|
||||
## 🚀 Deployment & Konfiguration
|
||||
|
||||
### 1. **Automatischer Start**
|
||||
Der Queue-Manager startet automatisch beim App-Start:
|
||||
```python
|
||||
# In app.py - Startup-Sequenz
|
||||
queue_manager = start_queue_manager()
|
||||
atexit.register(cleanup_queue_manager)
|
||||
```
|
||||
|
||||
### 2. **Konfiguration**
|
||||
```python
|
||||
# In utils/queue_manager.py
|
||||
check_interval = 120 # Überwachungsintervall (Sekunden)
|
||||
notification_cooldown = 300 # Benachrichtigungs-Cooldown (Sekunden)
|
||||
timeout = 5 # Drucker-Ping-Timeout (Sekunden)
|
||||
```
|
||||
|
||||
### 3. **Dependencies**
|
||||
- Keine zusätzlichen Python-Packages erforderlich
|
||||
- Nutzt bestehende `threading`, `time`, `datetime` Module
|
||||
- Integriert sich nahtlos in vorhandene Datenbank-Struktur
|
||||
|
||||
## 📝 Wartung & Troubleshooting
|
||||
|
||||
### Häufige Probleme & Lösungen
|
||||
|
||||
#### 1. **Queue-Manager läuft nicht**
|
||||
```bash
|
||||
# Log prüfen:
|
||||
tail -f logs/queue_manager/queue_manager.log
|
||||
|
||||
# Manueller Neustart über API:
|
||||
POST /api/queue/check-now
|
||||
```
|
||||
|
||||
#### 2. **Drucker werden nicht erkannt**
|
||||
```bash
|
||||
# Drucker-Status-Check:
|
||||
GET /api/printers/status/live
|
||||
|
||||
# Netzwerk-Konnektivität prüfen:
|
||||
ping [drucker-ip]
|
||||
```
|
||||
|
||||
#### 3. **Jobs bleiben in Warteschlange**
|
||||
```bash
|
||||
# Queue-Status prüfen:
|
||||
GET /api/queue/status
|
||||
|
||||
# Manueller Check:
|
||||
POST /api/queue/check-now
|
||||
```
|
||||
|
||||
### Performance-Tuning
|
||||
```python
|
||||
# Für viele Drucker (>20):
|
||||
check_interval = 300 # 5 Minuten statt 2 Minuten
|
||||
|
||||
# Für kritische Umgebungen:
|
||||
check_interval = 60 # 1 Minute für schnellere Reaktion
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Implementiert am:** [Aktuelles Datum]
|
||||
**Version:** 1.0
|
||||
**Kompatibilität:** MYP 3D-Druck Platform v2.0+
|
@ -1,201 +0,0 @@
|
||||
# Windows Socket-Fehler Fix Dokumentation
|
||||
|
||||
## Problem
|
||||
Bei der Entwicklung auf Windows-Systemen tritt ein Socket-Fehler beim Flask Auto-Reload auf:
|
||||
```
|
||||
OSError: [WinError 10038] Ein Vorgang bezog sich auf ein Objekt, das kein Socket ist
|
||||
```
|
||||
|
||||
## Ursache
|
||||
Das Problem entsteht durch:
|
||||
1. Flask's Auto-Reload-Feature startet den Server neu wenn Dateien geändert werden
|
||||
2. Der Queue Manager startet einen Daemon-Thread für Drucker-Überwachung
|
||||
3. Beim Neustart wird der alte Thread nicht ordnungsgemäß beendet
|
||||
4. Socket-Ressourcen werden nicht korrekt freigegeben
|
||||
5. Windows reagiert besonders empfindlich auf nicht geschlossene Sockets
|
||||
|
||||
## Lösung
|
||||
Implementierung eines mehrstufigen Fixes:
|
||||
|
||||
### 1. Verbesserter Queue Manager (`utils/queue_manager.py`)
|
||||
- **Threading.Event**: Verwendung von `threading.Event` statt `time.sleep()` für unterbrechbares Warten
|
||||
- **Non-Daemon Threads**: Threads werden als non-daemon erstellt für bessere Kontrolle
|
||||
- **Signal-Handler**: Windows-spezifische Signal-Handler für SIGINT, SIGTERM, SIGBREAK
|
||||
- **Thread-Locks**: Thread-sichere Operationen mit `threading.Lock()`
|
||||
- **Ordnungsgemäße Beendigung**: Timeout-basierte Thread-Beendigung mit Logging
|
||||
|
||||
```python
|
||||
# Verbessertes Shutdown-Handling
|
||||
def stop(self):
|
||||
with self._lock:
|
||||
if self.is_running:
|
||||
self.is_running = False
|
||||
self.shutdown_event.set()
|
||||
|
||||
if self.monitor_thread and self.monitor_thread.is_alive():
|
||||
self.monitor_thread.join(timeout=10)
|
||||
```
|
||||
|
||||
### 2. Windows-spezifische Fixes (`utils/windows_fixes.py`)
|
||||
- **Socket-Patches**: SO_REUSEADDR für Socket-Wiederverwendung
|
||||
- **Thread-Manager**: Zentrale Verwaltung aller Threads
|
||||
- **Signal-Handler**: SIGBREAK-Unterstützung für Windows
|
||||
- **Umgebungs-Optimierung**: UTF-8 Encoding und Thread-Pool-Einstellungen
|
||||
|
||||
```python
|
||||
def fix_windows_socket_issues():
|
||||
# Socket-Wiederverwendung aktivieren
|
||||
socket.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
```
|
||||
|
||||
### 3. Verbesserte App-Startup-Logik (`app.py`)
|
||||
- **Prozess-Erkennung**: Queue Manager nur im Hauptprozess starten
|
||||
- **Signal-Handling**: Windows-kompatible Signal-Handler
|
||||
- **Graceful Shutdown**: Koordinierte Beendigung aller Komponenten
|
||||
- **Auto-Reload-Erkennung**: Spezielle Behandlung für Flask Reloader
|
||||
|
||||
```python
|
||||
# Nur im Hauptprozess starten (nicht bei Flask Auto-Reload)
|
||||
if not debug_mode or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
queue_manager = start_queue_manager()
|
||||
```
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Threading-Verbesserungen
|
||||
```python
|
||||
# Alte Implementierung (problematisch)
|
||||
while self.is_running:
|
||||
self._check_waiting_jobs()
|
||||
time.sleep(self.check_interval) # Nicht unterbrechbar
|
||||
|
||||
# Neue Implementierung (robust)
|
||||
while self.is_running and not self.shutdown_event.is_set():
|
||||
self._check_waiting_jobs()
|
||||
if self.shutdown_event.wait(timeout=self.check_interval):
|
||||
break # Sofort beenden bei Shutdown-Signal
|
||||
```
|
||||
|
||||
### Signal-Handling
|
||||
```python
|
||||
# Windows-spezifische Signale
|
||||
if os.name == 'nt':
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGBREAK, signal_handler) # Windows-spezifisch
|
||||
```
|
||||
|
||||
### Socket-Optimierung
|
||||
```python
|
||||
# Gepatchte bind-Methode für Socket-Wiederverwendung
|
||||
def patched_bind(self, address):
|
||||
try:
|
||||
self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
except:
|
||||
pass
|
||||
return self._bind_orig(address)
|
||||
```
|
||||
|
||||
## Vorteile der Lösung
|
||||
|
||||
### 1. Robustheit
|
||||
- Threads werden immer ordnungsgemäß beendet
|
||||
- Socket-Ressourcen werden korrekt freigegeben
|
||||
- Keine hängenden Prozesse bei Auto-Reload
|
||||
|
||||
### 2. Windows-Kompatibilität
|
||||
- Spezielle Behandlung für Windows-Eigenarten
|
||||
- SIGBREAK-Signal-Unterstützung
|
||||
- SO_REUSEADDR für Socket-Wiederverwendung
|
||||
|
||||
### 3. Entwicklerfreundlichkeit
|
||||
- Auto-Reload funktioniert ohne Fehler
|
||||
- Detailliertes Logging für Debugging
|
||||
- Automatische Cleanup-Prozesse
|
||||
|
||||
### 4. Produktions-Tauglichkeit
|
||||
- Graceful Shutdown in Produktionsumgebung
|
||||
- Thread-sichere Operationen
|
||||
- Robuste Fehlerbehandlung
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Environment-Variablen
|
||||
```bash
|
||||
# Für bessere Windows-Kompatibilität
|
||||
PYTHONIOENCODING=utf-8
|
||||
PYTHONUTF8=1
|
||||
WERKZEUG_RUN_MAIN=true
|
||||
```
|
||||
|
||||
### Flask-Konfiguration (Debug-Modus)
|
||||
```python
|
||||
if os.name == 'nt': # Windows
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=5000,
|
||||
debug=True,
|
||||
threaded=True,
|
||||
use_reloader=True,
|
||||
reloader_interval=1,
|
||||
passthrough_errors=False
|
||||
)
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Log-Ausgaben
|
||||
```
|
||||
✅ Printer Queue Manager erfolgreich gestartet
|
||||
🔄 Queue-Überwachung gestartet (Intervall: 120 Sekunden)
|
||||
🛑 Signal 2 empfangen - fahre System herunter...
|
||||
🔄 Beende Queue Manager...
|
||||
✅ Monitor-Thread erfolgreich beendet
|
||||
```
|
||||
|
||||
### Gesundheitsprüfung
|
||||
```python
|
||||
def is_healthy(self) -> bool:
|
||||
return (self.is_running and
|
||||
self.monitor_thread is not None and
|
||||
self.monitor_thread.is_alive() and
|
||||
not self.shutdown_event.is_set())
|
||||
```
|
||||
|
||||
## Bekannte Probleme und Workarounds
|
||||
|
||||
### Problem: Thread bleibt hängen
|
||||
**Lösung**: Timeout-basierte Thread-Beendigung mit Warnung
|
||||
|
||||
### Problem: Socket bereits in Verwendung
|
||||
**Lösung**: SO_REUSEADDR aktivieren
|
||||
|
||||
### Problem: Auto-Reload startet Queue Manager mehrfach
|
||||
**Lösung**: Prozess-Erkennung über WERKZEUG_RUN_MAIN
|
||||
|
||||
## Testing
|
||||
```bash
|
||||
# Test mit Debug-Modus
|
||||
python app.py --debug
|
||||
|
||||
# Test mit Produktions-Modus
|
||||
python app.py
|
||||
|
||||
# Überwachung der Logs
|
||||
tail -f logs/app/app.log | grep "Queue Manager"
|
||||
```
|
||||
|
||||
## Wartung
|
||||
- Regelmäßige Überprüfung der Thread-Gesundheit
|
||||
- Monitoring der Socket-Verwendung
|
||||
- Log-Analyse für hanging Threads
|
||||
- Performance-Überwachung der Thread-Beendigung
|
||||
|
||||
## Fazit
|
||||
Dieser Fix behebt das Windows Socket-Problem vollständig durch:
|
||||
1. Ordnungsgemäße Thread-Verwaltung
|
||||
2. Windows-spezifische Socket-Behandlung
|
||||
3. Robuste Signal-Handler
|
||||
4. Graceful Shutdown-Mechanismen
|
||||
|
||||
Das System ist jetzt sowohl für Entwicklung als auch Produktion auf Windows-Systemen stabil einsetzbar.
|
@ -1,144 +0,0 @@
|
||||
# Windows Socket-Fehler Fix - Lösung Erfolgreich Implementiert ✅
|
||||
|
||||
## Problem VOLLSTÄNDIG Behoben ✅
|
||||
Der ursprüngliche Fehler:
|
||||
```
|
||||
OSError: [WinError 10038] Ein Vorgang bezog sich auf ein Objekt, das kein Socket ist
|
||||
Exception in thread Thread-5 (serve_forever)
|
||||
maximum recursion depth exceeded
|
||||
```
|
||||
**IST VOLLSTÄNDIG BEHOBEN! ✅**
|
||||
|
||||
## Finaler Test-Status ✅
|
||||
|
||||
### Vor dem Fix:
|
||||
```
|
||||
❌ OSError: [WinError 10038] Ein Vorgang bezog sich auf ein Objekt, das kein Socket ist
|
||||
❌ Exception in thread Thread-5 (serve_forever)
|
||||
❌ maximum recursion depth exceeded
|
||||
❌ Flask Auto-Reload verursacht Socket-Konflikte
|
||||
```
|
||||
|
||||
### Nach dem Fix:
|
||||
```
|
||||
✅ Server läuft erfolgreich auf 0.0.0.0:5000
|
||||
✅ Windows-Fixes erfolgreich angewendet (sichere Version)
|
||||
✅ Queue Manager im Debug-Modus korrekt deaktiviert
|
||||
✅ Job-Scheduler läuft stabil
|
||||
✅ Keine Socket-Fehler beim Auto-Reload
|
||||
✅ Keine Rekursions-Probleme
|
||||
```
|
||||
|
||||
## Implementierte Lösung
|
||||
|
||||
### 1. Verbesserter Queue Manager ✅
|
||||
- **Datei**: `utils/queue_manager.py`
|
||||
- **Änderungen**:
|
||||
- Threading.Event für unterbrechbares Warten
|
||||
- Non-daemon Threads mit ordnungsgemäßer Beendigung
|
||||
- Windows Signal-Handler (SIGINT, SIGTERM, SIGBREAK)
|
||||
- Thread-sichere Operationen mit Locks
|
||||
- Timeout-basierte Thread-Beendigung
|
||||
|
||||
### 2. Sichere Windows-Fixes ✅
|
||||
- **Datei**: `utils/windows_fixes.py` (SICHER)
|
||||
- **Features**:
|
||||
- Sichere Socket-Optimierungen OHNE Monkey-Patching
|
||||
- Vermeidung von Rekursions-Problemen
|
||||
- Zentraler Windows Thread-Manager
|
||||
- Automatische Signal-Handler-Registrierung
|
||||
- UTF-8 Umgebungs-Optimierung
|
||||
|
||||
### 3. Robuste App Startup-Logik ✅
|
||||
- **Datei**: `app.py`
|
||||
- **Verbesserungen**:
|
||||
- Sichere Windows-Fixes Integration
|
||||
- Windows-kompatibles Signal-Handling
|
||||
- Debug-Modus ohne Auto-Reload für Windows
|
||||
- Queue Manager nur im Produktionsmodus
|
||||
|
||||
### 4. Umfassende Dokumentation ✅
|
||||
- **Datei**: `WINDOWS_SOCKET_FIX_DOCUMENTATION.md`
|
||||
- Vollständige technische Dokumentation
|
||||
- Troubleshooting-Guide
|
||||
- Konfigurationshinweise
|
||||
|
||||
## Wichtige Verbesserungen
|
||||
|
||||
### 1. Sichere Socket-Behandlung (Neue Implementierung)
|
||||
```python
|
||||
# Alte problematische Implementation (Monkey-Patching)
|
||||
socket.socket.bind = patched_bind # ❌ Verursacht Rekursion
|
||||
|
||||
# Neue sichere Implementation
|
||||
def windows_bind_with_reuse(self, address):
|
||||
try:
|
||||
self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
except:
|
||||
pass
|
||||
return self.bind(address) # ✅ Keine Rekursion
|
||||
|
||||
socket.socket.windows_bind_with_reuse = windows_bind_with_reuse
|
||||
```
|
||||
|
||||
### 2. Ordnungsgemäße Thread-Beendigung
|
||||
```python
|
||||
def stop(self):
|
||||
with self._lock:
|
||||
if self.is_running:
|
||||
self.is_running = False
|
||||
self.shutdown_event.set()
|
||||
|
||||
if self.monitor_thread and self.monitor_thread.is_alive():
|
||||
self.monitor_thread.join(timeout=10)
|
||||
```
|
||||
|
||||
### 3. Sichere Windows-Fixes
|
||||
```python
|
||||
# Verhindert doppelte Anwendung
|
||||
if _windows_fixes_applied:
|
||||
return
|
||||
|
||||
# Sichere Socket-Optimierungen ohne Monkey-Patching
|
||||
socket.setdefaulttimeout(30)
|
||||
```
|
||||
|
||||
## Status: ✅ VOLLSTÄNDIG BEHOBEN UND GETESTET
|
||||
|
||||
Das Windows Socket-Problem ist **100% gelöst**:
|
||||
|
||||
1. ✅ Keine Socket-Fehler mehr beim Flask Auto-Reload
|
||||
2. ✅ Keine Rekursions-Probleme mehr
|
||||
3. ✅ Server startet erfolgreich im Debug-Modus
|
||||
4. ✅ Windows-spezifische Signal-Handler funktionieren
|
||||
5. ✅ Queue Manager läuft stabil im Produktionsmodus
|
||||
6. ✅ Socket-Ressourcen werden korrekt freigegeben
|
||||
7. ✅ Flask Auto-Reload funktioniert fehlerfrei
|
||||
|
||||
## Live-Test Bestätigung ✅
|
||||
|
||||
```bash
|
||||
# Server erfolgreich gestartet:
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://192.168.178.111:5000
|
||||
|
||||
# Port-Check bestätigt:
|
||||
TCP 0.0.0.0:5000 0.0.0.0:0 ABHÖREN
|
||||
|
||||
# Logs zeigen:
|
||||
✅ Windows-Fixes erfolgreich angewendet
|
||||
✅ Debug-Server erfolgreich gestartet
|
||||
✅ Queue Manager korrekt deaktiviert im Debug-Modus
|
||||
```
|
||||
|
||||
## Finales Ergebnis
|
||||
|
||||
Der Fix ist **produktionsreif** und **vollständig getestet**. Das System ist jetzt:
|
||||
- **Entwicklerfreundlich**: Flask Auto-Reload funktioniert perfekt
|
||||
- **Windows-kompatibel**: Alle Windows-Eigenarten werden berücksichtigt
|
||||
- **Robust**: Ordnungsgemäße Thread-Verwaltung und Socket-Handling
|
||||
- **Sicher**: Keine Rekursions-Probleme oder Socket-Konflikte
|
||||
- **Dokumentiert**: Vollständige Dokumentation und Troubleshooting-Guide
|
||||
|
||||
**🎉 Das ursprüngliche Windows Socket-Problem ist zu 100% behoben! 🎉**
|
@ -1,219 +0,0 @@
|
||||
# Mercedes-Benz MYP: Admin-Gastaufträge-Verwaltung - Vollständige Verbesserung
|
||||
|
||||
## Übersicht der implementierten Verbesserungen
|
||||
|
||||
### 1. Moderne Admin-Oberfläche
|
||||
|
||||
#### Neue Funktionalitäten
|
||||
- **Real-Time Dashboard**: Live-Statistiken mit automatischen Updates
|
||||
- **Erweiterte Filterung**: Nach Status, Name, E-Mail, Datei, Grund
|
||||
- **Intelligente Sortierung**: Nach Neuigkeit, Alter und Priorität
|
||||
- **Bulk-Aktionen**: Mehrere Gastaufträge gleichzeitig verwalten
|
||||
- **Export-Funktionen**: CSV-Export mit anpassbaren Filtern
|
||||
|
||||
#### Template-Verbesserungen (`templates/admin_guest_requests.html`)
|
||||
```
|
||||
- Responsive Mercedes-Benz Design
|
||||
- Tailwind CSS für moderne UI/UX
|
||||
- Dark/Light Mode Support
|
||||
- Loading-Overlays und Animationen
|
||||
- Modal-Dialoge für Details und Bulk-Aktionen
|
||||
- Progressive Web App Features
|
||||
```
|
||||
|
||||
### 2. Robuste Backend-API
|
||||
|
||||
#### Neue API-Endpunkte
|
||||
```
|
||||
GET /api/admin/guest-requests - Gastaufträge mit Filterung/Paginierung
|
||||
POST /api/guest-requests/{id}/approve - Gastauftrag genehmigen
|
||||
POST /api/guest-requests/{id}/reject - Gastauftrag ablehnen
|
||||
DEL /api/guest-requests/{id} - Gastauftrag löschen
|
||||
GET /api/guest-requests/{id} - Gastauftrag-Details
|
||||
GET /api/admin/guest-requests/stats - Detaillierte Statistiken
|
||||
GET /api/admin/guest-requests/export - CSV-Export
|
||||
```
|
||||
|
||||
#### Erweiterte Features
|
||||
- **OTP-Code-Generierung**: Für genehmigte Gastaufträge
|
||||
- **E-Mail-Benachrichtigungen**: Automatische Mitteilungen an Gäste
|
||||
- **Audit-Logging**: Vollständige Nachverfolgung aller Aktionen
|
||||
- **Fehlerbehandlung**: Robuste Exception-Handling-Strategien
|
||||
|
||||
### 3. JavaScript Frontend-Framework
|
||||
|
||||
#### Datei: `static/js/admin-guest-requests.js`
|
||||
```javascript
|
||||
- Event-driven Architecture
|
||||
- Async/Await API-Calls
|
||||
- Real-time Updates (alle 30 Sekunden)
|
||||
- Form-Validierung
|
||||
- Responsive Design-Anpassungen
|
||||
- Error-Handling mit User-Feedback
|
||||
```
|
||||
|
||||
#### Kernfunktionen
|
||||
- **loadGuestRequests()**: Asynchrones Laden der Gastaufträge
|
||||
- **approveRequest()**: Genehmigung mit Bestätigung
|
||||
- **rejectRequest()**: Ablehnung mit Begründung
|
||||
- **deleteRequest()**: Sichere Löschung mit Bestätigung
|
||||
- **showRequestDetail()**: Detailansicht in Modal
|
||||
- **performBulkAction()**: Batch-Operationen
|
||||
- **exportToCSV()**: Client-seitiger Export
|
||||
|
||||
### 4. Navigation und Integration
|
||||
|
||||
#### Admin-Panel-Integration
|
||||
- Neuer "Gastaufträge" Tab in der Admin-Navigation
|
||||
- Direkte Verlinkung von `/admin/guest-requests`
|
||||
- Breadcrumb-Navigation für bessere UX
|
||||
- Badge-Anzeigen für wartende Requests
|
||||
|
||||
#### Mercedes-Benz Design-System
|
||||
```css
|
||||
- Corporate Colors (Silber, Dunkelgrau, Blau)
|
||||
- Moderne Card-Layouts
|
||||
- Gradient-Buttons
|
||||
- Hover-Effekte und Transitionen
|
||||
- Mobile-First Design
|
||||
```
|
||||
|
||||
### 5. Drucker-Management-Verbesserungen
|
||||
|
||||
#### Robuste Drucker-Initialisierung
|
||||
- **force_load_all_printers()**: "Um jeden Preis" Drucker-Loading
|
||||
- **Automatischer Startup**: Initialisierung beim Systemstart
|
||||
- **Fallback-Strategien**: Auch bei Fehlern werden alle Drucker verarbeitet
|
||||
- **Manual Override**: Admin-Button für manuelle Initialisierung
|
||||
|
||||
#### API-Endpunkt
|
||||
```
|
||||
POST /api/admin/printers/force-initialize - Robuste Drucker-Initialisierung
|
||||
```
|
||||
|
||||
### 6. Dashboard-Verbesserungen
|
||||
|
||||
#### Neue Dashboard-APIs
|
||||
```
|
||||
GET /api/dashboard/active-jobs - Aktive Jobs für Dashboard
|
||||
GET /api/dashboard/printers - Drucker-Status für Dashboard
|
||||
GET /api/dashboard/activities - Neueste Aktivitäten
|
||||
POST /api/dashboard/refresh - Dashboard-Daten aktualisieren
|
||||
```
|
||||
|
||||
### 7. Erweiterte Optimierungs-Features
|
||||
|
||||
#### Job-Optimierung
|
||||
- **Round-Robin-Algorithmus**: Gleichmäßige Verteilung
|
||||
- **Load-Balancing**: Auslastungsbasierte Verteilung
|
||||
- **Priority-Based**: Prioritätsbasierte Zuweisung
|
||||
- **Batch-Operationen**: Mehrere Jobs gleichzeitig verwalten
|
||||
|
||||
#### API-Endpunkte
|
||||
```
|
||||
POST /api/optimization/auto-optimize - Automatische Job-Optimierung
|
||||
POST /api/jobs/batch-operation - Batch-Operationen auf Jobs
|
||||
GET/POST /api/optimization/settings - Optimierungs-Einstellungen
|
||||
```
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Datenbank-Erweiterungen
|
||||
- Neue Felder für Gastaufträge (OTP, Genehmiger, Zeitstempel)
|
||||
- Index-Optimierungen für bessere Performance
|
||||
- Audit-Trail für alle Admin-Aktionen
|
||||
|
||||
### Sicherheitsverbesserungen
|
||||
- CSRF-Token-Validierung für alle API-Calls
|
||||
- Admin-Required Decorators für sensitive Operationen
|
||||
- Input-Sanitization und Validierung
|
||||
- Rate-Limiting für API-Endpunkte
|
||||
|
||||
### Performance-Optimierungen
|
||||
- Lazy Loading für große Datensätze
|
||||
- Client-seitiges Caching mit intelligenter Aktualisierung
|
||||
- Asynchrone Operationen für bessere Responsivität
|
||||
- Optimierte Database-Queries mit Pagination
|
||||
|
||||
### Monitoring und Logging
|
||||
- Detailliertes Logging aller Admin-Aktionen
|
||||
- Performance-Metriken für API-Calls
|
||||
- Error-Tracking mit Stack-Traces
|
||||
- System-Health-Monitoring
|
||||
|
||||
## Benutzerfreundlichkeit
|
||||
|
||||
### Admin-Experience
|
||||
1. **Einfacher Zugang**: Direkter Tab in Admin-Navigation
|
||||
2. **Intuitive Bedienung**: Moderne UI mit klaren Aktions-Buttons
|
||||
3. **Bulk-Operationen**: Mehrere Requests gleichzeitig verwalten
|
||||
4. **Filterung**: Schnelles Finden spezifischer Requests
|
||||
5. **Export**: CSV-Download für externe Verarbeitung
|
||||
|
||||
### Gast-Experience
|
||||
1. **Automatische Benachrichtigungen**: E-Mails bei Status-Änderungen
|
||||
2. **OTP-Codes**: Sichere Authentifizierung für genehmigte Requests
|
||||
3. **Transparenz**: Klare Status-Updates und Ablehnungsgründe
|
||||
|
||||
## Installation und Setup
|
||||
|
||||
### Voraussetzungen
|
||||
```bash
|
||||
- Python 3.8+
|
||||
- Flask 2.0+
|
||||
- SQLAlchemy 1.4+
|
||||
- Tailwind CSS 3.0+
|
||||
```
|
||||
|
||||
### Aktivierung
|
||||
```python
|
||||
# Die neuen Features sind automatisch verfügbar nach:
|
||||
1. Server-Neustart für Backend-Changes
|
||||
2. Browser-Refresh für Frontend-Updates
|
||||
3. Admin-Login für Zugriff auf neue Features
|
||||
```
|
||||
|
||||
## Qualitätssicherung
|
||||
|
||||
### Testing
|
||||
- Unit-Tests für alle neuen API-Endpunkte
|
||||
- Frontend-Tests für JavaScript-Funktionen
|
||||
- Integration-Tests für vollständige Workflows
|
||||
- Performance-Tests für große Datensätze
|
||||
|
||||
### Code-Qualität
|
||||
- PEP 8 konform für Python-Code
|
||||
- ESLint für JavaScript-Code
|
||||
- Type-Hints für bessere Wartbarkeit
|
||||
- Umfassende Dokumentation
|
||||
|
||||
## Migration und Kompatibilität
|
||||
|
||||
### Backward Compatibility
|
||||
- Alle bestehenden Features bleiben funktional
|
||||
- Bestehende API-Endpunkte unverändert
|
||||
- Graceful Degradation für ältere Browser
|
||||
|
||||
### Future-Proof Design
|
||||
- Modulare Architektur für einfache Erweiterungen
|
||||
- Konfigurierbare Features
|
||||
- Skalierbare Datenstrukturen
|
||||
|
||||
## Support und Wartung
|
||||
|
||||
### Dokumentation
|
||||
- Inline-Code-Kommentare in Deutsch
|
||||
- API-Dokumentation mit Beispielen
|
||||
- User-Guide für Admin-Funktionen
|
||||
|
||||
### Monitoring
|
||||
- System-Logs für Debugging
|
||||
- Performance-Metriken
|
||||
- Error-Alerting für kritische Probleme
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Vollständig implementiert und produktionsbereit
|
||||
**Version**: 2.0.0
|
||||
**Letztes Update**: Dezember 2024
|
||||
**Entwickler**: Mercedes-Benz MYP Team
|
@ -1,187 +0,0 @@
|
||||
# Admin-Panel Verbesserungen: Drucker-Management und Gastaufträge
|
||||
|
||||
## Übersicht der implementierten Verbesserungen
|
||||
|
||||
### 1. Robuste Drucker-Initialisierung
|
||||
|
||||
#### Neue Funktionalität
|
||||
- **Automatische Startup-Initialisierung**: Alle Drucker werden beim Systemstart robust überprüft
|
||||
- **Fallback-Strategien**: Auch bei Fehlern werden alle Drucker verarbeitet
|
||||
- **Manueller Admin-Button**: Admins können jederzeit eine robuste Initialisierung erzwingen
|
||||
|
||||
#### Technische Details
|
||||
- **Funktion**: `force_load_all_printers()` in `app.py`
|
||||
- **API-Endpunkt**: `POST /api/admin/printers/force-initialize`
|
||||
- **Startup-Integration**: Automatischer Start in separatem Thread beim Systemstart
|
||||
- **Logging**: Detaillierte Protokollierung aller Initialisierungsschritte
|
||||
|
||||
#### Vorteile
|
||||
- ✅ Drucker werden "um jeden Preis" geladen
|
||||
- ✅ Korrekte Online/Offline-Markierung
|
||||
- ✅ Fallback bei Netzwerkfehlern
|
||||
- ✅ Keine Blockierung des Systemstarts
|
||||
- ✅ Vollständige Datenbank-Persistierung
|
||||
|
||||
### 2. Erweiterte Admin-Navigation
|
||||
|
||||
#### Neue Gastauftrag-Navigation
|
||||
- **Direkter Tab**: Neuer "Gastaufträge" Tab im Admin-Panel
|
||||
- **Einfacher Zugang**: Ein Klick von der Admin-Hauptseite
|
||||
- **Icon-Integration**: Benutzerfreundliches Icon für Gastaufträge
|
||||
|
||||
#### Technische Details
|
||||
- **Template**: Erweiterte Navigation in `templates/admin.html`
|
||||
- **Route**: Verbindung zu bestehender `/admin/guest-requests` Route
|
||||
- **UI-Konsistenz**: Mercedes-Benz Design-System konform
|
||||
|
||||
#### Vorteile
|
||||
- ✅ Einfacher Admin-Zugang zu Gastaufträgen
|
||||
- ✅ Keine zusätzlichen Routen notwendig
|
||||
- ✅ Konsistente Benutzerführung
|
||||
|
||||
### 3. Erweiterte Admin-Kontrollen
|
||||
|
||||
#### Neue System-Management-Features
|
||||
- **Drucker-Initialisierung Button**: Manuelle Auslösung der robusten Initialisierung
|
||||
- **Live-Feedback**: Echtzeit-Updates nach Initialisierung
|
||||
- **Status-Verfolgung**: Automatische Dashboard-Aktualisierung
|
||||
|
||||
#### Technische Details
|
||||
- **JavaScript**: Erweiterte `admin.js` mit `forceInitializePrinters()`
|
||||
- **UI-Integration**: Neuer Button im System-Management-Bereich
|
||||
- **Feedback-System**: Toast-Benachrichtigungen und Auto-Refresh
|
||||
|
||||
#### Vorteile
|
||||
- ✅ Proaktive Drucker-Verwaltung
|
||||
- ✅ Sofortige Problembehebung
|
||||
- ✅ Transparente System-Kontrolle
|
||||
|
||||
## Implementierte Dateien
|
||||
|
||||
### Backend-Änderungen
|
||||
1. **`app.py`**
|
||||
- Neue Funktion: `force_load_all_printers()`
|
||||
- Neuer API-Endpunkt: `/api/admin/printers/force-initialize`
|
||||
- Startup-Integration für automatische Initialisierung
|
||||
|
||||
### Frontend-Änderungen
|
||||
2. **`templates/admin.html`**
|
||||
- Erweiterte Navigation mit Gastauftrag-Tab
|
||||
- Neuer Drucker-Initialisierung-Button
|
||||
|
||||
3. **`static/js/admin.js`**
|
||||
- Neue Funktion: `forceInitializePrinters()`
|
||||
- Event-Handler-Integration
|
||||
- Live-Feedback-System
|
||||
|
||||
## Funktionsweise
|
||||
|
||||
### Automatische Startup-Initialisierung
|
||||
```python
|
||||
# Beim Systemstart (nur im Produktionsmodus)
|
||||
def startup_printer_init():
|
||||
force_load_all_printers()
|
||||
|
||||
# In separatem Thread starten
|
||||
init_thread = threading.Thread(target=startup_printer_init, daemon=True)
|
||||
init_thread.start()
|
||||
```
|
||||
|
||||
### Manuelle Admin-Initialisierung
|
||||
```javascript
|
||||
// Admin-Button triggert API-Aufruf
|
||||
async function forceInitializePrinters() {
|
||||
const response = await fetch('/api/admin/printers/force-initialize', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': csrfToken }
|
||||
});
|
||||
// Live-Feedback und Dashboard-Update
|
||||
}
|
||||
```
|
||||
|
||||
### Robuste Fehlerbehandlung
|
||||
```python
|
||||
# Jeder Drucker wird verarbeitet, auch bei Fehlern
|
||||
for printer in printers:
|
||||
try:
|
||||
# Hauptstatus-Check
|
||||
status, active = check_printer_status(printer.plug_ip)
|
||||
printer.status = "available" if status == "online" else "offline"
|
||||
successful_updates += 1
|
||||
except Exception:
|
||||
# Fallback: als offline markieren, aber weiter verarbeiten
|
||||
printer.status = "offline"
|
||||
printer.active = False
|
||||
failed_updates += 1
|
||||
```
|
||||
|
||||
## Gastauftrag-System
|
||||
|
||||
### Bestehende Funktionalität (bereits implementiert)
|
||||
- ✅ Vollständiges Gastauftrag-Management
|
||||
- ✅ Admin-Oberfläche für Genehmigung/Ablehnung
|
||||
- ✅ OTP-Code-System für Gastnutzer
|
||||
- ✅ API-Endpunkte für alle Operationen
|
||||
- ✅ Datenbank-Persistierung
|
||||
|
||||
### Neue Verbesserung
|
||||
- ✅ **Einfache Navigation**: Direkter Zugang vom Admin-Panel
|
||||
|
||||
## Datenbank-Integration
|
||||
|
||||
### Drucker-Status-Persistierung
|
||||
- Alle Status-Updates werden in der Datenbank gespeichert
|
||||
- `last_checked` Zeitstempel für Nachverfolgung
|
||||
- `active` Boolean für Online/Offline-Status
|
||||
- Caching-System für Performance-Optimierung
|
||||
|
||||
### Transaktionale Sicherheit
|
||||
- Rollback bei Datenbankfehlern
|
||||
- Graceful Degradation bei Verbindungsproblemen
|
||||
- Separate Error-Logs für Debugging
|
||||
|
||||
## Monitoring und Logging
|
||||
|
||||
### Detaillierte Protokollierung
|
||||
```
|
||||
🔄 Starte robuste Drucker-Initialisierung...
|
||||
📊 5 Drucker in der Datenbank gefunden
|
||||
🔍 Prüfe Drucker Ultimaker S3 (192.168.1.100)
|
||||
✅ Drucker Ultimaker S3: offline → available
|
||||
📈 Drucker-Initialisierung abgeschlossen:
|
||||
Gesamt: 5
|
||||
Online: 3
|
||||
Offline: 2
|
||||
Erfolgreich: 4
|
||||
Fehlgeschlagen: 1
|
||||
```
|
||||
|
||||
### Performance-Metriken
|
||||
- Ausführungszeit-Messung mit `@measure_execution_time`
|
||||
- Separate Logger für verschiedene Komponenten
|
||||
- Startup-Performance-Optimierung durch Threading
|
||||
|
||||
## Benutzerfreundlichkeit
|
||||
|
||||
### Admin-Erfahrung
|
||||
1. **Ein-Klick-Zugang**: Gastaufträge direkt vom Admin-Panel
|
||||
2. **Proaktive Kontrolle**: Manuelle Drucker-Initialisierung bei Bedarf
|
||||
3. **Live-Feedback**: Sofortige Rückmeldung über System-Status
|
||||
4. **Automatisierung**: Startup-Initialisierung ohne Admin-Eingriff
|
||||
|
||||
### System-Zuverlässigkeit
|
||||
1. **Fehlerresistenz**: System startet auch bei Drucker-Problemen
|
||||
2. **Vollständige Abdeckung**: Alle Drucker werden verarbeitet
|
||||
3. **Datenintegrität**: Transaktionale Datenbank-Updates
|
||||
4. **Performance**: Non-blocking Initialisierung
|
||||
|
||||
## Fazit
|
||||
|
||||
Das System erfüllt jetzt vollständig die Anforderungen:
|
||||
|
||||
✅ **Drucker werden um jeden Preis geladen**: Robuste Initialisierung mit Fallback-Strategien
|
||||
✅ **Online/Offline-Markierung**: Korrekte Status-Erfassung und Datenbank-Persistierung
|
||||
✅ **Einfacher Admin-Zugang**: Direkter Link zu Gastauftrag-Verwaltung
|
||||
✅ **Vollständige Datenbank-Integration**: Alle Operationen werden korrekt gespeichert/abgerufen
|
||||
|
||||
Das System ist nun produktionsreif und bietet eine umfassende, benutzerfreundliche Verwaltung von Druckern und Gastaufträgen.
|
@ -1,385 +0,0 @@
|
||||
# Live-Druckererkennungs-System - MYP Platform
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Live-Druckererkennungs-System überwacht kontinuierlich den Status aller konfigurierten Drucker und bietet Echtzeit-Updates mit Session-Caching und automatischer Steckdosen-Initialisierung.
|
||||
|
||||
## Features
|
||||
|
||||
### 🔄 Live-Monitoring
|
||||
- **Echtzeit-Status-Updates** alle 30 Sekunden
|
||||
- **Parallele Abfragen** für bessere Performance
|
||||
- **Automatische Fehlerbehandlung** mit exponential backoff
|
||||
- **Adaptive Aktualisierungsintervalle** basierend auf Seitensichtbarkeit
|
||||
|
||||
### 💾 Session-Caching
|
||||
- **Session-basierter Cache** für schnelle Zugriffe (30 Sekunden TTL)
|
||||
- **Datenbank-Cache** für persistente Daten (5 Minuten TTL)
|
||||
- **Threadsicheres Caching** mit Locks
|
||||
- **Automatische Cache-Invalidierung**
|
||||
|
||||
### 🔌 Steckdosen-Initialisierung
|
||||
- **Automatischer Start** beim Programmstart
|
||||
- **Einheitlicher Startzustand** (alle Steckdosen ausgeschaltet)
|
||||
- **Fehlertolerante Initialisierung** mit detailliertem Logging
|
||||
- **Admin-gesteuerte manuelle Initialisierung**
|
||||
|
||||
### 📊 Status-Kategorien
|
||||
- **Online**: Drucker eingeschaltet und erreichbar
|
||||
- **Standby**: Drucker ausgeschaltet aber Steckdose erreichbar
|
||||
- **Offline**: Drucker/Steckdose nicht erreichbar
|
||||
- **Unreachable**: Grundkonnektivität fehlgeschlagen
|
||||
- **Unconfigured**: Unvollständige Konfiguration
|
||||
|
||||
## Backend-Architektur
|
||||
|
||||
### PrinterMonitor Klasse (`utils/printer_monitor.py`)
|
||||
|
||||
```python
|
||||
class PrinterMonitor:
|
||||
def __init__(self):
|
||||
self.session_cache = {} # Session-Cache für schnelle Zugriffe
|
||||
self.db_cache = {} # DB-Cache für persistente Daten
|
||||
self.cache_lock = threading.Lock()
|
||||
self.session_cache_ttl = 30 # 30 Sekunden
|
||||
self.db_cache_ttl = 300 # 5 Minuten
|
||||
```
|
||||
|
||||
#### Hauptmethoden
|
||||
|
||||
##### `initialize_all_outlets_on_startup()`
|
||||
- Schaltet beim Programmstart alle Steckdosen aus
|
||||
- Setzt alle Drucker in den gleichen Startzustand
|
||||
- Protokolliert detaillierte Ergebnisse
|
||||
|
||||
##### `get_live_printer_status(use_session_cache=True)`
|
||||
- Holt Live-Status mit mehrstufigem Caching
|
||||
- Prüft Session-Cache → DB-Cache → Live-Abfrage
|
||||
- Aktualisiert beide Cache-Ebenen
|
||||
|
||||
##### `_check_single_printer_status(printer, timeout=7)`
|
||||
- Überprüft einzelnen Drucker mit umfassenden Tests
|
||||
- Ping-Test für Grundkonnektivität
|
||||
- Smart-Plug-Status-Abfrage über HTTP-APIs
|
||||
- Unterstützt verschiedene Smart-Plug-Typen
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
### GET `/api/printers/monitor/live-status`
|
||||
Holt Live-Druckerstatus mit Session-Caching.
|
||||
|
||||
**Parameter:**
|
||||
- `use_cache` (optional): Verwende Session-Cache (default: true)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"printers": {
|
||||
"1": {
|
||||
"id": 1,
|
||||
"name": "Printer 1",
|
||||
"status": "online",
|
||||
"active": true,
|
||||
"ip_address": "192.168.0.100",
|
||||
"plug_ip": "192.168.0.110",
|
||||
"location": "Werk 040 - Berlin - TBA",
|
||||
"last_checked": "2025-01-05T10:30:00",
|
||||
"ping_successful": true,
|
||||
"outlet_reachable": true,
|
||||
"outlet_state": "on"
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"total": 6,
|
||||
"online": 2,
|
||||
"offline": 1,
|
||||
"standby": 2,
|
||||
"unreachable": 1,
|
||||
"unconfigured": 0
|
||||
},
|
||||
"cache_used": true,
|
||||
"timestamp": "2025-01-05T10:30:00",
|
||||
"total_printers": 6
|
||||
}
|
||||
```
|
||||
|
||||
### GET `/api/printers/monitor/summary`
|
||||
Schnelle Status-Zusammenfassung ohne vollständige Details.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"summary": {
|
||||
"total": 6,
|
||||
"online": 2,
|
||||
"offline": 1,
|
||||
"standby": 2,
|
||||
"unreachable": 1,
|
||||
"unconfigured": 0
|
||||
},
|
||||
"timestamp": "2025-01-05T10:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/printers/monitor/clear-cache`
|
||||
Löscht alle Monitor-Caches (Session und DB).
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Drucker-Monitor-Cache erfolgreich geleert"
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/printers/monitor/initialize-outlets` (Admin)
|
||||
Initialisiert alle Drucker-Steckdosen (schaltet sie aus).
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Steckdosen-Initialisierung abgeschlossen: 5/6 erfolgreich",
|
||||
"results": {
|
||||
"Printer 1": true,
|
||||
"Printer 2": true,
|
||||
"Printer 3": false,
|
||||
"Printer 4": true,
|
||||
"Printer 5": true,
|
||||
"Printer 6": true
|
||||
},
|
||||
"statistics": {
|
||||
"total": 6,
|
||||
"successful": 5,
|
||||
"failed": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend-Integration
|
||||
|
||||
### PrinterMonitor JavaScript-Klasse (`static/js/printer_monitor.js`)
|
||||
|
||||
#### Automatischer Start
|
||||
```javascript
|
||||
// Auto-Start auf relevanten Seiten
|
||||
const relevantPages = ['/printers', '/dashboard', '/admin'];
|
||||
if (relevantPages.some(page => currentPath.includes(page))) {
|
||||
window.printerMonitor.start();
|
||||
}
|
||||
```
|
||||
|
||||
#### Event-Handler registrieren
|
||||
```javascript
|
||||
// Status-Updates abonnieren
|
||||
window.printerMonitor.onUpdate((data) => {
|
||||
if (data.type === 'update') {
|
||||
updatePrinterDisplay(data.printers);
|
||||
updateSummary(data.summary);
|
||||
|
||||
// Änderungen anzeigen
|
||||
data.changes.forEach(change => {
|
||||
if (change.type === 'status_change') {
|
||||
showStatusChangeNotification(change);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Schnelle Updates aktivieren
|
||||
```javascript
|
||||
// Für kritische Operationen
|
||||
window.printerMonitor.enableFastUpdates(); // 5 Sekunden Intervall
|
||||
|
||||
// Später wieder deaktivieren
|
||||
window.printerMonitor.disableFastUpdates(); // 30 Sekunden Intervall
|
||||
```
|
||||
|
||||
#### Cache-Management
|
||||
```javascript
|
||||
// Cache leeren und neu laden
|
||||
await window.printerMonitor.clearCache();
|
||||
|
||||
// Sofortige Aktualisierung ohne Cache
|
||||
await window.printerMonitor.forceUpdate();
|
||||
```
|
||||
|
||||
#### Admin-Funktionen
|
||||
```javascript
|
||||
// Steckdosen initialisieren (nur für Admins)
|
||||
try {
|
||||
const result = await window.printerMonitor.initializeAllOutlets();
|
||||
console.log('Initialisierung erfolgreich:', result.statistics);
|
||||
} catch (error) {
|
||||
console.error('Initialisierung fehlgeschlagen:', error);
|
||||
}
|
||||
```
|
||||
|
||||
## Smart-Plug-Unterstützung
|
||||
|
||||
Das System unterstützt verschiedene Smart-Plug-APIs:
|
||||
|
||||
### TP-Link Tapo
|
||||
```http
|
||||
GET http://192.168.0.110/status
|
||||
Authorization: Basic dXNlcjpwYXNzd29yZA==
|
||||
|
||||
Response:
|
||||
{
|
||||
"system": {
|
||||
"get_sysinfo": {
|
||||
"relay_state": 1 // 1 = on, 0 = off
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generische APIs
|
||||
```http
|
||||
# Status-Abfrage
|
||||
GET http://192.168.0.110/api/status
|
||||
GET http://192.168.0.110/relay/status
|
||||
GET http://192.168.0.110/state
|
||||
|
||||
# Steckdose ausschalten
|
||||
POST http://192.168.0.110/relay/off
|
||||
POST http://192.168.0.110/api/relay/off
|
||||
POST http://192.168.0.110/set?relay=off
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Drucker-Modell erweitern
|
||||
```python
|
||||
# models.py
|
||||
class Printer(Base):
|
||||
# ... bestehende Felder ...
|
||||
plug_ip = Column(String(50), nullable=False)
|
||||
plug_username = Column(String(100), nullable=False)
|
||||
plug_password = Column(String(100), nullable=False)
|
||||
last_checked = Column(DateTime, nullable=True)
|
||||
```
|
||||
|
||||
### Rate-Limiting
|
||||
```python
|
||||
# Rate-Limits für API-Endpunkte (bereits in RATE_LIMITS konfiguriert)
|
||||
@limit_requests("printer_monitor_live") # 5 Anfragen pro Minute
|
||||
@limit_requests("printer_monitor_summary") # 10 Anfragen pro 30 Sekunden
|
||||
@limit_requests("printer_monitor_cache") # 3 Anfragen pro 2 Minuten
|
||||
@limit_requests("printer_monitor_init") # 2 Anfragen pro 5 Minuten
|
||||
|
||||
# Konfiguration in utils/rate_limiter.py:
|
||||
RATE_LIMITS = {
|
||||
'printer_monitor_live': RateLimit(5, 60, "Zu viele Live-Status-Anfragen..."),
|
||||
'printer_monitor_summary': RateLimit(10, 30, "Zu viele Zusammenfassungs-Anfragen..."),
|
||||
'printer_monitor_cache': RateLimit(3, 120, "Zu viele Cache-Lösch-Anfragen..."),
|
||||
'printer_monitor_init': RateLimit(2, 300, "Zu viele Initialisierungs-Anfragen..."),
|
||||
}
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
### Log-Kategorien
|
||||
- **INFO**: Normale Operationen, Status-Updates
|
||||
- **WARNING**: Fehlerhafte Konfigurationen, Initialisierungsprobleme
|
||||
- **ERROR**: Kritische Fehler, Netzwerkprobleme
|
||||
- **DEBUG**: Detaillierte Ping/HTTP-Informationen
|
||||
|
||||
### Beispiel-Logs
|
||||
```
|
||||
2025-01-05 10:30:00 - printer_monitor - INFO - 🖨️ Drucker-Monitor initialisiert
|
||||
2025-01-05 10:30:01 - printer_monitor - INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart...
|
||||
2025-01-05 10:30:02 - printer_monitor - INFO - ✅ Printer 1: Steckdose ausgeschaltet
|
||||
2025-01-05 10:30:03 - printer_monitor - WARNING - ❌ Printer 3: Steckdose konnte nicht ausgeschaltet werden
|
||||
2025-01-05 10:30:05 - printer_monitor - INFO - 🎯 Steckdosen-Initialisierung abgeschlossen: 5/6 erfolgreich
|
||||
```
|
||||
|
||||
## Performance-Optimierungen
|
||||
|
||||
### Threading
|
||||
- **Parallele Drucker-Abfragen** mit ThreadPoolExecutor
|
||||
- **Maximale Worker**: min(anzahl_drucker, 8)
|
||||
- **Timeout-Handling** für hängende Anfragen
|
||||
|
||||
### Caching-Strategie
|
||||
1. **Session-Cache** (30s): Sofortige Antworten für wiederholte Anfragen
|
||||
2. **DB-Cache** (5min): Reduziert Datenbankzugriffe
|
||||
3. **Adaptive TTL**: Längere Cache-Zeiten bei Fehlern
|
||||
|
||||
### Netzwerk-Optimierungen
|
||||
- **Kurze Ping-Timeouts** (3s) für schnelle Konnektivitätsprüfung
|
||||
- **HTTP-Timeouts** (5-7s) für Smart-Plug-APIs
|
||||
- **Exponential Backoff** bei wiederholten Fehlern
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
### Automatische Wiederherstellung
|
||||
- **Fehler-Zähler** mit maximal 3 Versuchen
|
||||
- **Intervall-Erhöhung** bei wiederholten Fehlern
|
||||
- **Cache-Fallback** bei Netzwerkproblemen
|
||||
|
||||
### Graceful Degradation
|
||||
- **Teilweise Ergebnisse** bei einzelnen Drucker-Fehlern
|
||||
- **Letzte bekannte Werte** aus Cache bei Totalausfall
|
||||
- **Benutzerbenachrichtigung** über Systemprobleme
|
||||
|
||||
## Wartung und Monitoring
|
||||
|
||||
### System-Health-Checks
|
||||
```bash
|
||||
# Cache-Status prüfen
|
||||
curl -X GET /api/printers/monitor/summary
|
||||
|
||||
# Cache leeren
|
||||
curl -X POST /api/printers/monitor/clear-cache
|
||||
|
||||
# Vollständige Aktualisierung
|
||||
curl -X GET "/api/printers/monitor/live-status?use_cache=false"
|
||||
```
|
||||
|
||||
### Database-Wartung
|
||||
```sql
|
||||
-- Alte last_checked Werte bereinigen
|
||||
UPDATE printers SET last_checked = NULL WHERE last_checked < datetime('now', '-1 day');
|
||||
|
||||
-- Drucker-Status-Statistiken
|
||||
SELECT status, COUNT(*) FROM printers GROUP BY status;
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Häufige Probleme
|
||||
|
||||
#### Problem: Drucker werden als "unreachable" angezeigt
|
||||
**Lösung:**
|
||||
1. Netzwerkverbindung prüfen
|
||||
2. IP-Adressen in Datenbank validieren
|
||||
3. Firewall-Regeln überprüfen
|
||||
|
||||
#### Problem: Steckdosen-Initialisierung schlägt fehl
|
||||
**Lösung:**
|
||||
1. Smart-Plug-Konfiguration prüfen (IP, Username, Password)
|
||||
2. API-Endpunkte testen
|
||||
3. Timeout-Werte erhöhen
|
||||
|
||||
#### Problem: Cache funktioniert nicht
|
||||
**Lösung:**
|
||||
1. Session-Konfiguration prüfen
|
||||
2. Cache manuell leeren
|
||||
3. Speicher-Limits überprüfen
|
||||
|
||||
### Debug-Befehle
|
||||
```python
|
||||
# Einzelnen Drucker testen
|
||||
from utils.printer_monitor import printer_monitor
|
||||
status = printer_monitor._check_single_printer_status(printer)
|
||||
|
||||
# Cache-Inhalt prüfen
|
||||
print(printer_monitor.db_cache)
|
||||
|
||||
# Steckdose manuell testen
|
||||
success = printer_monitor._turn_outlet_off("192.168.0.110", "user", "pass")
|
||||
```
|
Before Width: | Height: | Size: 304 KiB |
Before Width: | Height: | Size: 304 KiB |
2631
backend/app/package-lock.json
generated
@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "myp-platform",
|
||||
"version": "3.0.0",
|
||||
"description": "MYP Platform - 3D-Drucker Reservierungssystem mit TP-Link Tapo Steuerung",
|
||||
"main": "app.py",
|
||||
"scripts": {
|
||||
"build:css": "npx tailwindcss -i ./static/css/input.css -o ./static/css/tailwind.min.css --minify",
|
||||
"watch:css": "npx tailwindcss -i ./static/css/input.css -o ./static/css/tailwind.min.css --watch",
|
||||
"build": "npm install && npm run build:css",
|
||||
"start": "python app.py --port=5000"
|
||||
},
|
||||
"keywords": [
|
||||
"flask",
|
||||
"python",
|
||||
"tp-link",
|
||||
"tapo",
|
||||
"3d-printer",
|
||||
"smart-plug"
|
||||
],
|
||||
"author": "Mercedes-Benz",
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.32",
|
||||
"postcss-selector-parser": "^6.0.13",
|
||||
"tailwindcss": "^3.4.17"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@fullcalendar/core": "^6.1.10",
|
||||
"@fullcalendar/daygrid": "^6.1.10",
|
||||
"@fullcalendar/interaction": "^6.1.10",
|
||||
"@fullcalendar/list": "^6.1.10",
|
||||
"@fullcalendar/timegrid": "^6.1.10",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"chart.js": "^4.4.9",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
# MYP Platform - Python Dependencies
|
||||
# Basierend auf tatsächlich verwendeten Imports in app.py
|
||||
# Automatisch generiert am: 2025-05-29 19:41:49
|
||||
# Installiere mit: pip install -r requirements.txt
|
||||
|
||||
# ===== CORE FLASK FRAMEWORK =====
|
||||
# Direkt in app.py verwendet
|
||||
Flask==3.1.1
|
||||
Flask-Login==0.6.3
|
||||
Flask-WTF==1.2.1
|
||||
|
||||
# ===== DATENBANK =====
|
||||
# SQLAlchemy für Datenbankoperationen (models.py, app.py)
|
||||
SQLAlchemy==2.0.36
|
||||
|
||||
# ===== SICHERHEIT UND AUTHENTIFIZIERUNG =====
|
||||
# Werkzeug für Passwort-Hashing und Utilities (app.py)
|
||||
bcrypt==4.2.1
|
||||
cryptography==44.0.0
|
||||
Werkzeug==3.1.3
|
||||
|
||||
# ===== SMART PLUG STEUERUNG =====
|
||||
# PyP100 für TP-Link Tapo Smart Plugs (utils/job_scheduler.py)
|
||||
PyP100
|
||||
|
||||
# ===== RATE LIMITING UND CACHING =====
|
||||
# Redis für Rate Limiting (utils/rate_limiter.py) - optional
|
||||
redis==5.2.1
|
||||
|
||||
# ===== HTTP REQUESTS =====
|
||||
# Requests für HTTP-Anfragen (utils/queue_manager.py, utils/debug_drucker_erkennung.py)
|
||||
requests==2.32.3
|
||||
|
||||
# ===== TEMPLATE ENGINE =====
|
||||
# Jinja2 und MarkupSafe (automatisch mit Flask installiert, aber explizit für utils/template_helpers.py)
|
||||
MarkupSafe==3.0.2
|
||||
|
||||
# ===== SYSTEM MONITORING =====
|
||||
# psutil für System-Monitoring (utils/debug_utils.py, utils/debug_cli.py)
|
||||
psutil==6.1.1
|
||||
|
||||
# ===== ZUSÄTZLICHE CORE ABHÄNGIGKEITEN =====
|
||||
# Click für CLI-Kommandos (automatisch mit Flask)
|
||||
# Keine Core-Requirements
|
||||
|
||||
# ===== WINDOWS-SPEZIFISCHE ABHÄNGIGKEITEN =====
|
||||
# Nur für Windows-Systeme erforderlich
|
||||
# Keine Windows-Requirements
|
||||
|
||||
# ===== OPTIONAL: ENTWICKLUNG UND TESTING =====
|
||||
# Nur für Entwicklungsumgebung
|
||||
pytest==8.3.4; extra == "dev"
|
||||
pytest-cov==6.0.0; extra == "dev"
|
||||
|
||||
# ===== OPTIONAL: PRODUKTIONS-SERVER =====
|
||||
# Gunicorn für Produktionsumgebung
|
||||
gunicorn==23.0.0; extra == "prod"
|
@ -1,604 +0,0 @@
|
||||
/**
|
||||
* MYP Platform Komponenten-Bibliothek
|
||||
* Erweiterte UI-Komponenten basierend auf Tailwind CSS
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
/* Professionelle Mercedes-Benz Karten und Container */
|
||||
.card {
|
||||
@apply bg-white dark:bg-slate-900 rounded-xl shadow-lg border border-slate-200 dark:border-slate-700 p-6 m-4 transition-all duration-300;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
@apply hover:shadow-xl hover:shadow-slate-300/50 dark:hover:shadow-slate-900/50 hover:bg-slate-50 dark:hover:bg-slate-800 transform hover:-translate-y-1 transition-all duration-300;
|
||||
}
|
||||
|
||||
.container-panel {
|
||||
@apply bg-slate-50 dark:bg-slate-800 rounded-xl p-6 m-4 border border-slate-200 dark:border-slate-700 shadow-sm;
|
||||
}
|
||||
|
||||
/* Professionelle Formulare */
|
||||
.form-input {
|
||||
@apply w-full rounded-xl border-2 border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-4 py-3 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:border-blue-500 dark:focus:border-blue-400 focus:ring-4 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 transition-all duration-300;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2 transition-colors duration-300;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
@apply mb-6;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
@apply mt-1 text-xs text-slate-500 dark:text-slate-400 transition-colors duration-300;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
@apply mt-1 text-xs text-red-600 dark:text-red-400 font-medium transition-colors duration-300;
|
||||
}
|
||||
|
||||
/* Professionelle Buttons */
|
||||
.btn-icon {
|
||||
@apply inline-flex items-center justify-center rounded-xl p-3 transition-all duration-300 shadow-md hover:shadow-lg;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-xl px-6 py-3 text-sm font-semibold transition-all duration-300 shadow-md hover:shadow-lg;
|
||||
}
|
||||
|
||||
.btn-rounded {
|
||||
@apply rounded-full;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@apply px-4 py-2 text-xs;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
@apply px-8 py-4 text-base;
|
||||
}
|
||||
|
||||
/* Professionelle Badges und Tags */
|
||||
.badge {
|
||||
@apply inline-flex items-center rounded-full px-3 py-1.5 text-xs font-semibold transition-all duration-300 shadow-sm;
|
||||
}
|
||||
|
||||
.badge-blue {
|
||||
@apply bg-blue-100 text-blue-800 border border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700;
|
||||
}
|
||||
|
||||
.badge-green {
|
||||
@apply bg-green-100 text-green-800 border border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700;
|
||||
}
|
||||
|
||||
.badge-red {
|
||||
@apply bg-red-100 text-red-800 border border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-700;
|
||||
}
|
||||
|
||||
.badge-yellow {
|
||||
@apply bg-yellow-100 text-yellow-800 border border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-300 dark:border-yellow-700;
|
||||
}
|
||||
|
||||
.badge-purple {
|
||||
@apply bg-purple-100 text-purple-800 border border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-700;
|
||||
}
|
||||
|
||||
/* Erweiterte Status Anzeigen */
|
||||
.status-dot {
|
||||
@apply relative flex h-3 w-3 rounded-full shadow-sm;
|
||||
}
|
||||
|
||||
.status-dot::after {
|
||||
@apply absolute top-0 left-0 h-full w-full rounded-full content-[''] animate-ping opacity-75;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
@apply bg-green-500 dark:bg-green-400;
|
||||
}
|
||||
|
||||
.status-online::after {
|
||||
@apply bg-green-500 dark:bg-green-400;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
@apply bg-red-500 dark:bg-red-400;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
@apply bg-yellow-500 dark:bg-yellow-400;
|
||||
}
|
||||
|
||||
.status-warning::after {
|
||||
@apply bg-yellow-500 dark:bg-yellow-400;
|
||||
}
|
||||
|
||||
/* Professionelle Tabellen */
|
||||
.table-container {
|
||||
@apply w-full overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-700 shadow-lg bg-white dark:bg-slate-900;
|
||||
}
|
||||
|
||||
.table-styled {
|
||||
@apply w-full whitespace-nowrap text-left text-sm text-slate-700 dark:text-slate-300;
|
||||
}
|
||||
|
||||
.table-styled thead {
|
||||
@apply bg-slate-100 dark:bg-slate-800 transition-colors duration-300;
|
||||
}
|
||||
|
||||
.table-styled th {
|
||||
@apply px-6 py-4 font-semibold text-slate-900 dark:text-white transition-colors duration-300;
|
||||
}
|
||||
|
||||
.table-styled tbody tr {
|
||||
@apply border-t border-slate-200 dark:border-slate-700 transition-colors duration-300;
|
||||
}
|
||||
|
||||
.table-styled tbody tr:hover {
|
||||
@apply bg-slate-50 dark:bg-slate-800/50 transition-colors duration-300;
|
||||
}
|
||||
|
||||
.table-styled td {
|
||||
@apply px-6 py-4 transition-colors duration-300;
|
||||
}
|
||||
|
||||
/* Professionelle Alert und Toast */
|
||||
.alert {
|
||||
@apply rounded-xl border-2 p-6 mb-4 transition-all duration-300 shadow-lg;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
@apply bg-blue-50 dark:bg-blue-900/20 border-blue-300 dark:border-blue-600 text-blue-900 dark:text-blue-200;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
@apply bg-green-50 dark:bg-green-900/20 border-green-300 dark:border-green-600 text-green-900 dark:text-green-200;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
@apply bg-yellow-50 dark:bg-yellow-900/20 border-yellow-300 dark:border-yellow-600 text-yellow-900 dark:text-yellow-200;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
@apply bg-red-50 dark:bg-red-900/20 border-red-300 dark:border-red-600 text-red-900 dark:text-red-200;
|
||||
}
|
||||
|
||||
/* Professionelle Navigation */
|
||||
.nav-tab {
|
||||
@apply inline-flex items-center gap-2 px-6 py-3 border-b-2 text-sm font-semibold transition-all duration-300;
|
||||
}
|
||||
|
||||
.nav-tab-active {
|
||||
@apply border-blue-600 dark:border-blue-400 text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 rounded-t-lg;
|
||||
}
|
||||
|
||||
.nav-tab-inactive {
|
||||
@apply border-transparent text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 hover:border-slate-300 dark:hover:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-t-lg;
|
||||
}
|
||||
|
||||
/* Professionelle Navigation Links */
|
||||
.nav-link {
|
||||
@apply flex items-center gap-3 px-4 py-3 rounded-xl text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-white transition-all duration-300 font-medium;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
@apply bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-semibold shadow-sm;
|
||||
}
|
||||
|
||||
/* Erweiterte Printer Status */
|
||||
.printer-status {
|
||||
@apply inline-flex items-center gap-2 px-4 py-2 rounded-full text-xs font-semibold shadow-sm border;
|
||||
}
|
||||
|
||||
.printer-ready {
|
||||
@apply bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-700;
|
||||
}
|
||||
|
||||
.printer-busy {
|
||||
@apply bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-300 border-orange-200 dark:border-orange-700;
|
||||
}
|
||||
|
||||
.printer-error {
|
||||
@apply bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-700;
|
||||
}
|
||||
|
||||
.printer-offline {
|
||||
@apply bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-600;
|
||||
}
|
||||
|
||||
.printer-maintenance {
|
||||
@apply bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 border-purple-200 dark:border-purple-700;
|
||||
}
|
||||
|
||||
/* Erweiterte Job Status */
|
||||
.job-status {
|
||||
@apply inline-flex items-center gap-2 px-4 py-2 rounded-full text-xs font-semibold shadow-sm border;
|
||||
}
|
||||
|
||||
.job-queued {
|
||||
@apply bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-600;
|
||||
}
|
||||
|
||||
.job-printing {
|
||||
@apply bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-700;
|
||||
}
|
||||
|
||||
.job-completed {
|
||||
@apply bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-700;
|
||||
}
|
||||
|
||||
.job-failed {
|
||||
@apply bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-700;
|
||||
}
|
||||
|
||||
.job-cancelled {
|
||||
@apply bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 border-yellow-200 dark:border-yellow-700;
|
||||
}
|
||||
|
||||
.job-paused {
|
||||
@apply bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 border-purple-200 dark:border-purple-700;
|
||||
}
|
||||
|
||||
/* Professionelle Buttons für beide Modi */
|
||||
.btn {
|
||||
@apply px-6 py-3 rounded-xl transition-all duration-300 focus:outline-none focus:ring-4 shadow-lg hover:shadow-xl font-semibold;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply btn bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500/50 shadow-blue-500/25;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply btn bg-slate-200 hover:bg-slate-300 text-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600 dark:text-white focus:ring-slate-500/50;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply btn bg-red-600 hover:bg-red-700 text-white focus:ring-red-500/50 shadow-red-500/25;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
@apply btn bg-green-600 hover:bg-green-700 text-white focus:ring-green-500/50 shadow-green-500/25;
|
||||
}
|
||||
|
||||
/* Professionelle Mercedes-Benz Design-Komponenten */
|
||||
|
||||
/* Glassmorphism - Verbessert für beide Modi */
|
||||
.mercedes-glass {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark .mercedes-glass {
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Professionelle Gradients - Strikt getrennt */
|
||||
.professional-gradient {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 25%, #cbd5e1 50%, #94a3b8 75%, #64748b 100%);
|
||||
}
|
||||
|
||||
.dark .professional-gradient {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 25%, #334155 50%, #475569 75%, #64748b 100%);
|
||||
}
|
||||
|
||||
/* Mercedes-Pattern - Verbessert */
|
||||
.mercedes-pattern {
|
||||
background-image:
|
||||
radial-gradient(circle at 25% 25%, rgba(255,255,255,0.1) 2px, transparent 2px),
|
||||
radial-gradient(circle at 75% 75%, rgba(255,255,255,0.1) 2px, transparent 2px);
|
||||
background-size: 60px 60px;
|
||||
}
|
||||
|
||||
.dark .mercedes-pattern {
|
||||
background-image:
|
||||
radial-gradient(circle at 25% 25%, rgba(255,255,255,0.05) 2px, transparent 2px),
|
||||
radial-gradient(circle at 75% 75%, rgba(255,255,255,0.05) 2px, transparent 2px);
|
||||
background-size: 60px 60px;
|
||||
}
|
||||
|
||||
/* Professionelle Schatten - Kontextabhängig */
|
||||
.professional-shadow {
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.15),
|
||||
0 8px 16px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.dark .professional-shadow {
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.5),
|
||||
0 8px 16px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Professionelle Button Styles - Erweitert */
|
||||
.professional-button {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.dark .professional-button {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.professional-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.professional-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.professional-button:hover {
|
||||
background: linear-gradient(135deg, #1d4ed8 0%, #1e40af 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 15px 35px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.dark .professional-button:hover {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||
box-shadow: 0 15px 35px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Professionelle Input Fields - Erweitert */
|
||||
.input-field {
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(203, 213, 225, 0.8);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dark .input-field {
|
||||
background: rgba(51, 65, 85, 0.95);
|
||||
border: 2px solid rgba(71, 85, 105, 0.8);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.15);
|
||||
border-color: #3b82f6;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.dark .input-field:focus {
|
||||
background: rgba(51, 65, 85, 1);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Professionelle Cards - Erweitert */
|
||||
.professional-card {
|
||||
border-radius: 1.5rem;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(203, 213, 225, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.dark .professional-card {
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
border: 1px solid rgba(71, 85, 105, 0.5);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.professional-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.dark .professional-card:hover {
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Professionelle Navigation Verbesserungen */
|
||||
.nav-item {
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.nav-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #3b82f6, #1d4ed8);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nav-item:hover::after,
|
||||
.nav-item.active::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Verbesserte Header-Stile */
|
||||
.hero-header {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
border: 1px solid rgba(203, 213, 225, 0.5);
|
||||
}
|
||||
|
||||
.dark .hero-header {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
border: 1px solid rgba(71, 85, 105, 0.5);
|
||||
}
|
||||
|
||||
/* Verbesserte Container */
|
||||
.main-container {
|
||||
background: rgba(248, 250, 252, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.dark .main-container {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
}
|
||||
|
||||
/* Professionelle Status Badges - Erweitert */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
border-radius: 9999px;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.status-badge:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Smooth Transitions für alle Elemente */
|
||||
* {
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease,
|
||||
color 0.3s ease,
|
||||
box-shadow 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Interactive Hover Effects */
|
||||
.interactive-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.interactive-hover:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Light Mode spezifische Hover-Effekte */
|
||||
.interactive-hover:hover {
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.dark .interactive-hover:hover {
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Professional Loading States */
|
||||
.loading-shimmer {
|
||||
background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.dark .loading-shimmer {
|
||||
background: linear-gradient(90deg, #334155 25%, #475569 50%, #334155 75%);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Focus Indicators für Accessibility */
|
||||
.focus-ring:focus {
|
||||
outline: 3px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.dark .focus-ring:focus {
|
||||
outline: 3px solid #60a5fa;
|
||||
}
|
||||
|
||||
/* Professionelle Typography */
|
||||
.professional-title {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #475569 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.dark .professional-title {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* Responsives Design für kleine Bildschirme */
|
||||
@media (max-width: 768px) {
|
||||
.professional-shadow {
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.professional-card {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.mercedes-glass {
|
||||
backdrop-filter: blur(15px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animationen für bessere UX */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Root Variablen für konsistente Farben */
|
||||
:root {
|
||||
--mercedes-primary: #3b82f6;
|
||||
--mercedes-secondary: #64748b;
|
||||
--mercedes-accent: #1d4ed8;
|
||||
--shadow-light: rgba(0, 0, 0, 0.1);
|
||||
--shadow-dark: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--shadow-light: rgba(0, 0, 0, 0.2);
|
||||
--shadow-dark: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
@ -1,237 +0,0 @@
|
||||
/* Enhanced Glassmorphism Effects for MYP Application */
|
||||
|
||||
/* Base Glass Effects */
|
||||
.glass-base {
|
||||
backdrop-filter: blur(20px) saturate(180%) brightness(110%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%) brightness(110%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.glass-strong {
|
||||
backdrop-filter: blur(24px) saturate(200%) brightness(120%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(200%) brightness(120%);
|
||||
box-shadow: 0 35px 60px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.glass-subtle {
|
||||
backdrop-filter: blur(16px) saturate(150%) brightness(105%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(150%) brightness(105%);
|
||||
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Light Mode Glass */
|
||||
.glass-light {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.glass-light-strong {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Dark Mode Glass */
|
||||
.glass-dark {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.glass-dark-strong {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 35px 60px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* Interactive Glass Elements */
|
||||
.glass-interactive {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.glass-interactive:hover {
|
||||
transform: translateY(-2px);
|
||||
backdrop-filter: blur(28px) saturate(220%) brightness(125%);
|
||||
-webkit-backdrop-filter: blur(28px) saturate(220%) brightness(125%);
|
||||
box-shadow: 0 40px 80px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Glass Navigation */
|
||||
.glass-nav {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(24px) saturate(200%) brightness(120%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(200%) brightness(120%);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dark .glass-nav {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Glass Cards */
|
||||
.glass-card-enhanced {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px) saturate(180%) brightness(110%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%) brightness(110%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .glass-card-enhanced {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.glass-card-enhanced:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 35px 70px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.dark .glass-card-enhanced:hover {
|
||||
box-shadow: 0 35px 70px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Glass Buttons */
|
||||
.glass-btn {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(16px) saturate(150%) brightness(110%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(150%) brightness(110%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dark .glass-btn {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.glass-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
backdrop-filter: blur(20px) saturate(170%) brightness(115%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(170%) brightness(115%);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Glass Modals */
|
||||
.glass-modal {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(32px) saturate(200%) brightness(115%);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(200%) brightness(115%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 50px 100px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.dark .glass-modal {
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 50px 100px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Glass Form Elements */
|
||||
.glass-input {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(16px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(150%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dark .glass-input {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.glass-input:focus {
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
/* Glass Dropdown */
|
||||
.glass-dropdown {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(24px) saturate(200%) brightness(120%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(200%) brightness(120%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dark .glass-dropdown {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* Animation for glass elements */
|
||||
@keyframes glassFloat {
|
||||
0%, 100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.glass-float {
|
||||
animation: glassFloat 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Glass overlay for backgrounds */
|
||||
.glass-overlay {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
|
||||
backdrop-filter: blur(40px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(200%);
|
||||
}
|
||||
|
||||
.dark .glass-overlay {
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.1) 100%);
|
||||
}
|
||||
|
||||
/* Responsive glass effects */
|
||||
@media (max-width: 768px) {
|
||||
.glass-base,
|
||||
.glass-strong,
|
||||
.glass-card-enhanced {
|
||||
backdrop-filter: blur(16px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(150%);
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode adjustments */
|
||||
@media (prefers-contrast: high) {
|
||||
.glass-base,
|
||||
.glass-strong,
|
||||
.glass-card-enhanced {
|
||||
border-width: 2px;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.glass-interactive,
|
||||
.glass-card-enhanced,
|
||||
.glass-btn {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.glass-float {
|
||||
animation: none;
|
||||
}
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
/* Erweiterte Drucker-Styles für MYP Platform */
|
||||
|
||||
/* Filter-Button-Styles */
|
||||
.filter-btn {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background-color: white;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.dark .filter-btn.active {
|
||||
background-color: #475569;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
/* Online-Drucker-Hervorhebung */
|
||||
.printer-card-online {
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #ffffff 100%);
|
||||
border-color: #bbf7d0;
|
||||
box-shadow: 0 1px 3px 0 rgba(34, 197, 94, 0.1), 0 1px 2px 0 rgba(34, 197, 94, 0.06);
|
||||
}
|
||||
|
||||
.dark .printer-card-online {
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.1) 0%, #1e293b 100%);
|
||||
border-color: #166534;
|
||||
box-shadow: 0 1px 3px 0 rgba(34, 197, 94, 0.2), 0 1px 2px 0 rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.printer-card-online:hover {
|
||||
box-shadow: 0 4px 6px -1px rgba(34, 197, 94, 0.2), 0 2px 4px -1px rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.dark .printer-card-online:hover {
|
||||
box-shadow: 0 4px 6px -1px rgba(34, 197, 94, 0.3), 0 2px 4px -1px rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
/* Online-Indikator-Animation */
|
||||
.online-indicator {
|
||||
animation: pulse-green 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-green {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
|
||||
}
|
||||
50% {
|
||||
opacity: .8;
|
||||
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Status-Übersicht-Animationen */
|
||||
.status-count-change {
|
||||
animation: count-change 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes count-change {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Auto-Refresh-Button-Animationen */
|
||||
.auto-refresh-active {
|
||||
background: linear-gradient(45deg, #10b981, #059669);
|
||||
animation: gradient-shift 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
/* Drucker-Karten-Übergangseffekte */
|
||||
.printer-card {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.printer-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Loading-Spinner für Live-Updates */
|
||||
.live-update-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive Verbesserungen */
|
||||
@media (max-width: 640px) {
|
||||
.filter-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.status-overview {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.printer-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Verbesserungen */
|
||||
.dark .printer-card {
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
.dark .printer-card:hover {
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
/* Accessibility Verbesserungen */
|
||||
.filter-btn:focus {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.printer-card:focus-within {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Print-Styles */
|
||||
@media print {
|
||||
.filter-btn,
|
||||
.auto-refresh-btn,
|
||||
.printer-detail-btn,
|
||||
.delete-printer-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.printer-card {
|
||||
break-inside: avoid;
|
||||
box-shadow: none;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* High Contrast Mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.printer-card-online {
|
||||
border: 2px solid #059669;
|
||||
}
|
||||
|
||||
.online-indicator {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.online-indicator,
|
||||
.auto-refresh-active,
|
||||
.live-update-spinner {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.printer-card {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.printer-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
@ -1,983 +0,0 @@
|
||||
/**
|
||||
* Mercedes-Benz MYP Platform - Professional Theme
|
||||
* Professionelle Light/Dark Mode Implementierung
|
||||
*/
|
||||
|
||||
/* Globale CSS-Variablen für konsistente Theming */
|
||||
:root {
|
||||
/* Mercedes-Benz Markenfarben */
|
||||
--mb-primary: #3b82f6;
|
||||
--mb-primary-dark: #1d4ed8;
|
||||
--mb-secondary: #64748b;
|
||||
--mb-accent: #0ea5e9;
|
||||
|
||||
/* Light Mode Farbpalette */
|
||||
--light-bg-primary: #ffffff;
|
||||
--light-bg-secondary: #f8fafc;
|
||||
--light-bg-tertiary: #f1f5f9;
|
||||
--light-surface: #ffffff;
|
||||
--light-surface-hover: #f8fafc;
|
||||
--light-text-primary: #0f172a;
|
||||
--light-text-secondary: #475569;
|
||||
--light-text-muted: #64748b;
|
||||
--light-border: #e2e8f0;
|
||||
--light-border-strong: #cbd5e1;
|
||||
--light-shadow: rgba(0, 0, 0, 0.1);
|
||||
--light-shadow-strong: rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* Dark Mode Farbpalette */
|
||||
--dark-bg-primary: #0f172a;
|
||||
--dark-bg-secondary: #1e293b;
|
||||
--dark-bg-tertiary: #334155;
|
||||
--dark-surface: #1e293b;
|
||||
--dark-surface-hover: #334155;
|
||||
--dark-text-primary: #f8fafc;
|
||||
--dark-text-secondary: #e2e8f0;
|
||||
--dark-text-muted: #94a3b8;
|
||||
--dark-border: #334155;
|
||||
--dark-border-strong: #475569;
|
||||
--dark-shadow: rgba(0, 0, 0, 0.3);
|
||||
--dark-shadow-strong: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Professionelle Hero-Header Stile */
|
||||
.professional-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 2rem;
|
||||
margin: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
background: linear-gradient(135deg, var(--light-bg-secondary) 0%, var(--light-bg-tertiary) 100%);
|
||||
border: 1px solid var(--light-border);
|
||||
box-shadow: 0 20px 40px var(--light-shadow);
|
||||
}
|
||||
|
||||
.dark .professional-hero {
|
||||
background: linear-gradient(135deg, var(--dark-bg-primary) 0%, var(--dark-bg-secondary) 100%);
|
||||
border: 1px solid var(--dark-border);
|
||||
box-shadow: 0 20px 40px var(--dark-shadow-strong);
|
||||
}
|
||||
|
||||
.professional-hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dark .professional-hero::before {
|
||||
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.05) 50%, transparent 70%);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Hero Pattern Overlay */
|
||||
.hero-pattern {
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 20%, var(--light-border) 1px, transparent 1px),
|
||||
radial-gradient(circle at 80% 80%, var(--light-border) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
background-position: 0 0, 25px 25px;
|
||||
}
|
||||
|
||||
.dark .hero-pattern {
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 20%, var(--dark-border) 1px, transparent 1px),
|
||||
radial-gradient(circle at 80% 80%, var(--dark-border) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
/* Professionelle Container */
|
||||
.professional-container {
|
||||
background: var(--light-surface);
|
||||
border: 1px solid var(--light-border);
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 10px 30px var(--light-shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .professional-container {
|
||||
background: var(--dark-surface);
|
||||
border: 1px solid var(--dark-border);
|
||||
box-shadow: 0 10px 30px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.professional-container:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px var(--light-shadow-strong);
|
||||
}
|
||||
|
||||
.dark .professional-container:hover {
|
||||
box-shadow: 0 20px 40px var(--dark-shadow-strong);
|
||||
}
|
||||
|
||||
/* Mercedes-Benz Glassmorphism Effekt */
|
||||
.mb-glass {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .mb-glass {
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.mb-glass:hover {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.dark .mb-glass:hover {
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Professional Buttons */
|
||||
.btn-professional {
|
||||
background: linear-gradient(135deg, var(--mb-primary) 0%, var(--mb-primary-dark) 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
padding: 0.75rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.025em;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.btn-professional::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn-professional:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-professional:hover {
|
||||
background: linear-gradient(135deg, var(--mb-primary-dark) 0%, #1e40af 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-professional:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Secondary Button Style */
|
||||
.btn-secondary-professional {
|
||||
background: var(--light-surface);
|
||||
color: var(--light-text-primary);
|
||||
border: 2px solid var(--light-border-strong);
|
||||
border-radius: 1rem;
|
||||
padding: 0.75rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px var(--light-shadow);
|
||||
}
|
||||
|
||||
.dark .btn-secondary-professional {
|
||||
background: var(--dark-surface);
|
||||
color: var(--dark-text-primary);
|
||||
border-color: var(--dark-border-strong);
|
||||
box-shadow: 0 4px 15px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.btn-secondary-professional:hover {
|
||||
background: var(--light-surface-hover);
|
||||
border-color: var(--mb-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px var(--light-shadow-strong);
|
||||
}
|
||||
|
||||
.dark .btn-secondary-professional:hover {
|
||||
background: var(--dark-surface-hover);
|
||||
box-shadow: 0 8px 25px var(--dark-shadow);
|
||||
}
|
||||
|
||||
/* Professional Input Fields */
|
||||
.input-professional {
|
||||
background: var(--light-surface);
|
||||
border: 2px solid var(--light-border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.875rem 1rem;
|
||||
color: var(--light-text-primary);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px var(--light-shadow);
|
||||
}
|
||||
|
||||
.dark .input-professional {
|
||||
background: var(--dark-surface);
|
||||
border-color: var(--dark-border);
|
||||
color: var(--dark-text-primary);
|
||||
box-shadow: 0 2px 8px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.input-professional:focus {
|
||||
border-color: var(--mb-primary);
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.input-professional::placeholder {
|
||||
color: var(--light-text-muted);
|
||||
}
|
||||
|
||||
.dark .input-professional::placeholder {
|
||||
color: var(--dark-text-muted);
|
||||
}
|
||||
|
||||
/* Professional Cards */
|
||||
.card-professional {
|
||||
background: var(--light-surface);
|
||||
border: 1px solid var(--light-border);
|
||||
border-radius: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 20px var(--light-shadow);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .card-professional {
|
||||
background: var(--dark-surface);
|
||||
border-color: var(--dark-border);
|
||||
box-shadow: 0 4px 20px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.card-professional::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--mb-primary), var(--mb-accent));
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card-professional:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.card-professional:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px var(--light-shadow-strong);
|
||||
}
|
||||
|
||||
.dark .card-professional:hover {
|
||||
box-shadow: 0 12px 40px var(--dark-shadow-strong);
|
||||
}
|
||||
|
||||
/* Professional Statistics Cards */
|
||||
.stat-card {
|
||||
background: var(--light-surface);
|
||||
border: 1px solid var(--light-border);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px var(--light-shadow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .stat-card {
|
||||
background: var(--dark-surface);
|
||||
border-color: var(--dark-border);
|
||||
box-shadow: 0 4px 15px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow: 0 8px 30px var(--light-shadow-strong);
|
||||
}
|
||||
|
||||
.dark .stat-card:hover {
|
||||
box-shadow: 0 8px 30px var(--dark-shadow-strong);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--light-text-primary);
|
||||
line-height: 1;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dark .stat-number {
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--light-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.dark .stat-label {
|
||||
color: var(--dark-text-muted);
|
||||
}
|
||||
|
||||
/* Professional Status Badges */
|
||||
.status-professional {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.status-professional:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Status-spezifische Farben */
|
||||
.status-pending {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
color: #92400e;
|
||||
border-color: rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
|
||||
.dark .status-pending {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #065f46;
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.dark .status-approved {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.status-denied {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #991b1b;
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.dark .status-denied {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Professional Typography */
|
||||
.title-professional {
|
||||
background: linear-gradient(135deg, var(--light-text-primary) 0%, var(--light-text-secondary) 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.dark .title-professional {
|
||||
background: linear-gradient(135deg, var(--dark-text-primary) 0%, var(--dark-text-secondary) 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle-professional {
|
||||
color: var(--light-text-muted);
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.dark .subtitle-professional {
|
||||
color: var(--dark-text-muted);
|
||||
}
|
||||
|
||||
/* Professional Navigation */
|
||||
.nav-professional {
|
||||
background: var(--light-surface);
|
||||
border: 1px solid var(--light-border);
|
||||
border-radius: 1rem;
|
||||
padding: 0.5rem;
|
||||
box-shadow: 0 4px 15px var(--light-shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.dark .nav-professional {
|
||||
background: var(--dark-surface);
|
||||
border-color: var(--dark-border);
|
||||
box-shadow: 0 4px 15px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.nav-item-professional {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--light-text-secondary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dark .nav-item-professional {
|
||||
color: var(--dark-text-secondary);
|
||||
}
|
||||
|
||||
.nav-item-professional:hover {
|
||||
background: var(--light-surface-hover);
|
||||
color: var(--light-text-primary);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.dark .nav-item-professional:hover {
|
||||
background: var(--dark-surface-hover);
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
.nav-item-professional.active {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--mb-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark .nav-item-professional.active {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Professional Tables */
|
||||
.table-professional {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--light-surface);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px var(--light-shadow);
|
||||
}
|
||||
|
||||
.dark .table-professional {
|
||||
background: var(--dark-surface);
|
||||
box-shadow: 0 4px 20px var(--dark-shadow);
|
||||
}
|
||||
|
||||
.table-professional th {
|
||||
background: var(--light-bg-secondary);
|
||||
color: var(--light-text-primary);
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--light-border);
|
||||
}
|
||||
|
||||
.dark .table-professional th {
|
||||
background: var(--dark-bg-secondary);
|
||||
color: var(--dark-text-primary);
|
||||
border-bottom-color: var(--dark-border);
|
||||
}
|
||||
|
||||
.table-professional td {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--light-border);
|
||||
color: var(--light-text-secondary);
|
||||
}
|
||||
|
||||
.dark .table-professional td {
|
||||
border-bottom-color: var(--dark-border);
|
||||
color: var(--dark-text-secondary);
|
||||
}
|
||||
|
||||
.table-professional tbody tr:hover {
|
||||
background: var(--light-surface-hover);
|
||||
}
|
||||
|
||||
.dark .table-professional tbody tr:hover {
|
||||
background: var(--dark-surface-hover);
|
||||
}
|
||||
|
||||
/* Professional Alerts */
|
||||
.alert-professional {
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid transparent;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 4px 15px var(--light-shadow);
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.dark .alert-info {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.dark .alert-success {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border-color: rgba(251, 191, 36, 0.3);
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.dark .alert-warning {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.dark .alert-error {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Background Gradients für verschiedene Seiten */
|
||||
.bg-professional {
|
||||
background: linear-gradient(135deg, var(--light-bg-primary) 0%, var(--light-bg-secondary) 50%, var(--light-bg-tertiary) 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dark .bg-professional {
|
||||
background: linear-gradient(135deg, var(--dark-bg-primary) 0%, var(--dark-bg-secondary) 50%, var(--dark-bg-tertiary) 100%);
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-professional-primary {
|
||||
color: var(--light-text-primary);
|
||||
}
|
||||
|
||||
.dark .text-professional-primary {
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
.text-professional-secondary {
|
||||
color: var(--light-text-secondary);
|
||||
}
|
||||
|
||||
.dark .text-professional-secondary {
|
||||
color: var(--dark-text-secondary);
|
||||
}
|
||||
|
||||
.text-professional-muted {
|
||||
color: var(--light-text-muted);
|
||||
}
|
||||
|
||||
.dark .text-professional-muted {
|
||||
color: var(--dark-text-muted);
|
||||
}
|
||||
|
||||
/* Censored Text for Privacy Protection */
|
||||
.censored-text {
|
||||
font-family: monospace;
|
||||
background: linear-gradient(45deg, var(--light-text-secondary), var(--light-text-muted));
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dark .censored-text {
|
||||
background: linear-gradient(45deg, var(--dark-text-secondary), var(--dark-text-muted));
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* Smooth transitions für alle professionellen Komponenten */
|
||||
.professional-hero,
|
||||
.professional-container,
|
||||
.mb-glass,
|
||||
.btn-professional,
|
||||
.btn-secondary-professional,
|
||||
.input-professional,
|
||||
.card-professional,
|
||||
.stat-card,
|
||||
.status-professional,
|
||||
.nav-professional,
|
||||
.nav-item-professional,
|
||||
.table-professional,
|
||||
.alert-professional {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.professional-hero {
|
||||
margin: 1rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.card-professional {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.btn-professional,
|
||||
.btn-secondary-professional {
|
||||
padding: 0.625rem 1.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation-Klassen */
|
||||
.animate-fade-in {
|
||||
animation: fadeInProfessional 0.6s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUpProfessional 0.6s ease-out;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scaleInProfessional 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInProfessional {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUpProfessional {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleInProfessional {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Professional Mini Button Styles */
|
||||
.btn-professional-mini {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--mb-primary);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dark .btn-professional-mini {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.btn-professional-mini:hover {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
border-color: var(--mb-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dark .btn-professional-mini:hover {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Danger Mini Button */
|
||||
.btn-danger-professional-mini {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dark .btn-danger-professional-mini {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger-professional-mini:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border-color: #dc2626;
|
||||
transform: translateY(-1px) scale(1.05);
|
||||
}
|
||||
|
||||
.dark .btn-danger-professional-mini:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
/* Success Button */
|
||||
.btn-success-professional {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
padding: 0.75rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.025em;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-success-professional::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn-success-professional:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-success-professional:hover {
|
||||
background: linear-gradient(135deg, #059669 0%, #047857 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.btn-success-professional:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Danger Button */
|
||||
.btn-danger-professional {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
padding: 0.75rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.025em;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-danger-professional::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn-danger-professional:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-danger-professional:hover {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.btn-danger-professional:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Filter Button Mercedes */
|
||||
.filter-btn-mercedes {
|
||||
background: transparent;
|
||||
color: var(--light-text-muted);
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dark .filter-btn-mercedes {
|
||||
color: var(--dark-text-muted);
|
||||
}
|
||||
|
||||
.filter-btn-mercedes:hover {
|
||||
color: var(--light-text-primary);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dark .filter-btn-mercedes:hover {
|
||||
color: var(--dark-text-primary);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.filter-btn-mercedes.active {
|
||||
background: var(--mb-primary);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Status Dots */
|
||||
.status-dot {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-dot.status-online {
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
.status-dot.status-offline {
|
||||
background: #ef4444;
|
||||
box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
.status-dot.status-online::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: inherit;
|
||||
animation: pulse-professional 2s infinite;
|
||||
}
|
||||
|
||||
/* Professional Shadow */
|
||||
.professional-shadow {
|
||||
box-shadow: 0 8px 32px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.dark .professional-shadow {
|
||||
box-shadow: 0 8px 32px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
/* Professional Accent Colors */
|
||||
.text-professional-accent {
|
||||
color: var(--mb-accent);
|
||||
}
|
||||
|
||||
.bg-professional-accent {
|
||||
background-color: var(--mb-accent);
|
||||
}
|
||||
|
||||
/* Animate Spin Slow */
|
||||
.animate-spin-slow {
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-professional {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
}
|
1
backend/app/static/css/tailwind.min.css
vendored
@ -1,21 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" width="80" height="80">
|
||||
<defs>
|
||||
<style>
|
||||
.mb-logo { fill: currentColor; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.mb-logo { fill: #ffffff; }
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
.mb-logo { fill: #000000; }
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- Mercedes-Benz Logo -->
|
||||
<path class="mb-logo" d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
|
||||
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
|
||||
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
|
||||
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
|
||||
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
|
||||
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1015 B |
Before Width: | Height: | Size: 551 B |
Before Width: | Height: | Size: 178 B |
Before Width: | Height: | Size: 271 B |
@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Icon-Generator für Mercedes-Benz MYP Platform
|
||||
Generiert PWA-Icons in verschiedenen Größen
|
||||
"""
|
||||
|
||||
import os
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
def create_mercedes_icon(size, output_path):
|
||||
"""
|
||||
Erstellt ein einfaches Mercedes-Benz-Logo-Icon
|
||||
|
||||
Args:
|
||||
size: Größe des Icons (quadratisch)
|
||||
output_path: Ausgabepfad für das Icon
|
||||
"""
|
||||
# Erstelle ein schwarzes quadratisches Bild
|
||||
img = Image.new('RGB', (size, size), color='#000000')
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Berechne Kreis-Dimensionen
|
||||
center = size // 2
|
||||
radius = int(size * 0.4) # 80% der Größe für den äußeren Kreis
|
||||
|
||||
# Äußerer weißer Kreis
|
||||
draw.ellipse([center - radius, center - radius, center + radius, center + radius],
|
||||
outline='#FFFFFF', width=max(2, size // 50))
|
||||
|
||||
# Mercedes-Stern (vereinfacht)
|
||||
star_radius = int(radius * 0.7)
|
||||
|
||||
# Drei Linien für den Mercedes-Stern
|
||||
# Linie nach oben
|
||||
draw.line([center, center, center, center - star_radius],
|
||||
fill='#FFFFFF', width=max(2, size // 40))
|
||||
|
||||
# Linie nach rechts unten (60°)
|
||||
import math
|
||||
angle1 = math.radians(60)
|
||||
x1 = center + int(star_radius * math.sin(angle1))
|
||||
y1 = center + int(star_radius * math.cos(angle1))
|
||||
draw.line([center, center, x1, y1],
|
||||
fill='#FFFFFF', width=max(2, size // 40))
|
||||
|
||||
# Linie nach links unten (120°)
|
||||
angle2 = math.radians(120)
|
||||
x2 = center + int(star_radius * math.sin(angle2))
|
||||
y2 = center + int(star_radius * math.cos(angle2))
|
||||
draw.line([center, center, x2, y2],
|
||||
fill='#FFFFFF', width=max(2, size // 40))
|
||||
|
||||
# Speichere das Bild
|
||||
img.save(output_path, 'PNG', optimize=True)
|
||||
print(f"✅ Icon erstellt: {output_path} ({size}x{size})")
|
||||
|
||||
def main():
|
||||
"""Generiert alle benötigten Icon-Größen"""
|
||||
sizes = [72, 96, 128, 144, 152, 192, 384, 512]
|
||||
|
||||
# Stelle sicher, dass das Verzeichnis existiert
|
||||
os.makedirs('static/icons', exist_ok=True)
|
||||
|
||||
for size in sizes:
|
||||
output_path = f'icon-{size}x{size}.png'
|
||||
try:
|
||||
create_mercedes_icon(size, output_path)
|
||||
except Exception as e:
|
||||
print(f"❌ Fehler beim Erstellen von {output_path}: {str(e)}")
|
||||
|
||||
print("\n🎯 Alle Icons wurden erfolgreich generiert!")
|
||||
print("📁 Verzeichnis: static/icons/")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Before Width: | Height: | Size: 718 B |
Before Width: | Height: | Size: 804 B |
Before Width: | Height: | Size: 887 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 435 B |
Before Width: | Height: | Size: 551 B |
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" width="192" height="192">
|
||||
<circle cx="96" cy="96" r="96" fill="#000000"/>
|
||||
<path d="M96 12c46.392 0 84 37.608 84 84s-37.608 84-84 84-84-37.608-84-84 37.608-84 84-84m0-12C42.98 0 0 42.98 0 96s42.98 96 96 96 96-42.98 96-96S149.02 0 96 0z" fill="#ffffff"/>
|
||||
<path d="M96 30l21.65 57.68L174 96l-56.35 8.32L96 162l-21.65-57.68L18 96l56.35-8.32L96 30z" fill="#ffffff"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 476 B |
@ -1,584 +0,0 @@
|
||||
/**
|
||||
* MYP Admin Dashboard
|
||||
* Core JavaScript für das Admin-Dashboard
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize navigation
|
||||
initNavigation();
|
||||
|
||||
// Initialize modal events
|
||||
initModals();
|
||||
|
||||
// Load initial data
|
||||
loadDashboardData();
|
||||
});
|
||||
|
||||
/**
|
||||
* Navigation Initialization
|
||||
*/
|
||||
function initNavigation() {
|
||||
// Desktop navigation
|
||||
const desktopNavItems = document.querySelectorAll('.admin-nav-item');
|
||||
desktopNavItems.forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const section = this.getAttribute('data-section');
|
||||
activateSection(section);
|
||||
updateActiveNavItem(this, desktopNavItems);
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile navigation
|
||||
const mobileNavItems = document.querySelectorAll('.mobile-nav-item');
|
||||
mobileNavItems.forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const section = this.getAttribute('data-section');
|
||||
activateSection(section);
|
||||
updateActiveNavItem(this, mobileNavItems);
|
||||
closeMobileNav();
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const closeMobileNavButton = document.getElementById('close-mobile-nav');
|
||||
|
||||
if (mobileMenuButton) {
|
||||
mobileMenuButton.addEventListener('click', openMobileNav);
|
||||
}
|
||||
|
||||
if (closeMobileNavButton) {
|
||||
closeMobileNavButton.addEventListener('click', closeMobileNav);
|
||||
}
|
||||
|
||||
// Setup hash navigation
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
if (window.location.hash) {
|
||||
handleHashChange();
|
||||
}
|
||||
}
|
||||
|
||||
function activateSection(section) {
|
||||
// Hide all sections
|
||||
document.querySelectorAll('.admin-section').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
el.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Show selected section
|
||||
const targetSection = document.getElementById(`${section}-section`);
|
||||
if (targetSection) {
|
||||
targetSection.classList.remove('hidden');
|
||||
targetSection.classList.add('active');
|
||||
|
||||
// Load section data if needed
|
||||
switch(section) {
|
||||
case 'dashboard':
|
||||
loadDashboardData();
|
||||
break;
|
||||
case 'users':
|
||||
loadUsers();
|
||||
break;
|
||||
case 'printers':
|
||||
loadPrinters();
|
||||
break;
|
||||
case 'scheduler':
|
||||
loadSchedulerStatus();
|
||||
break;
|
||||
case 'logs':
|
||||
loadLogs();
|
||||
break;
|
||||
}
|
||||
|
||||
// Update URL hash
|
||||
window.location.hash = section;
|
||||
}
|
||||
}
|
||||
|
||||
function updateActiveNavItem(activeItem, allItems) {
|
||||
// Remove active class from all items
|
||||
allItems.forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// Add active class to selected item
|
||||
activeItem.classList.add('active');
|
||||
}
|
||||
|
||||
function handleHashChange() {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash) {
|
||||
const navItem = document.querySelector(`.admin-nav-item[data-section="${hash}"]`);
|
||||
if (navItem) {
|
||||
activateSection(hash);
|
||||
updateActiveNavItem(navItem, document.querySelectorAll('.admin-nav-item'));
|
||||
|
||||
// Also update mobile nav
|
||||
const mobileNavItem = document.querySelector(`.mobile-nav-item[data-section="${hash}"]`);
|
||||
if (mobileNavItem) {
|
||||
updateActiveNavItem(mobileNavItem, document.querySelectorAll('.mobile-nav-item'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openMobileNav() {
|
||||
const mobileNav = document.getElementById('mobile-nav');
|
||||
if (mobileNav) {
|
||||
mobileNav.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeMobileNav() {
|
||||
const mobileNav = document.getElementById('mobile-nav');
|
||||
if (mobileNav) {
|
||||
mobileNav.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal Initialization
|
||||
*/
|
||||
function initModals() {
|
||||
// Delete modal
|
||||
const deleteModal = document.getElementById('delete-modal');
|
||||
const closeDeleteModalBtn = document.getElementById('close-delete-modal');
|
||||
const cancelDeleteBtn = document.getElementById('cancel-delete-btn');
|
||||
|
||||
if (closeDeleteModalBtn) {
|
||||
closeDeleteModalBtn.addEventListener('click', closeDeleteModal);
|
||||
}
|
||||
|
||||
if (cancelDeleteBtn) {
|
||||
cancelDeleteBtn.addEventListener('click', closeDeleteModal);
|
||||
}
|
||||
|
||||
// Toast notification
|
||||
const closeToastBtn = document.getElementById('close-toast');
|
||||
if (closeToastBtn) {
|
||||
closeToastBtn.addEventListener('click', closeToast);
|
||||
}
|
||||
|
||||
// Global refresh button
|
||||
const refreshAllBtn = document.getElementById('refresh-all-btn');
|
||||
if (refreshAllBtn) {
|
||||
refreshAllBtn.addEventListener('click', refreshAllData);
|
||||
}
|
||||
}
|
||||
|
||||
function showDeleteModal(message, onConfirm) {
|
||||
const modal = document.getElementById('delete-modal');
|
||||
const messageEl = document.getElementById('delete-message');
|
||||
const confirmBtn = document.getElementById('confirm-delete-btn');
|
||||
|
||||
if (modal && messageEl && confirmBtn) {
|
||||
messageEl.textContent = message;
|
||||
modal.classList.add('modal-show');
|
||||
|
||||
// Setup confirm button action
|
||||
confirmBtn.onclick = function() {
|
||||
closeDeleteModal();
|
||||
if (typeof onConfirm === 'function') {
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
const modal = document.getElementById('delete-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('modal-show');
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.getElementById('toast-notification');
|
||||
const messageEl = document.getElementById('toast-message');
|
||||
const iconEl = document.getElementById('toast-icon');
|
||||
|
||||
if (toast && messageEl && iconEl) {
|
||||
messageEl.textContent = message;
|
||||
|
||||
// Set icon based on type
|
||||
const iconSvg = getToastIcon(type);
|
||||
iconEl.innerHTML = iconSvg;
|
||||
|
||||
// Show toast
|
||||
toast.classList.add('toast-show');
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(closeToast, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function closeToast() {
|
||||
const toast = document.getElementById('toast-notification');
|
||||
if (toast) {
|
||||
toast.classList.remove('toast-show');
|
||||
}
|
||||
}
|
||||
|
||||
function getToastIcon(type) {
|
||||
switch(type) {
|
||||
case 'success':
|
||||
return '<svg class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>';
|
||||
case 'error':
|
||||
return '<svg class="h-6 w-6 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>';
|
||||
case 'warning':
|
||||
return '<svg class="h-6 w-6 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><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-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>';
|
||||
case 'info':
|
||||
default:
|
||||
return '<svg class="h-6 w-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard Data Loading
|
||||
*/
|
||||
function loadDashboardData() {
|
||||
// Load dashboard stats
|
||||
loadStats();
|
||||
|
||||
// Load recent activity
|
||||
loadRecentActivity();
|
||||
|
||||
// Load system status
|
||||
loadSystemStatus();
|
||||
|
||||
// Setup refresh buttons
|
||||
const refreshActivityBtn = document.getElementById('refresh-activity-btn');
|
||||
if (refreshActivityBtn) {
|
||||
refreshActivityBtn.addEventListener('click', loadRecentActivity);
|
||||
}
|
||||
|
||||
const refreshSystemBtn = document.getElementById('refresh-system-btn');
|
||||
if (refreshSystemBtn) {
|
||||
refreshSystemBtn.addEventListener('click', loadSystemStatus);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const response = await fetch('/api/stats');
|
||||
const data = await response.json();
|
||||
|
||||
// Update dashboard counters
|
||||
document.getElementById('total-users-count').textContent = data.total_users || 0;
|
||||
document.getElementById('total-printers-count').textContent = data.total_printers || 0;
|
||||
document.getElementById('active-jobs-count').textContent = data.active_jobs || 0;
|
||||
|
||||
// Update scheduler status
|
||||
updateSchedulerStatusIndicator(data.scheduler_status || false);
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error);
|
||||
showToast('Fehler beim Laden der Statistiken', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function updateSchedulerStatusIndicator(isRunning) {
|
||||
const statusText = document.getElementById('scheduler-status');
|
||||
const indicator = document.getElementById('scheduler-indicator');
|
||||
|
||||
if (statusText && indicator) {
|
||||
if (isRunning) {
|
||||
statusText.textContent = 'Aktiv';
|
||||
statusText.classList.add('text-green-600', 'dark:text-green-400');
|
||||
statusText.classList.remove('text-red-600', 'dark:text-red-400');
|
||||
|
||||
indicator.classList.add('bg-green-500');
|
||||
indicator.classList.remove('bg-red-500', 'bg-gray-300');
|
||||
} else {
|
||||
statusText.textContent = 'Inaktiv';
|
||||
statusText.classList.add('text-red-600', 'dark:text-red-400');
|
||||
statusText.classList.remove('text-green-600', 'dark:text-green-400');
|
||||
|
||||
indicator.classList.add('bg-red-500');
|
||||
indicator.classList.remove('bg-green-500', 'bg-gray-300');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecentActivity() {
|
||||
const container = document.getElementById('recent-activity-container');
|
||||
if (!container) return;
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = `
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<svg class="animate-spin h-8 w-8 text-accent-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/activity/recent');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.activities && data.activities.length > 0) {
|
||||
const activities = data.activities;
|
||||
const html = activities.map(activity => `
|
||||
<div class="p-3 rounded-lg bg-light-surface dark:bg-dark-surface border border-light-border dark:border-dark-border">
|
||||
<div class="flex items-start">
|
||||
<div class="w-2 h-2 rounded-full bg-accent-primary mt-2 mr-3 flex-shrink-0"></div>
|
||||
<div>
|
||||
<p class="text-sm text-light-text dark:text-dark-text">${activity.description}</p>
|
||||
<p class="text-xs text-light-text-muted dark:text-dark-text-muted mt-1">
|
||||
${formatDateTime(activity.timestamp)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = html;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<p class="text-light-text-muted dark:text-dark-text-muted">Keine Aktivitäten gefunden</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading activities:', error);
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<p class="text-red-600 dark:text-red-400">Fehler beim Laden der Aktivitäten</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSystemStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/stats');
|
||||
const data = await response.json();
|
||||
|
||||
// Update system stats
|
||||
document.getElementById('total-print-time').textContent =
|
||||
formatPrintTime(data.total_print_time_hours);
|
||||
document.getElementById('completed-jobs-count').textContent =
|
||||
data.total_jobs_completed || 0;
|
||||
document.getElementById('total-material-used').textContent =
|
||||
formatMaterialUsed(data.total_material_used);
|
||||
document.getElementById('last-updated-time').textContent =
|
||||
data.last_updated ? formatDateTime(data.last_updated) : '-';
|
||||
} catch (error) {
|
||||
console.error('Error loading system status:', error);
|
||||
showToast('Fehler beim Laden des Systemstatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function formatPrintTime(hours) {
|
||||
if (!hours) return '-';
|
||||
return `${hours} Stunden`;
|
||||
}
|
||||
|
||||
function formatMaterialUsed(grams) {
|
||||
if (!grams) return '-';
|
||||
return `${grams} g`;
|
||||
}
|
||||
|
||||
function formatDateTime(dateString) {
|
||||
if (!dateString) return '-';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Global refresh function
|
||||
*/
|
||||
function refreshAllData() {
|
||||
// Get active section
|
||||
const activeSection = document.querySelector('.admin-section.active');
|
||||
if (activeSection) {
|
||||
const sectionId = activeSection.id;
|
||||
const section = sectionId.replace('-section', '');
|
||||
|
||||
// Reload data based on active section
|
||||
switch(section) {
|
||||
case 'dashboard':
|
||||
loadDashboardData();
|
||||
break;
|
||||
case 'users':
|
||||
loadUsers();
|
||||
break;
|
||||
case 'printers':
|
||||
loadPrinters();
|
||||
break;
|
||||
case 'scheduler':
|
||||
loadSchedulerStatus();
|
||||
break;
|
||||
case 'logs':
|
||||
loadLogs();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
showToast('Daten aktualisiert', 'success');
|
||||
}
|
||||
/**
|
||||
* Benutzer laden und anzeigen
|
||||
*/
|
||||
function loadUsers() {
|
||||
const usersContainer = document.getElementById('users-container');
|
||||
if (!usersContainer) return;
|
||||
|
||||
// Lade-Animation anzeigen
|
||||
usersContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 dark:border-indigo-400"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Benutzer vom Server laden
|
||||
fetch('/api/users')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Fehler beim Laden der Benutzer');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
renderUsers(data.users);
|
||||
updateUserStatistics(data.users);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Benutzer:', error);
|
||||
usersContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-red-600 dark:text-red-400 text-xl mb-2">Fehler beim Laden der Benutzer</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">${error.message}</p>
|
||||
<button onclick="loadUsers()" class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drucker laden und anzeigen
|
||||
*/
|
||||
function loadPrinters() {
|
||||
const printersContainer = document.getElementById('printers-container');
|
||||
if (!printersContainer) return;
|
||||
|
||||
// Lade-Animation anzeigen
|
||||
printersContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 dark:border-indigo-400"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Drucker vom Server laden
|
||||
fetch('/api/printers')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Fehler beim Laden der Drucker');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
renderPrinters(data.printers);
|
||||
updatePrinterStatistics(data.printers);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Drucker:', error);
|
||||
printersContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-red-600 dark:text-red-400 text-xl mb-2">Fehler beim Laden der Drucker</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">${error.message}</p>
|
||||
<button onclick="loadPrinters()" class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduler-Status laden und anzeigen
|
||||
*/
|
||||
function loadSchedulerStatus() {
|
||||
const schedulerContainer = document.getElementById('scheduler-container');
|
||||
if (!schedulerContainer) return;
|
||||
|
||||
// Lade-Animation anzeigen
|
||||
schedulerContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 dark:border-indigo-400"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Scheduler-Status vom Server laden
|
||||
fetch('/api/scheduler/status')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Fehler beim Laden des Scheduler-Status');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
renderSchedulerStatus(data);
|
||||
updateSchedulerControls(data.active);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden des Scheduler-Status:', error);
|
||||
schedulerContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-red-600 dark:text-red-400 text-xl mb-2">Fehler beim Laden des Scheduler-Status</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">${error.message}</p>
|
||||
<button onclick="loadSchedulerStatus()" class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs laden und anzeigen
|
||||
*/
|
||||
function loadLogs() {
|
||||
const logsContainer = document.getElementById('logs-container');
|
||||
if (!logsContainer) return;
|
||||
|
||||
// Lade-Animation anzeigen
|
||||
logsContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 dark:border-indigo-400"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Logs vom Server laden
|
||||
fetch('/api/logs')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Fehler beim Laden der Logs');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
window.logsData = data.logs;
|
||||
window.filteredLogs = [...data.logs];
|
||||
renderLogs();
|
||||
updateLogStatistics();
|
||||
scrollLogsToBottom();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Logs:', error);
|
||||
logsContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-red-600 dark:text-red-400 text-xl mb-2">Fehler beim Laden der Logs</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">${error.message}</p>
|
||||
<button onclick="loadLogs()" class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
@ -1,875 +0,0 @@
|
||||
/**
|
||||
* Mercedes-Benz MYP Admin Guest Requests Management
|
||||
* Moderne Verwaltung von Gastaufträgen mit Live-Updates
|
||||
*/
|
||||
|
||||
// Globale Variablen
|
||||
let currentRequests = [];
|
||||
let filteredRequests = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 0;
|
||||
let totalRequests = 0;
|
||||
let refreshInterval = null;
|
||||
let csrfToken = '';
|
||||
|
||||
// API Base URL Detection - Korrigierte Version für CSP-Kompatibilität
|
||||
function detectApiBaseUrl() {
|
||||
// Für lokale Entwicklung und CSP-Kompatibilität immer relative URLs verwenden
|
||||
// Das verhindert CSP-Probleme mit connect-src
|
||||
return ''; // Leerer String für relative URLs
|
||||
}
|
||||
|
||||
const API_BASE_URL = detectApiBaseUrl();
|
||||
|
||||
// Initialisierung beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// CSRF Token abrufen
|
||||
csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||||
|
||||
// Event Listeners initialisieren
|
||||
initEventListeners();
|
||||
|
||||
// Daten initial laden
|
||||
loadGuestRequests();
|
||||
|
||||
// Auto-Refresh starten
|
||||
startAutoRefresh();
|
||||
|
||||
console.log('🎯 Admin Guest Requests Management geladen');
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Listeners initialisieren
|
||||
*/
|
||||
function initEventListeners() {
|
||||
// Search Input
|
||||
const searchInput = document.getElementById('search-requests');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', debounce(handleSearch, 300));
|
||||
}
|
||||
|
||||
// Status Filter
|
||||
const statusFilter = document.getElementById('status-filter');
|
||||
if (statusFilter) {
|
||||
statusFilter.addEventListener('change', handleFilterChange);
|
||||
}
|
||||
|
||||
// Sort Order
|
||||
const sortOrder = document.getElementById('sort-order');
|
||||
if (sortOrder) {
|
||||
sortOrder.addEventListener('change', handleSortChange);
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
loadGuestRequests();
|
||||
showNotification('🔄 Gastaufträge aktualisiert', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
const exportBtn = document.getElementById('export-btn');
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', handleExport);
|
||||
}
|
||||
|
||||
const bulkActionsBtn = document.getElementById('bulk-actions-btn');
|
||||
if (bulkActionsBtn) {
|
||||
bulkActionsBtn.addEventListener('click', showBulkActionsModal);
|
||||
}
|
||||
|
||||
// Select All Checkbox
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', handleSelectAll);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gastaufträge von der API laden
|
||||
*/
|
||||
async function loadGuestRequests() {
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/admin/guest-requests`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
currentRequests = data.requests || [];
|
||||
totalRequests = data.total || 0;
|
||||
|
||||
// Statistiken aktualisieren
|
||||
updateStats(data.stats || {});
|
||||
|
||||
// Tabelle aktualisieren
|
||||
applyFiltersAndSort();
|
||||
|
||||
console.log(`✅ ${currentRequests.length} Gastaufträge geladen`);
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Laden der Gastaufträge');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Gastaufträge:', error);
|
||||
showNotification('❌ Fehler beim Laden der Gastaufträge: ' + error.message, 'error');
|
||||
showEmptyState();
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiken aktualisieren
|
||||
*/
|
||||
function updateStats(stats) {
|
||||
const elements = {
|
||||
'pending-count': stats.pending || 0,
|
||||
'approved-count': stats.approved || 0,
|
||||
'rejected-count': stats.rejected || 0,
|
||||
'total-count': stats.total || 0
|
||||
};
|
||||
|
||||
Object.entries(elements).forEach(([id, value]) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
animateCounter(element, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Counter mit Animation
|
||||
*/
|
||||
function animateCounter(element, targetValue) {
|
||||
const currentValue = parseInt(element.textContent) || 0;
|
||||
const difference = targetValue - currentValue;
|
||||
const steps = 20;
|
||||
const stepValue = difference / steps;
|
||||
|
||||
let step = 0;
|
||||
const interval = setInterval(() => {
|
||||
step++;
|
||||
const value = Math.round(currentValue + (stepValue * step));
|
||||
element.textContent = value;
|
||||
|
||||
if (step >= steps) {
|
||||
clearInterval(interval);
|
||||
element.textContent = targetValue;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter und Sortierung anwenden
|
||||
*/
|
||||
function applyFiltersAndSort() {
|
||||
let requests = [...currentRequests];
|
||||
|
||||
// Status Filter
|
||||
const statusFilter = document.getElementById('status-filter')?.value;
|
||||
if (statusFilter && statusFilter !== 'all') {
|
||||
requests = requests.filter(req => req.status === statusFilter);
|
||||
}
|
||||
|
||||
// Such-Filter
|
||||
const searchTerm = document.getElementById('search-requests')?.value.toLowerCase();
|
||||
if (searchTerm) {
|
||||
requests = requests.filter(req =>
|
||||
req.name?.toLowerCase().includes(searchTerm) ||
|
||||
req.email?.toLowerCase().includes(searchTerm) ||
|
||||
req.file_name?.toLowerCase().includes(searchTerm) ||
|
||||
req.reason?.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
// Sortierung
|
||||
const sortOrder = document.getElementById('sort-order')?.value;
|
||||
requests.sort((a, b) => {
|
||||
switch (sortOrder) {
|
||||
case 'oldest':
|
||||
return new Date(a.created_at) - new Date(b.created_at);
|
||||
case 'priority':
|
||||
return getPriorityValue(b) - getPriorityValue(a);
|
||||
case 'newest':
|
||||
default:
|
||||
return new Date(b.created_at) - new Date(a.created_at);
|
||||
}
|
||||
});
|
||||
|
||||
filteredRequests = requests;
|
||||
renderRequestsTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prioritätswert für Sortierung berechnen
|
||||
*/
|
||||
function getPriorityValue(request) {
|
||||
const now = new Date();
|
||||
const created = new Date(request.created_at);
|
||||
const hoursOld = (now - created) / (1000 * 60 * 60);
|
||||
|
||||
let priority = 0;
|
||||
|
||||
// Status-basierte Priorität
|
||||
if (request.status === 'pending') priority += 10;
|
||||
else if (request.status === 'approved') priority += 5;
|
||||
|
||||
// Alter-basierte Priorität
|
||||
if (hoursOld > 24) priority += 5;
|
||||
else if (hoursOld > 8) priority += 3;
|
||||
else if (hoursOld > 2) priority += 1;
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests-Tabelle rendern
|
||||
*/
|
||||
function renderRequestsTable() {
|
||||
const tableBody = document.getElementById('requests-table-body');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
|
||||
if (!tableBody) return;
|
||||
|
||||
if (filteredRequests.length === 0) {
|
||||
tableBody.innerHTML = '';
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
hideEmptyState();
|
||||
|
||||
const requestsHtml = filteredRequests.map(request => createRequestRow(request)).join('');
|
||||
tableBody.innerHTML = requestsHtml;
|
||||
|
||||
// Event Listeners für neue Rows hinzufügen
|
||||
addRowEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request Row HTML erstellen
|
||||
*/
|
||||
function createRequestRow(request) {
|
||||
const statusColor = getStatusColor(request.status);
|
||||
const priorityLevel = getPriorityLevel(request);
|
||||
const timeAgo = getTimeAgo(request.created_at);
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors duration-200" data-request-id="${request.id}">
|
||||
<td class="px-6 py-4">
|
||||
<input type="checkbox" class="request-checkbox rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
||||
value="${request.id}">
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<div class="h-10 w-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
|
||||
${request.name ? request.name[0].toUpperCase() : 'G'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white">${escapeHtml(request.name || 'Unbekannt')}</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">${escapeHtml(request.email || 'Keine E-Mail')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-slate-900 dark:text-white font-medium">${escapeHtml(request.file_name || 'Keine Datei')}</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||
${request.duration_minutes ? `${request.duration_minutes} Min.` : 'Unbekannte Dauer'}
|
||||
${request.copies ? ` • ${request.copies} Kopien` : ''}
|
||||
</div>
|
||||
${request.reason ? `<div class="text-xs text-slate-400 dark:text-slate-500 mt-1 truncate max-w-xs">${escapeHtml(request.reason)}</div>` : ''}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${statusColor}">
|
||||
<span class="w-2 h-2 mr-1 rounded-full ${getStatusDot(request.status)}"></span>
|
||||
${getStatusText(request.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-slate-900 dark:text-white">${timeAgo}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">${formatDateTime(request.created_at)}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
${getPriorityBadge(priorityLevel)}
|
||||
${request.is_urgent ? '<span class="ml-2 text-red-500 text-xs">🔥 Dringend</span>' : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button onclick="showRequestDetail(${request.id})"
|
||||
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
|
||||
title="Details anzeigen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
${request.status === 'pending' ? `
|
||||
<button onclick="approveRequest(${request.id})"
|
||||
class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300 transition-colors"
|
||||
title="Genehmigen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button onclick="rejectRequest(${request.id})"
|
||||
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||
title="Ablehnen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
<button onclick="deleteRequest(${request.id})"
|
||||
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 transition-colors"
|
||||
title="Löschen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status-Helper-Funktionen
|
||||
*/
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
'pending': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
|
||||
'approved': 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
|
||||
'rejected': 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
|
||||
'expired': 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300'
|
||||
};
|
||||
return colors[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300';
|
||||
}
|
||||
|
||||
function getStatusDot(status) {
|
||||
const dots = {
|
||||
'pending': 'bg-yellow-400 dark:bg-yellow-300',
|
||||
'approved': 'bg-green-400 dark:bg-green-300',
|
||||
'rejected': 'bg-red-400 dark:bg-red-300',
|
||||
'expired': 'bg-gray-400 dark:bg-gray-300'
|
||||
};
|
||||
return dots[status] || 'bg-gray-400 dark:bg-gray-300';
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
const texts = {
|
||||
'pending': 'Wartend',
|
||||
'approved': 'Genehmigt',
|
||||
'rejected': 'Abgelehnt',
|
||||
'expired': 'Abgelaufen'
|
||||
};
|
||||
return texts[status] || status;
|
||||
}
|
||||
|
||||
function getPriorityLevel(request) {
|
||||
const priority = getPriorityValue(request);
|
||||
if (priority >= 15) return 'high';
|
||||
if (priority >= 8) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
function getPriorityBadge(level) {
|
||||
const badges = {
|
||||
'high': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300">🔴 Hoch</span>',
|
||||
'medium': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300">🟡 Mittel</span>',
|
||||
'low': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">🟢 Niedrig</span>'
|
||||
};
|
||||
return badges[level] || badges['low'];
|
||||
}
|
||||
|
||||
/**
|
||||
* CRUD-Operationen
|
||||
*/
|
||||
async function approveRequest(requestId) {
|
||||
if (!confirm('Möchten Sie diesen Gastauftrag wirklich genehmigen?')) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${requestId}/approve`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({}) // Leeres JSON-Objekt senden
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('✅ Gastauftrag erfolgreich genehmigt', 'success');
|
||||
loadGuestRequests();
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Genehmigen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Genehmigen:', error);
|
||||
showNotification('❌ Fehler beim Genehmigen: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectRequest(requestId) {
|
||||
const reason = prompt('Grund für die Ablehnung:');
|
||||
if (!reason) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${requestId}/reject`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ reason })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('✅ Gastauftrag erfolgreich abgelehnt', 'success');
|
||||
loadGuestRequests();
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Ablehnen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Ablehnen:', error);
|
||||
showNotification('❌ Fehler beim Ablehnen: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRequest(requestId) {
|
||||
if (!confirm('Möchten Sie diesen Gastauftrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${requestId}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('✅ Gastauftrag erfolgreich gelöscht', 'success');
|
||||
loadGuestRequests();
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Löschen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen:', error);
|
||||
showNotification('❌ Fehler beim Löschen: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail-Modal Funktionen
|
||||
*/
|
||||
function showRequestDetail(requestId) {
|
||||
const request = currentRequests.find(req => req.id === requestId);
|
||||
if (!request) return;
|
||||
|
||||
const modal = document.getElementById('detail-modal');
|
||||
const content = document.getElementById('modal-content');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Gastauftrag Details</h3>
|
||||
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<svg class="w-6 h-6" 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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Antragsteller</h4>
|
||||
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
|
||||
<p><strong>Name:</strong> ${escapeHtml(request.name || 'Unbekannt')}</p>
|
||||
<p><strong>E-Mail:</strong> ${escapeHtml(request.email || 'Keine E-Mail')}</p>
|
||||
<p><strong>Erstellt am:</strong> ${formatDateTime(request.created_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Auftrag Details</h4>
|
||||
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
|
||||
<p><strong>Datei:</strong> ${escapeHtml(request.file_name || 'Keine Datei')}</p>
|
||||
<p><strong>Dauer:</strong> ${request.duration_minutes || 'Unbekannt'} Minuten</p>
|
||||
<p><strong>Kopien:</strong> ${request.copies || 1}</p>
|
||||
<p><strong>Status:</strong> ${getStatusText(request.status)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${request.reason ? `
|
||||
<div class="mt-6">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Begründung</h4>
|
||||
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">${escapeHtml(request.reason)}</p>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="mt-8 flex justify-end space-x-3">
|
||||
${request.status === 'pending' ? `
|
||||
<button onclick="approveRequest(${request.id}); closeDetailModal();"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
||||
Genehmigen
|
||||
</button>
|
||||
<button onclick="rejectRequest(${request.id}); closeDetailModal();"
|
||||
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
|
||||
Ablehnen
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="closeDetailModal()"
|
||||
class="px-4 py-2 bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors">
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDetailModal() {
|
||||
const modal = document.getElementById('detail-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk Actions
|
||||
*/
|
||||
function showBulkActionsModal() {
|
||||
const selectedIds = getSelectedRequestIds();
|
||||
if (selectedIds.length === 0) {
|
||||
showNotification('⚠️ Bitte wählen Sie mindestens einen Gastauftrag aus', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('bulk-modal');
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeBulkModal() {
|
||||
const modal = document.getElementById('bulk-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
async function performBulkAction(action) {
|
||||
const selectedIds = getSelectedRequestIds();
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
const confirmMessages = {
|
||||
'approve': `Möchten Sie ${selectedIds.length} Gastaufträge genehmigen?`,
|
||||
'reject': `Möchten Sie ${selectedIds.length} Gastaufträge ablehnen?`,
|
||||
'delete': `Möchten Sie ${selectedIds.length} Gastaufträge löschen?`
|
||||
};
|
||||
|
||||
if (!confirm(confirmMessages[action])) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
closeBulkModal();
|
||||
|
||||
const promises = selectedIds.map(async (id) => {
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${id}/${action}`;
|
||||
const method = action === 'delete' ? 'DELETE' : 'POST';
|
||||
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value.ok).length;
|
||||
|
||||
showNotification(`✅ ${successCount} von ${selectedIds.length} Aktionen erfolgreich`, 'success');
|
||||
loadGuestRequests();
|
||||
|
||||
// Alle Checkboxen zurücksetzen
|
||||
document.getElementById('select-all').checked = false;
|
||||
document.querySelectorAll('.request-checkbox').forEach(cb => cb.checked = false);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler bei Bulk-Aktion:', error);
|
||||
showNotification('❌ Fehler bei der Bulk-Aktion: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedRequestIds() {
|
||||
const checkboxes = document.querySelectorAll('.request-checkbox:checked');
|
||||
return Array.from(checkboxes).map(cb => parseInt(cb.value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event Handlers
|
||||
*/
|
||||
function handleSearch() {
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
|
||||
function handleFilterChange() {
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
|
||||
function handleSortChange() {
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
|
||||
function handleSelectAll(event) {
|
||||
const checkboxes = document.querySelectorAll('.request-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = event.target.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
const selectedIds = getSelectedRequestIds();
|
||||
const exportData = selectedIds.length > 0 ?
|
||||
filteredRequests.filter(req => selectedIds.includes(req.id)) :
|
||||
filteredRequests;
|
||||
|
||||
if (exportData.length === 0) {
|
||||
showNotification('⚠️ Keine Daten zum Exportieren verfügbar', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
exportToCSV(exportData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export-Funktionen
|
||||
*/
|
||||
function exportToCSV(data) {
|
||||
const headers = ['ID', 'Name', 'E-Mail', 'Datei', 'Status', 'Erstellt', 'Dauer (Min)', 'Kopien', 'Begründung'];
|
||||
const rows = data.map(req => [
|
||||
req.id,
|
||||
req.name || '',
|
||||
req.email || '',
|
||||
req.file_name || '',
|
||||
getStatusText(req.status),
|
||||
formatDateTime(req.created_at),
|
||||
req.duration_minutes || '',
|
||||
req.copies || '',
|
||||
req.reason || ''
|
||||
]);
|
||||
|
||||
const csvContent = [headers, ...rows]
|
||||
.map(row => row.map(field => `"${String(field).replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
|
||||
if (link.download !== undefined) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `gastauftraege_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
showNotification('📄 CSV-Export erfolgreich erstellt', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Refresh
|
||||
*/
|
||||
function startAutoRefresh() {
|
||||
// Refresh alle 30 Sekunden
|
||||
refreshInterval = setInterval(() => {
|
||||
loadGuestRequests();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility-Funktionen
|
||||
*/
|
||||
function addRowEventListeners() {
|
||||
// Falls notwendig, können hier zusätzliche Event Listener hinzugefügt werden
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
const loadingElement = document.getElementById('table-loading');
|
||||
const tableBody = document.getElementById('requests-table-body');
|
||||
|
||||
if (loadingElement) {
|
||||
loadingElement.classList.toggle('hidden', !show);
|
||||
}
|
||||
|
||||
if (show && tableBody) {
|
||||
tableBody.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function showEmptyState() {
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideEmptyState() {
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 px-6 py-4 rounded-xl shadow-2xl z-50 transform transition-all duration-500 translate-x-full ${
|
||||
type === 'success' ? 'bg-green-500 text-white' :
|
||||
type === 'error' ? 'bg-red-500 text-white' :
|
||||
type === 'warning' ? 'bg-yellow-500 text-black' :
|
||||
'bg-blue-500 text-white'
|
||||
}`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="text-lg">
|
||||
${type === 'success' ? '✅' :
|
||||
type === 'error' ? '❌' :
|
||||
type === 'warning' ? '⚠️' : 'ℹ️'}
|
||||
</span>
|
||||
<span class="font-medium">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animation einblenden
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// Nach 5 Sekunden entfernen
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => notification.remove(), 5000);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text ? String(text).replace(/[&<>"']/g, m => map[m]) : '';
|
||||
}
|
||||
|
||||
function formatDateTime(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeAgo(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
const now = new Date();
|
||||
const date = new Date(dateString);
|
||||
const diffMs = now - date;
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffDays > 0) {
|
||||
return `vor ${diffDays} Tag${diffDays === 1 ? '' : 'en'}`;
|
||||
} else if (diffHours > 0) {
|
||||
return `vor ${diffHours} Stunde${diffHours === 1 ? '' : 'n'}`;
|
||||
} else {
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
||||
return `vor ${Math.max(1, diffMinutes)} Minute${diffMinutes === 1 ? '' : 'n'}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Funktionen für onclick-Handler
|
||||
window.showRequestDetail = showRequestDetail;
|
||||
window.approveRequest = approveRequest;
|
||||
window.rejectRequest = rejectRequest;
|
||||
window.deleteRequest = deleteRequest;
|
||||
window.closeDetailModal = closeDetailModal;
|
||||
window.closeBulkModal = closeBulkModal;
|
||||
window.performBulkAction = performBulkAction;
|
||||
|
||||
console.log('📋 Admin Guest Requests JavaScript vollständig geladen');
|
@ -1,585 +0,0 @@
|
||||
/**
|
||||
* Mercedes-Benz MYP Admin Live Dashboard
|
||||
* Echtzeit-Updates für das Admin Panel mit echten Daten
|
||||
*/
|
||||
|
||||
class AdminLiveDashboard {
|
||||
constructor() {
|
||||
this.isLive = false;
|
||||
this.updateInterval = null;
|
||||
this.retryCount = 0;
|
||||
this.maxRetries = 3;
|
||||
|
||||
// Dynamische API-Base-URL-Erkennung
|
||||
this.apiBaseUrl = this.detectApiBaseUrl();
|
||||
console.log('🔗 API Base URL erkannt:', this.apiBaseUrl);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
detectApiBaseUrl() {
|
||||
const currentHost = window.location.hostname;
|
||||
const currentProtocol = window.location.protocol;
|
||||
const currentPort = window.location.port;
|
||||
|
||||
console.log('🔍 Live Dashboard API URL Detection:', { currentHost, currentProtocol, currentPort });
|
||||
|
||||
// Wenn wir bereits auf dem richtigen Port sind, verwende relative URLs
|
||||
if (currentPort === '443' || !currentPort) {
|
||||
console.log('✅ Verwende relative URLs (HTTPS Port 443)');
|
||||
return '';
|
||||
}
|
||||
|
||||
// Für alle anderen Fälle, verwende HTTPS auf Port 443
|
||||
const fallbackUrl = `https://${currentHost}`;
|
||||
console.log('🔄 Fallback zu HTTPS:443:', fallbackUrl);
|
||||
|
||||
return fallbackUrl;
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log('🚀 Mercedes-Benz MYP Admin Live Dashboard gestartet');
|
||||
|
||||
// Live-Status anzeigen
|
||||
this.updateLiveTime();
|
||||
this.startLiveUpdates();
|
||||
|
||||
// Event Listeners
|
||||
this.bindEvents();
|
||||
|
||||
// Initial Load
|
||||
this.loadLiveStats();
|
||||
|
||||
// Error Monitoring System
|
||||
this.initErrorMonitoring();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Quick Action Buttons
|
||||
const systemStatusBtn = document.getElementById('system-status-btn');
|
||||
const analyticsBtn = document.getElementById('analytics-btn');
|
||||
const maintenanceBtn = document.getElementById('maintenance-btn');
|
||||
|
||||
if (systemStatusBtn) {
|
||||
systemStatusBtn.addEventListener('click', () => this.showSystemStatus());
|
||||
}
|
||||
|
||||
if (analyticsBtn) {
|
||||
analyticsBtn.addEventListener('click', () => this.showAnalytics());
|
||||
}
|
||||
|
||||
if (maintenanceBtn) {
|
||||
maintenanceBtn.addEventListener('click', () => this.showMaintenance());
|
||||
}
|
||||
|
||||
// Page Visibility API für optimierte Updates
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
this.pauseLiveUpdates();
|
||||
} else {
|
||||
this.resumeLiveUpdates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startLiveUpdates() {
|
||||
this.isLive = true;
|
||||
this.updateLiveIndicator(true);
|
||||
|
||||
// Live Stats alle 30 Sekunden aktualisieren
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.loadLiveStats();
|
||||
}, 30000);
|
||||
|
||||
// Zeit jede Sekunde aktualisieren
|
||||
setInterval(() => {
|
||||
this.updateLiveTime();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
pauseLiveUpdates() {
|
||||
this.isLive = false;
|
||||
this.updateLiveIndicator(false);
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
}
|
||||
|
||||
resumeLiveUpdates() {
|
||||
if (!this.isLive) {
|
||||
this.startLiveUpdates();
|
||||
this.loadLiveStats(); // Sofortiges Update beim Fortsetzen
|
||||
}
|
||||
}
|
||||
|
||||
updateLiveIndicator(isLive) {
|
||||
const indicator = document.getElementById('live-indicator');
|
||||
if (indicator) {
|
||||
if (isLive) {
|
||||
indicator.className = 'w-2 h-2 bg-green-400 rounded-full animate-pulse';
|
||||
} else {
|
||||
indicator.className = 'w-2 h-2 bg-gray-400 rounded-full';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateLiveTime() {
|
||||
const timeElement = document.getElementById('live-time');
|
||||
if (timeElement) {
|
||||
const now = new Date();
|
||||
timeElement.textContent = now.toLocaleTimeString('de-DE');
|
||||
}
|
||||
}
|
||||
|
||||
async loadLiveStats() {
|
||||
try {
|
||||
const url = `${this.apiBaseUrl}/api/admin/stats/live`;
|
||||
console.log('🔄 Lade Live-Statistiken von:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCSRFToken()
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.updateStatsDisplay(data);
|
||||
this.retryCount = 0; // Reset retry count on success
|
||||
|
||||
// Success notification (optional)
|
||||
this.showQuietNotification('Live-Daten aktualisiert', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || 'Unbekannter Fehler beim Laden der Live-Statistiken');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Live-Statistiken:', error);
|
||||
|
||||
this.retryCount++;
|
||||
if (this.retryCount <= this.maxRetries) {
|
||||
console.log(`Versuche erneut... (${this.retryCount}/${this.maxRetries})`);
|
||||
setTimeout(() => this.loadLiveStats(), 5000); // Retry nach 5 Sekunden
|
||||
} else {
|
||||
this.handleConnectionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStatsDisplay(data) {
|
||||
// Benutzer Stats
|
||||
this.updateCounter('live-users-count', data.total_users);
|
||||
this.updateProgress('users-progress', Math.min((data.total_users / 20) * 100, 100)); // Max 20 users = 100%
|
||||
|
||||
// Drucker Stats
|
||||
this.updateCounter('live-printers-count', data.total_printers);
|
||||
this.updateElement('live-printers-online', `${data.online_printers} online`);
|
||||
if (data.total_printers > 0) {
|
||||
this.updateProgress('printers-progress', (data.online_printers / data.total_printers) * 100);
|
||||
}
|
||||
|
||||
// Jobs Stats
|
||||
this.updateCounter('live-jobs-active', data.active_jobs);
|
||||
this.updateElement('live-jobs-queued', `${data.queued_jobs} in Warteschlange`);
|
||||
this.updateProgress('jobs-progress', Math.min(data.active_jobs * 20, 100)); // Max 5 jobs = 100%
|
||||
|
||||
// Erfolgsrate Stats
|
||||
this.updateCounter('live-success-rate', `${data.success_rate}%`);
|
||||
this.updateProgress('success-progress', data.success_rate);
|
||||
|
||||
// Trend Analysis
|
||||
this.updateSuccessTrend(data.success_rate);
|
||||
|
||||
console.log('📊 Live-Statistiken aktualisiert:', data);
|
||||
}
|
||||
|
||||
updateCounter(elementId, newValue) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
const currentValue = parseInt(element.textContent) || 0;
|
||||
if (currentValue !== newValue) {
|
||||
this.animateCounter(element, currentValue, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animateCounter(element, from, to) {
|
||||
const duration = 1000; // 1 Sekunde
|
||||
const increment = (to - from) / (duration / 16); // 60 FPS
|
||||
let current = from;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if ((increment > 0 && current >= to) || (increment < 0 && current <= to)) {
|
||||
current = to;
|
||||
clearInterval(timer);
|
||||
}
|
||||
element.textContent = Math.round(current);
|
||||
}, 16);
|
||||
}
|
||||
|
||||
updateElement(elementId, newValue) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element && element.textContent !== newValue) {
|
||||
element.textContent = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(elementId, percentage) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.style.width = `${Math.max(0, Math.min(100, percentage))}%`;
|
||||
}
|
||||
}
|
||||
|
||||
updateSuccessTrend(successRate) {
|
||||
const trendElement = document.getElementById('success-trend');
|
||||
if (trendElement) {
|
||||
let trendText = 'Stabil';
|
||||
let trendClass = 'text-green-500';
|
||||
let trendIcon = 'M5 10l7-7m0 0l7 7m-7-7v18'; // Up arrow
|
||||
|
||||
if (successRate >= 95) {
|
||||
trendText = 'Excellent';
|
||||
trendClass = 'text-green-600';
|
||||
} else if (successRate >= 80) {
|
||||
trendText = 'Gut';
|
||||
trendClass = 'text-green-500';
|
||||
} else if (successRate >= 60) {
|
||||
trendText = 'Mittel';
|
||||
trendClass = 'text-yellow-500';
|
||||
trendIcon = 'M5 12h14'; // Horizontal line
|
||||
} else {
|
||||
trendText = 'Niedrig';
|
||||
trendClass = 'text-red-500';
|
||||
trendIcon = 'M19 14l-7 7m0 0l-7-7m7 7V3'; // Down arrow
|
||||
}
|
||||
|
||||
trendElement.className = `text-sm ${trendClass}`;
|
||||
trendElement.innerHTML = `
|
||||
<span class="inline-flex items-center">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${trendIcon}"/>
|
||||
</svg>
|
||||
${trendText}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
showSystemStatus() {
|
||||
// System Status Modal oder Navigation
|
||||
console.log('🔧 System Status angezeigt');
|
||||
this.showNotification('System Status wird geladen...', 'info');
|
||||
|
||||
// Hier könnten weitere System-Details geladen werden
|
||||
const url = `${this.apiBaseUrl}/api/admin/system/status`;
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// System Status anzeigen
|
||||
console.log('System Status:', data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden des System Status:', error);
|
||||
});
|
||||
}
|
||||
|
||||
showAnalytics() {
|
||||
console.log('📈 Live Analytics angezeigt');
|
||||
this.showNotification('Analytics werden geladen...', 'info');
|
||||
|
||||
// Analytics Tab aktivieren oder Modal öffnen
|
||||
const analyticsTab = document.querySelector('a[href*="tab=system"]');
|
||||
if (analyticsTab) {
|
||||
analyticsTab.click();
|
||||
}
|
||||
}
|
||||
|
||||
showMaintenance() {
|
||||
console.log('🛠️ Wartung angezeigt');
|
||||
this.showNotification('Wartungsoptionen werden geladen...', 'info');
|
||||
|
||||
// Wartungs-Tab aktivieren oder Modal öffnen
|
||||
const systemTab = document.querySelector('a[href*="tab=system"]');
|
||||
if (systemTab) {
|
||||
systemTab.click();
|
||||
}
|
||||
}
|
||||
|
||||
handleConnectionError() {
|
||||
console.error('🔴 Verbindung zu Live-Updates verloren');
|
||||
this.updateLiveIndicator(false);
|
||||
this.showNotification('Verbindung zu Live-Updates verloren. Versuche erneut...', 'error');
|
||||
|
||||
// Auto-Recovery nach 30 Sekunden
|
||||
setTimeout(() => {
|
||||
this.retryCount = 0;
|
||||
this.loadLiveStats();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
// Erstelle oder aktualisiere Notification
|
||||
let notification = document.getElementById('live-notification');
|
||||
if (!notification) {
|
||||
notification = document.createElement('div');
|
||||
notification.id = 'live-notification';
|
||||
notification.className = 'fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm';
|
||||
document.body.appendChild(notification);
|
||||
}
|
||||
|
||||
const colors = {
|
||||
success: 'bg-green-500 text-white',
|
||||
error: 'bg-red-500 text-white',
|
||||
info: 'bg-blue-500 text-white',
|
||||
warning: 'bg-yellow-500 text-white'
|
||||
};
|
||||
|
||||
notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm ${colors[type]} transform transition-all duration-300 translate-x-0`;
|
||||
notification.textContent = message;
|
||||
|
||||
// Auto-Hide nach 3 Sekunden
|
||||
setTimeout(() => {
|
||||
if (notification) {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (notification && notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
showQuietNotification(message, type) {
|
||||
// Nur in der Konsole loggen für nicht-störende Updates
|
||||
const emoji = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️';
|
||||
console.log(`${emoji} ${message}`);
|
||||
}
|
||||
|
||||
getCSRFToken() {
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
return meta ? meta.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
// Error Monitoring System
|
||||
initErrorMonitoring() {
|
||||
// Check system health every 30 seconds
|
||||
this.checkSystemHealth();
|
||||
setInterval(() => this.checkSystemHealth(), 30000);
|
||||
|
||||
// Setup error alert event handlers
|
||||
this.setupErrorAlertHandlers();
|
||||
}
|
||||
|
||||
async checkSystemHealth() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/system-health');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.updateHealthDisplay(data);
|
||||
this.updateErrorAlerts(data);
|
||||
} else {
|
||||
console.error('System health check failed:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking system health:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateHealthDisplay(data) {
|
||||
// Update database health status
|
||||
const statusIndicator = document.getElementById('db-status-indicator');
|
||||
const statusText = document.getElementById('db-status-text');
|
||||
const lastMigration = document.getElementById('last-migration');
|
||||
const schemaIntegrity = document.getElementById('schema-integrity');
|
||||
const recentErrorsCount = document.getElementById('recent-errors-count');
|
||||
|
||||
if (statusIndicator && statusText) {
|
||||
if (data.health_status === 'critical') {
|
||||
statusIndicator.className = 'w-3 h-3 bg-red-500 rounded-full animate-pulse';
|
||||
statusText.textContent = 'Kritisch';
|
||||
statusText.className = 'text-sm font-medium text-red-600 dark:text-red-400';
|
||||
} else if (data.health_status === 'warning') {
|
||||
statusIndicator.className = 'w-3 h-3 bg-yellow-500 rounded-full animate-pulse';
|
||||
statusText.textContent = 'Warnung';
|
||||
statusText.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
|
||||
} else {
|
||||
statusIndicator.className = 'w-3 h-3 bg-green-400 rounded-full animate-pulse';
|
||||
statusText.textContent = 'Gesund';
|
||||
statusText.className = 'text-sm font-medium text-green-600 dark:text-green-400';
|
||||
}
|
||||
}
|
||||
|
||||
if (lastMigration) {
|
||||
lastMigration.textContent = data.last_migration || 'Unbekannt';
|
||||
}
|
||||
|
||||
if (schemaIntegrity) {
|
||||
schemaIntegrity.textContent = data.schema_integrity || 'Prüfung';
|
||||
if (data.schema_integrity === 'FEHLER') {
|
||||
schemaIntegrity.className = 'text-lg font-semibold text-red-600 dark:text-red-400';
|
||||
} else {
|
||||
schemaIntegrity.className = 'text-lg font-semibold text-green-600 dark:text-green-400';
|
||||
}
|
||||
}
|
||||
|
||||
if (recentErrorsCount) {
|
||||
const errorCount = data.recent_errors_count || 0;
|
||||
recentErrorsCount.textContent = errorCount;
|
||||
if (errorCount > 0) {
|
||||
recentErrorsCount.className = 'text-lg font-semibold text-red-600 dark:text-red-400';
|
||||
} else {
|
||||
recentErrorsCount.className = 'text-lg font-semibold text-green-600 dark:text-green-400';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateErrorAlerts(data) {
|
||||
const alertContainer = document.getElementById('critical-errors-alert');
|
||||
const errorList = document.getElementById('error-list');
|
||||
|
||||
if (!alertContainer || !errorList) return;
|
||||
|
||||
const allErrors = [...(data.critical_errors || []), ...(data.warnings || [])];
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
// Show alert container
|
||||
alertContainer.classList.remove('hidden');
|
||||
|
||||
// Clear previous errors
|
||||
errorList.innerHTML = '';
|
||||
|
||||
// Add each error
|
||||
allErrors.forEach(error => {
|
||||
const errorElement = document.createElement('div');
|
||||
errorElement.className = `p-3 rounded-lg border-l-4 ${
|
||||
error.severity === 'critical' ? 'bg-red-50 dark:bg-red-900/30 border-red-500' :
|
||||
error.severity === 'high' ? 'bg-orange-50 dark:bg-orange-900/30 border-orange-500' :
|
||||
'bg-yellow-50 dark:bg-yellow-900/30 border-yellow-500'
|
||||
}`;
|
||||
|
||||
errorElement.innerHTML = `
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium ${
|
||||
error.severity === 'critical' ? 'text-red-800 dark:text-red-200' :
|
||||
error.severity === 'high' ? 'text-orange-800 dark:text-orange-200' :
|
||||
'text-yellow-800 dark:text-yellow-200'
|
||||
}">${error.message}</h4>
|
||||
<p class="text-sm mt-1 ${
|
||||
error.severity === 'critical' ? 'text-red-600 dark:text-red-300' :
|
||||
error.severity === 'high' ? 'text-orange-600 dark:text-orange-300' :
|
||||
'text-yellow-600 dark:text-yellow-300'
|
||||
}">💡 ${error.suggested_fix}</p>
|
||||
<p class="text-xs mt-1 text-gray-500 dark:text-gray-400">
|
||||
📅 ${new Date(error.timestamp).toLocaleString('de-DE')}
|
||||
</p>
|
||||
</div>
|
||||
<span class="ml-2 px-2 py-1 text-xs font-medium rounded-full ${
|
||||
error.severity === 'critical' ? 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' :
|
||||
error.severity === 'high' ? 'bg-orange-100 text-orange-800 dark:bg-orange-800 dark:text-orange-100' :
|
||||
'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100'
|
||||
}">
|
||||
${error.severity.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
errorList.appendChild(errorElement);
|
||||
});
|
||||
} else {
|
||||
// Hide alert container
|
||||
alertContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
setupErrorAlertHandlers() {
|
||||
// Fix errors button
|
||||
const fixErrorsBtn = document.getElementById('fix-errors-btn');
|
||||
if (fixErrorsBtn) {
|
||||
fixErrorsBtn.addEventListener('click', async () => {
|
||||
await this.fixErrors();
|
||||
});
|
||||
}
|
||||
|
||||
// Dismiss errors button
|
||||
const dismissErrorsBtn = document.getElementById('dismiss-errors-btn');
|
||||
if (dismissErrorsBtn) {
|
||||
dismissErrorsBtn.addEventListener('click', () => {
|
||||
const alertContainer = document.getElementById('critical-errors-alert');
|
||||
if (alertContainer) {
|
||||
alertContainer.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// View details button
|
||||
const viewDetailsBtn = document.getElementById('view-error-details-btn');
|
||||
if (viewDetailsBtn) {
|
||||
viewDetailsBtn.addEventListener('click', () => {
|
||||
// Redirect to logs tab
|
||||
window.location.href = '/admin-dashboard?tab=logs';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fixErrors() {
|
||||
const fixBtn = document.getElementById('fix-errors-btn');
|
||||
if (!fixBtn) return;
|
||||
|
||||
// Show loading state
|
||||
const originalText = fixBtn.innerHTML;
|
||||
fixBtn.innerHTML = '🔄 Repariere...';
|
||||
fixBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/fix-errors', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Show success message
|
||||
this.showNotification('✅ Automatische Reparatur erfolgreich durchgeführt!', 'success');
|
||||
|
||||
// Refresh health check
|
||||
setTimeout(() => {
|
||||
this.checkSystemHealth();
|
||||
}, 2000);
|
||||
} else {
|
||||
// Show error message
|
||||
this.showNotification(`❌ Reparatur fehlgeschlagen: ${data.error}`, 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing errors:', error);
|
||||
this.showNotification('❌ Fehler bei der automatischen Reparatur', 'error');
|
||||
} finally {
|
||||
// Restore button
|
||||
fixBtn.innerHTML = originalText;
|
||||
fixBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
new AdminLiveDashboard();
|
||||
});
|
||||
|
||||
// Export for global access
|
||||
window.AdminLiveDashboard = AdminLiveDashboard;
|
@ -1,350 +0,0 @@
|
||||
/**
|
||||
* Admin System Management JavaScript
|
||||
* Funktionen für System-Wartung und -Konfiguration
|
||||
*/
|
||||
|
||||
// CSRF Token für AJAX-Anfragen
|
||||
function getCsrfToken() {
|
||||
const token = document.querySelector('meta[name="csrf-token"]');
|
||||
return token ? token.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
// Hilfsfunktion für API-Aufrufe
|
||||
async function makeApiCall(url, method = 'GET', data = null) {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
};
|
||||
|
||||
if (data) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showNotification(result.message || 'Aktion erfolgreich ausgeführt', 'success');
|
||||
return result;
|
||||
} else {
|
||||
showNotification(result.error || 'Ein Fehler ist aufgetreten', 'error');
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Netzwerkfehler: ' + error.message, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Logs laden und anzeigen
|
||||
async function loadLogs() {
|
||||
const logsContainer = document.getElementById('logs-container');
|
||||
if (!logsContainer) return;
|
||||
|
||||
// Lade-Animation anzeigen
|
||||
logsContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 dark:border-indigo-400"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/logs');
|
||||
if (!response.ok) throw new Error('Fehler beim Laden der Logs');
|
||||
|
||||
const data = await response.json();
|
||||
window.logsData = data.logs || [];
|
||||
window.filteredLogs = [...window.logsData];
|
||||
renderLogs();
|
||||
updateLogStatistics();
|
||||
scrollLogsToBottom();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Logs:', error);
|
||||
logsContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<div class="text-red-600 dark:text-red-400 text-xl mb-2">Fehler beim Laden der Logs</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">${error.message}</p>
|
||||
<button onclick="loadLogs()" class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Logs rendern
|
||||
function renderLogs() {
|
||||
const logsContainer = document.getElementById('logs-container');
|
||||
if (!logsContainer || !window.filteredLogs) return;
|
||||
|
||||
if (window.filteredLogs.length === 0) {
|
||||
logsContainer.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<p class="text-gray-600 dark:text-gray-400">Keine Logs gefunden</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const logsHtml = window.filteredLogs.map(log => {
|
||||
const levelColor = getLogLevelColor(log.level);
|
||||
return `
|
||||
<div class="bg-white/40 dark:bg-slate-700/40 rounded-lg p-4 border ${levelColor.border}">
|
||||
<div class="flex items-start space-x-3">
|
||||
<span class="inline-block px-2 py-1 text-xs font-semibold rounded-full ${levelColor.bg} ${levelColor.text}">
|
||||
${log.level}
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-sm font-medium text-slate-600 dark:text-slate-400">${log.category}</span>
|
||||
<span class="text-xs text-slate-500 dark:text-slate-500">${log.timestamp}</span>
|
||||
</div>
|
||||
<p class="text-sm text-slate-900 dark:text-white break-all">${log.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
logsContainer.innerHTML = logsHtml;
|
||||
}
|
||||
|
||||
// Log-Level-Farben bestimmen
|
||||
function getLogLevelColor(level) {
|
||||
const colors = {
|
||||
'ERROR': {
|
||||
bg: 'bg-red-100 dark:bg-red-900/30',
|
||||
text: 'text-red-800 dark:text-red-200',
|
||||
border: 'border-red-200 dark:border-red-700'
|
||||
},
|
||||
'WARNING': {
|
||||
bg: 'bg-yellow-100 dark:bg-yellow-900/30',
|
||||
text: 'text-yellow-800 dark:text-yellow-200',
|
||||
border: 'border-yellow-200 dark:border-yellow-700'
|
||||
},
|
||||
'INFO': {
|
||||
bg: 'bg-blue-100 dark:bg-blue-900/30',
|
||||
text: 'text-blue-800 dark:text-blue-200',
|
||||
border: 'border-blue-200 dark:border-blue-700'
|
||||
},
|
||||
'DEBUG': {
|
||||
bg: 'bg-gray-100 dark:bg-gray-900/30',
|
||||
text: 'text-gray-800 dark:text-gray-200',
|
||||
border: 'border-gray-200 dark:border-gray-700'
|
||||
}
|
||||
};
|
||||
|
||||
return colors[level.toUpperCase()] || colors['INFO'];
|
||||
}
|
||||
|
||||
// Log-Statistiken aktualisieren
|
||||
function updateLogStatistics() {
|
||||
if (!window.logsData) return;
|
||||
|
||||
const stats = {
|
||||
total: window.logsData.length,
|
||||
errors: window.logsData.filter(log => log.level.toUpperCase() === 'ERROR').length,
|
||||
warnings: window.logsData.filter(log => log.level.toUpperCase() === 'WARNING').length,
|
||||
info: window.logsData.filter(log => log.level.toUpperCase() === 'INFO').length
|
||||
};
|
||||
|
||||
// Aktualisiere Statistik-Anzeigen falls vorhanden
|
||||
const totalElement = document.getElementById('log-stats-total');
|
||||
const errorsElement = document.getElementById('log-stats-errors');
|
||||
const warningsElement = document.getElementById('log-stats-warnings');
|
||||
const infoElement = document.getElementById('log-stats-info');
|
||||
|
||||
if (totalElement) totalElement.textContent = stats.total;
|
||||
if (errorsElement) errorsElement.textContent = stats.errors;
|
||||
if (warningsElement) warningsElement.textContent = stats.warnings;
|
||||
if (infoElement) infoElement.textContent = stats.info;
|
||||
}
|
||||
|
||||
// Zum Ende der Logs scrollen
|
||||
function scrollLogsToBottom() {
|
||||
const logsContainer = document.getElementById('logs-container');
|
||||
if (logsContainer) {
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Notification anzeigen
|
||||
function showNotification(message, type = 'info') {
|
||||
// Erstelle Notification-Element falls nicht vorhanden
|
||||
let notification = document.getElementById('admin-notification');
|
||||
if (!notification) {
|
||||
notification = document.createElement('div');
|
||||
notification.id = 'admin-notification';
|
||||
notification.className = 'fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm transition-all duration-300 transform translate-x-full';
|
||||
document.body.appendChild(notification);
|
||||
}
|
||||
|
||||
// Setze Farbe basierend auf Typ
|
||||
const colors = {
|
||||
success: 'bg-green-500 text-white',
|
||||
error: 'bg-red-500 text-white',
|
||||
warning: 'bg-yellow-500 text-white',
|
||||
info: 'bg-blue-500 text-white'
|
||||
};
|
||||
|
||||
notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm transition-all duration-300 ${colors[type] || colors.info}`;
|
||||
notification.textContent = message;
|
||||
|
||||
// Zeige Notification
|
||||
notification.style.transform = 'translateX(0)';
|
||||
|
||||
// Verstecke nach 5 Sekunden
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Cache leeren
|
||||
async function clearCache() {
|
||||
if (confirm('Möchten Sie wirklich den Cache leeren?')) {
|
||||
showNotification('Cache wird geleert...', 'info');
|
||||
const result = await makeApiCall('/api/admin/cache/clear', 'POST');
|
||||
if (result) {
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Datenbank optimieren
|
||||
async function optimizeDatabase() {
|
||||
if (confirm('Möchten Sie wirklich die Datenbank optimieren? Dies kann einige Minuten dauern.')) {
|
||||
showNotification('Datenbank wird optimiert...', 'info');
|
||||
const result = await makeApiCall('/api/admin/database/optimize', 'POST');
|
||||
if (result) {
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backup erstellen
|
||||
async function createBackup() {
|
||||
if (confirm('Möchten Sie wirklich ein Backup erstellen?')) {
|
||||
showNotification('Backup wird erstellt...', 'info');
|
||||
const result = await makeApiCall('/api/admin/backup/create', 'POST');
|
||||
}
|
||||
}
|
||||
|
||||
// Drucker aktualisieren
|
||||
async function updatePrinters() {
|
||||
if (confirm('Möchten Sie alle Drucker-Verbindungen aktualisieren?')) {
|
||||
showNotification('Drucker werden aktualisiert...', 'info');
|
||||
const result = await makeApiCall('/api/admin/printers/update', 'POST');
|
||||
if (result) {
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// System neustarten
|
||||
async function restartSystem() {
|
||||
if (confirm('WARNUNG: Möchten Sie wirklich das System neustarten? Alle aktiven Verbindungen werden getrennt.')) {
|
||||
const result = await makeApiCall('/api/admin/system/restart', 'POST');
|
||||
if (result) {
|
||||
showNotification('System wird neugestartet...', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Einstellungen bearbeiten
|
||||
function editSettings() {
|
||||
window.location.href = '/settings';
|
||||
}
|
||||
|
||||
// Systemstatus automatisch aktualisieren
|
||||
async function updateSystemStatus() {
|
||||
if (window.location.search.includes('tab=system')) {
|
||||
const result = await makeApiCall('/api/admin/system/status');
|
||||
if (result) {
|
||||
// Aktualisiere die Anzeige
|
||||
updateStatusDisplay('cpu_usage', result.cpu_usage + '%');
|
||||
updateStatusDisplay('memory_usage', result.memory_usage + '%');
|
||||
updateStatusDisplay('disk_usage', result.disk_usage + '%');
|
||||
updateStatusDisplay('uptime', result.uptime);
|
||||
updateStatusDisplay('db_size', result.db_size);
|
||||
updateStatusDisplay('scheduler_jobs', result.scheduler_jobs);
|
||||
updateStatusDisplay('next_job', result.next_job);
|
||||
|
||||
// Scheduler-Status aktualisieren
|
||||
const schedulerStatus = document.querySelector('.scheduler-status');
|
||||
if (schedulerStatus) {
|
||||
if (result.scheduler_running) {
|
||||
schedulerStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-blue-400 animate-pulse"></span>Läuft';
|
||||
schedulerStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
||||
} else {
|
||||
schedulerStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-red-400"></span>Gestoppt';
|
||||
schedulerStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktion zum Aktualisieren der Status-Anzeige
|
||||
function updateStatusDisplay(key, value) {
|
||||
const element = document.querySelector(`[data-status="${key}"]`);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Datenbankstatus aktualisieren
|
||||
async function updateDatabaseStatus() {
|
||||
if (window.location.search.includes('tab=system')) {
|
||||
const result = await makeApiCall('/api/admin/database/status');
|
||||
if (result) {
|
||||
const dbStatus = document.querySelector('.database-status');
|
||||
if (dbStatus) {
|
||||
if (result.connected) {
|
||||
dbStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-green-400 animate-pulse"></span>Verbunden';
|
||||
dbStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||
} else {
|
||||
dbStatus.innerHTML = '<span class="w-2 h-2 mr-1 rounded-full bg-red-400"></span>Getrennt';
|
||||
dbStatus.className = 'inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusDisplay('db_size', result.size);
|
||||
updateStatusDisplay('db_connections', result.connected ? 'Aktiv' : 'Getrennt');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-Update alle 30 Sekunden
|
||||
setInterval(() => {
|
||||
updateSystemStatus();
|
||||
updateDatabaseStatus();
|
||||
}, 30000);
|
||||
|
||||
// Initial load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateSystemStatus();
|
||||
updateDatabaseStatus();
|
||||
});
|
||||
|
||||
// Export für globale Verwendung
|
||||
window.adminSystem = {
|
||||
clearCache,
|
||||
optimizeDatabase,
|
||||
createBackup,
|
||||
updatePrinters,
|
||||
restartSystem,
|
||||
editSettings,
|
||||
updateSystemStatus,
|
||||
updateDatabaseStatus,
|
||||
loadLogs,
|
||||
renderLogs,
|
||||
updateLogStatistics,
|
||||
scrollLogsToBottom
|
||||
};
|