diff --git a/README.md b/README.md index a644c099..35920a5c 100644 --- a/README.md +++ b/README.md @@ -1,376 +1,395 @@ -# MYP Reservation Platform +# MYP Druckerverwaltungssystem -Mercedes-Benz Werk 040 Berlin - 3D-Drucker Reservierungsplattform +**Manage Your Printer** - Mercedes-Benz Werk 040 Berlin +Vollständige 3D-Drucker Verwaltungsplattform mit Smart-Plug-Technologie + +## 🎯 System-Übersicht + +**MYP (Manage Your Printer)** ist ein System zur **zentralen Verwaltung und Steuerung von 3D-Druckern mittels Smart-Plug-Technologie**. Es digitalisiert den Reservierungsprozess für mehrere 3D-Drucker und ermögligt eine **automatisierte Schaltung der Drucker über WLAN-Steckdosen (TP-Link Tapo P110)**. + +### 🔑 Kernfunktionen + +#### **Benutzer- und Rechteverwaltung** +- **Registrierung, Login und Rollenkonzept** (Admin/Benutzer) +- **Administrierende** können Drucker und Nutzer verwalten +- **Standard-Benutzer** können Reservierungen anlegen und Druckjobs verwalten + +#### **Drucker- und Auftragsmanagement** +- **Zentrales Reservierungssystem** für Zeitfenster-Buchungen +- **Automatische Drucker-Schaltung**: Einschalten zum Reservierungsstart, Ausschalten nach Ende +- **Herstellerunabhängig**: Keine direkte Kommunikation mit 3D-Druckern - ausschließlich Stromsteuerung über Smart-Plug-Steckdosen +- **Einfache Integration**: Keine Eingriffe in die Druckerhardware erforderlich + +#### **Statistikerfassung** +- **Protokollierung** von Nutzungszeiten und abgeschlossenen Druckaufträgen +- **Auswertungen** (z.B. Gesamtdruckzeit pro Zeitraum) +- **Analytics-Dashboard** für Effizienzanalysen + +#### **Offline-Fähigkeit & Kiosk-Modus** +- **Autonomer Betrieb** ohne Internetzugang nach Installation +- **Raspberry Pi Kiosk-Modus**: Vollbild-Dashboard vor Ort +- **Touch-Interface** für aktuelle Druckerbelegungen und Systemstatus + +## 📋 Projektarchitektur + +Dieses Repository enthält **zwei sich ergänzende Projektarbeiten** für die IHK-Abschlussprüfung: + +### 🏗️ **Backend-System** (Till Tomczak) - **KERN-INFRASTRUKTUR** +- **Entwickler**: Till Tomczak +- **Fachrichtung**: Fachinformatiker für digitale Vernetzung +- **Technologie**: **Flask-basiertes Backend in Python** mit **SQLite-Datenbank** +- **Verantwortung**: Hardware-Integration, REST-APIs und cyber-physische Vernetzung + +### 📊 **Frontend-System** (Torben Haack) - **BENUTZEROBERFLÄCHE & ANALYTICS** +- **Entwickler**: Torben Haack +- **Fachrichtung**: Fachinformatiker für Daten- und Prozessanalyse +- **Technologie**: **Next.js-basierte Webanwendung** mit erweiterten Analytics +- **Verantwortung**: Moderne Web-UI, Datenvisualisierung und Benutzerfreundlichkeit + +## 🏗️ Technische Architektur + +### Cyber-Physische Lösung +``` +┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ +│ Frontend-Server │◄──►│ Backend-Server │◄──►│ Raspberry Pi │ +│ (Port 3000) │ │ (Port 443/5000) │ │ (Smart-Plugs) │ +│ Torben Haack │ │ Till Tomczak │ │ Till Tomczak │ +│ │ │ │ │ │ +│ • Next.js App │ │ • Flask REST-API │ │ • TP-Link Tapo P110 │ +│ • Analytics UI │ │ • SQLite Database │ │ • Hardware Control │ +│ • PWA-Features │ │ • Smart-Plug API │ │ • Kiosk Interface │ +│ • HTTPS Client │ │ • HTTPS Server │ │ • Offline Operation │ +│ • Export Functions │ │ • Session Management│ │ • Touch Interface │ +└─────────────────────┘ └─────────────────────┘ └─────────────────────┘ +``` + +### Kommunikations-Architektur +- **RESTful API**: Backend kommuniziert mit Frontend und externen Diensten +- **HTTPS-Verschlüsselung**: Selbstsignierte Zertifikate für sichere Übertragung +- **Progressive Web App (PWA)**: Offline-Funktionalität im Browser +- **Smart-Plug-Integration**: Lokale WLAN-Steuerung ohne Cloud-Abhängigkeit ## 🚀 Schnellstart -### MYP Control Center (Empfohlen) - -Das zentrale Installationssystem für alle Komponenten: - +### Backend-System (Hardware & APIs) ```bash -# Repository klonen -git clone -cd Projektarbeit-MYP +# Backend-Server starten (Till Tomczaks System) +cd backend +sudo ./setup.sh # Automatische Installation +python app.py # Oder für Development -# MYP Control Center starten -./myp_installer.sh +# Kiosk-Modus auf Raspberry Pi +sudo systemctl start myp-https.service ``` -Das MYP Control Center bietet: -- **Schnellstart-Installationen** (Vollständig, Backend-Only, Entwicklung) -- **Produktions-Installer** (Integration der v3.2 install.sh Funktionalität) -- **Granulare Installation** (Einzelne Komponenten) -- **System & Wartung** (Tests, Status, Informationen) - -### Direkter Produktions-Installer - -Für schnelle Produktions-Deployments direkt im MYP Control Center → Option 4: - -```bash -./myp_installer.sh -# → Wähle Option 4: Produktions-Installer -# → Backend installieren (Raspberry Pi) -# → Frontend installieren (Docker) -``` - -### Voraussetzungen - -- **Backend (Raspberry Pi)**: Python 3.11, systemd -- **Frontend (m040tbaraspi001)**: Docker, Docker Compose - -## 🌐 Zugriff - -- **Frontend**: https://m040tbaraspi001.de040.corpintra.net -- **Backend API**: https://raspberrypi/api - -## 🔧 Konfiguration - -### Netzwerk - -| Komponente | Hostname | IP | Port | -|------------|----------|----|----- | -| Frontend | m040tbaraspi001.de040.corpintra.net | 192.168.0.109 | 443 | -| Backend | raspberrypi | 192.168.0.105 | 443 | - -### TLS-Zertifikate - -Selbstsignierte Zertifikate werden automatisch generiert: -- Backend: `backend/app/certs/` -- Frontend: `frontend/certs/` - -## 📊 Health Checks - -```bash -# Backend -curl -k https://raspberrypi/api/test - -# Frontend -curl -k https://m040tbaraspi001.de040.corpintra.net/health -``` - -## 🛠️ Entwicklung - -### MYP Control Center (Entwicklungs-Setup) -```bash -./myp_installer.sh -# → Option 3: Entwicklungs-Setup -``` - -### MYP Kiosk-Modus (Produktions-bereit) - -Das Backend bietet parallel zur API auch ein **vollständiges Web-Interface** für Kiosk-Betrieb: - -```bash -./myp_installer.sh -# → Option 2: Backend-Only Installation (mit Kiosk Web Interface) -# oder -# → Option 5: Granulare Installation → Kiosk-Modus installieren -``` - -**Kiosk-Features:** -- ✅ **Web-Interface parallel zur API** - Kein separates Frontend nötig -- ✅ **Touch-optimierte Bedienung** - Ideal für Raspberry Pi Touchscreens -- ✅ **Automatischer Browser-Start** - Vollbild-Modus beim Boot -- ✅ **systemd Integration** - Service-basierte Kiosk-Verwaltung -- ✅ **Produktions-bereit** - SSL, Logging, Monitoring - -**Kiosk URLs:** -- Hauptinterface: `https://raspberrypi/` -- Dashboard: `https://raspberrypi/dashboard` -- Drucker-Verwaltung: `https://raspberrypi/printers` -- Job-Verwaltung: `https://raspberrypi/jobs` -- Admin-Panel: `https://raspberrypi/admin` - -### Manuelle Installation - -#### Backend Debug-Modus -```bash -cd backend/app -python3.11 app.py --debug -``` - -#### Frontend Development +### Frontend-System (Web-Interface) ```bash +# Frontend-Server starten (Torben Haacks System) cd frontend -npm run dev +pnpm install +pnpm db # Datenbank einrichten +pnpm dev # Development-Server + +# Produktions-Deployment +pnpm build && pnpm start ``` -#### Kiosk-Modus manuell starten +### Vollständiges System ```bash -# Backend starten -cd backend/app && python app.py +# Backend (API-Server) +cd backend && python app.py --host 0.0.0.0 --port 5000 & -# Kiosk-Browser starten (separates Terminal) -./backend/app/start_kiosk.sh +# Frontend (Web-Interface) +cd frontend && pnpm build && pnpm start & ``` -## 📁 Projektstruktur +## 🌐 Systemzugriff + +### Produktions-URLs +- **Web-Interface**: `http://localhost:3000` (Torben Haacks Frontend) +- **API-Backend**: `https://192.168.0.105:443/api` (Till Tomczaks APIs auf separatem Server) +- **Kiosk-Modus**: `https://192.168.0.105:443` (Lokales Touch-Interface) + +### Standard-Anmeldedaten +- **Benutzername**: `admin` +- **Passwort**: `admin123` + +### Netzwerk-Konfiguration +- **Backend-Server**: `192.168.0.105:443` (HTTPS) +- **Frontend-Server**: `localhost:3000` (HTTP Development) +- **SSL-Zertifikate**: Selbstsigniert (automatisch akzeptiert) + +## 📁 Projektstruktur & Integration ``` Projektarbeit-MYP/ -├── myp_installer.sh # 🎯 HAUPT-INSTALLER (Control Center) -├── backend/ -│ ├── app/ -│ │ ├── certs/ # TLS-Zertifikate -│ │ ├── database/ # SQLite-Datenbank -│ │ ├── logs/ # Anwendungslogs -│ │ └── app.py # Hauptanwendung -│ ├── myp.service # systemd Service -│ ├── requirements.txt # Python-Abhängigkeiten -│ └── legacy_setup_raspberry_pi.sh # Legacy Skript -├── frontend/ -│ ├── certs/ # TLS-Zertifikate -│ ├── docker/ -│ │ ├── caddy/ -│ │ │ └── Caddyfile # Reverse Proxy Konfiguration -│ │ └── legacy_deploy.sh # Legacy Skript -│ ├── src/ # Next.js Anwendung -│ └── docker-compose.yml -├── scripts/ -│ └── legacy_generate_certs.sh # Legacy Skript -├── archiv/ -│ └── myp_installer_legacy.sh # Archivierte Version -└── docs/ # Dokumentation +├── backend/ # 🏗️ KERN-INFRASTRUKTUR (Till Tomczak) +│ ├── app.py # Flask REST-API Server +│ ├── models.py # SQLite-Datenbank & Business Logic +│ ├── utils/ # Smart-Plug Integration (TP-Link Tapo P110) +│ ├── templates/ # Kiosk-Mode Web-Interface +│ ├── static/ # PWA-Assets für Offline-Betrieb +│ └── systemd/ # Raspberry Pi Service-Integration +├── +├── frontend/ # 📊 WEB-INTERFACE (Torben Haack) +│ ├── src/app/ # Next.js Haupt-Anwendung +│ ├── src/components/ # React UI-Komponenten +│ ├── src/lib/api/ # Backend-REST-API-Integration +│ └── src/lib/analytics/ # Statistik-Algorithmen +├── +├── docs/ # 📚 Gemeinsame Dokumentation +└── README.md # Diese Datei ``` -### Script-Status +## 🎯 Funktions-Aufgabenteilung -| Skript | Status | Verwendung | -|--------|--------|------------| -| `myp_installer.sh` | ✅ **AKTIV** | Haupt-Control-Center | -| `*legacy_*.sh` | 📦 Legacy | Historische Versionen | -| `archiv/myp_installer_legacy.sh` | 📦 Archiv | Alte Version 4.0 | +### Backend-Verantwortlichkeiten (Till Tomczak) +- ✅ **Smart-Plug-Steuerung**: TP-Link Tapo P110 WLAN-Steckdosen +- ✅ **Automatische Drucker-Schaltung**: Zeitgesteuerte Ein-/Ausschaltung +- ✅ **REST-API-Bereitstellung**: Vollständige API für alle Drucker-Operationen +- ✅ **Cyber-physische Vernetzung**: IT-System ↔ Hardware-Integration +- ✅ **SQLite-Datenbank**: Benutzer, Drucker, Jobs, Statistiken +- ✅ **HTTPS-Server**: Selbstsignierte Zertifikate und Session-Management +- ✅ **Raspberry Pi Integration**: Systemd-Services und Kiosk-Modus +- ✅ **Offline-Fähigkeit**: Autonomer Betrieb ohne Internet -## 🔒 Sicherheit +### Frontend-Verantwortlichkeiten (Torben Haack) +- ✅ **Moderne Web-UI**: React-basierte Benutzeroberfläche +- ✅ **Progressive Web App**: Offline-Funktionalität im Browser +- ✅ **Advanced Analytics**: Interaktive Charts und Datenvisualisierung +- ✅ **Reporting-System**: PDF/Excel-Export und automatisierte Berichte +- ✅ **Responsive Design**: Optimiert für Desktop, Tablet und Mobile +- ✅ **Backend-API-Integration**: Nahtlose REST-API-Anbindung +- ✅ **Statistik-Auswertungen**: Nutzungsanalysen und Trend-Analysen +- ✅ **Benutzerfreundlichkeit**: Intuitive Workflows für alle Stakeholder -- HTTPS-only (Port 443) -- Selbstsignierte TLS-Zertifikate -- HTTP → HTTPS Redirect -- Security Headers (HSTS, CSP, etc.) +## 🔗 API-Integration & Kommunikation -## 📝 Logs +### Backend-REST-Endpunkte (Till Tomczak) +```typescript +// Drucker-Management +GET /api/printers // Alle Drucker abrufen +POST /api/printers // Neuen Drucker hinzufügen +PUT /api/printers/{id} // Drucker aktualisieren +DELETE /api/printers/{id} // Drucker löschen -### Backend +// Reservierungs-Management +GET /api/jobs // Alle Reservierungen abrufen +POST /api/jobs // Neue Reservierung erstellen +PUT /api/jobs/{id}/finish // Reservierung beenden +DELETE /api/jobs/{id} // Reservierung abbrechen + +// Smart-Plug-Steuerung (TP-Link Tapo P110) +POST /api/plugs/{id}/on // Drucker einschalten +POST /api/plugs/{id}/off // Drucker ausschalten +GET /api/plugs/{id}/status // Plug-Status abfragen + +// Statistiken & Analytics +GET /api/stats // Nutzungsstatistiken +GET /api/reports // Report-Daten für Analytics +``` + +### Frontend-Integration (Torben Haack) +```typescript +// Backend-API Client - Konfiguriert für separaten Server +export class MYPApiClient { + constructor(baseURL: string = 'https://192.168.0.105:443/api') { + this.baseURL = baseURL; + } + + async getPrinters() { + return fetch(`${this.baseURL}/printers`).then(r => r.json()); + } + + async getJobs() { + return fetch(`${this.baseURL}/jobs`).then(r => r.json()); + } + + async getStats() { + return fetch(`${this.baseURL}/stats`).then(r => r.json()); + } +} + +// API-Konfiguration mit Fallback-URLs +export const API_BASE_URL = { + primary: 'https://192.168.0.105:443', + fallbacks: [ + 'https://192.168.0.105', + 'https://raspberrypi' + ] +}; +``` + +## 🖥️ Deployment-Szenarien + +### Szenario 1: Separate Server (Empfohlen) ```bash -# systemd Journal -sudo journalctl -u myp.service -f +# Backend-Server (z.B. Raspberry Pi oder Linux-Server) +cd backend +sudo systemctl start myp-https.service -# Anwendungslogs -tail -f backend/app/logs/app/app.log +# Frontend-Server (z.B. Node.js-Server oder Cloud-Deployment) +cd frontend +npm run build && npm start ``` -### Frontend +### Szenario 2: Docker-Deployment +```yaml +# docker-compose.yml +services: + backend: + build: ./backend + ports: ["5000:5000", "443:443"] + + frontend: + build: ./frontend + ports: ["3000:3000"] + environment: + - NEXT_PUBLIC_API_URL=http://backend:5000/api +``` + +### Szenario 3: Raspberry Pi Kiosk (Lokal) ```bash -# Docker Logs -docker-compose logs -f - -# Caddy Logs -docker-compose logs caddy +# Vollständige Kiosk-Installation +cd backend && sudo ./setup.sh +# Automatischer Start: Touch-Interface + Smart-Plug-Steuerung ``` -### Kiosk-Modus +## 🔧 Konfiguration & Environment + +### Backend-Konfiguration (.env) +```env +# Flask-Server Einstellungen +FLASK_HOST=0.0.0.0 +FLASK_PORT=5000 +SSL_ENABLED=true +DATABASE_URL=sqlite:///myp.db + +# Smart-Plug Konfiguration (TP-Link Tapo P110) +TAPO_USERNAME=your-tapo-email +TAPO_PASSWORD=your-tapo-password + +# Kiosk-Modus +KIOSK_MODE=true +OFFLINE_MODE=true +``` + +### Frontend-Konfiguration (.env.local) +```env +# Frontend-Server Einstellungen - Separater Backend-Server +NEXT_PUBLIC_API_URL=https://192.168.0.105:443 +DATABASE_URL=file:./db/frontend.db + +# SSL-Zertifikat Handling für selbstsignierte Zertifikate +NODE_TLS_REJECT_UNAUTHORIZED=0 + +# Analytics-Features +ENABLE_ADVANCED_ANALYTICS=true +CHART_REFRESH_INTERVAL=30000 +``` + +## 📊 Features im Überblick + +### Backend-Features (Till Tomczak) - Cyber-Physische Integration +- **TP-Link Tapo P110 Integration**: Lokale WLAN-Steckdosen-Steuerung +- **Automatische Zeitsteuerung**: Drucker Ein-/Ausschaltung nach Reservierung +- **Herstellerunabhängigkeit**: Keine direkten Drucker-Eingriffe erforderlich +- **Flask REST-APIs**: Vollständige CRUD-Operationen +- **SQLite-Datenbank**: Lokale Datenpersistenz ohne externe Abhängigkeiten +- **HTTPS-Verschlüsselung**: Selbstsignierte Zertifikate +- **Offline-Betrieb**: Vollständig autonomer Betrieb ohne Internet +- **Raspberry Pi Kiosk**: Touch-optimiertes Dashboard vor Ort + +### Frontend-Features (Torben Haack) - Moderne Web-Oberfläche +- **Progressive Web App**: Offline-Funktionalität im Browser +- **React 18 + Next.js 14**: Moderne, performante Web-Technologien +- **Analytics-Dashboard**: Recharts-Visualisierungen für Nutzungsstatistiken +- **Responsive Design**: Optimiert für alle Endgeräte (Desktop/Tablet/Mobile) +- **Real-time Updates**: Live-Synchronisation mit Backend-APIs +- **Export-Funktionen**: PDF/Excel-Reports für Management-Analysen +- **Benutzerfreundlich**: Intuitive Workflows für alle Stakeholder + +## 🛠️ Entwicklung + +### Backend-Entwicklung (Till Tomczak) ```bash -# Backend Service -sudo systemctl status myp.service - -# Kiosk-Browser Service -sudo systemctl status myp-kiosk-browser.service - -# Kiosk-Browser Logs -sudo journalctl -u myp-kiosk-browser.service -f +cd backend +python -m venv venv +source venv/bin/activate # Linux/Mac +pip install -r requirements.txt +python app.py --debug ``` -## 🔧 Services - -### Backend Services +### Frontend-Entwicklung (Torben Haack) ```bash -# Backend starten/stoppen -sudo systemctl start myp.service -sudo systemctl stop myp.service -sudo systemctl restart myp.service - -# Kiosk-Browser starten/stoppen (falls installiert) -sudo systemctl start myp-kiosk-browser.service -sudo systemctl stop myp-kiosk-browser.service - -# Automatischen Start aktivieren/deaktivieren -sudo systemctl enable myp.service -sudo systemctl enable myp-kiosk-browser.service +cd frontend +pnpm install +pnpm db:migrate +pnpm dev ``` -## 🆘 Troubleshooting - -### MYP Control Center verwenden +### Integration testen ```bash -./myp_installer.sh -# → Option 6: Systemvoraussetzungen prüfen -# → Option 7: Anwendung starten +# Backend-APIs testen +curl http://localhost:5000/api/printers + +# Frontend mit Backend-Integration +# Frontend auf :3000 konsumiert Backend-APIs von :5000 ``` -### Backend startet nicht -```bash -# Service Status prüfen -sudo systemctl status myp.service +## 📚 Dokumentation -# Logs prüfen -sudo journalctl -u myp.service --no-pager +### Backend-Dokumentation (Till Tomczak) +- [`backend/README.md`](backend/README.md) - Hardware-Setup & API-Dokumentation +- [`backend/docs/`](backend/docs/) - Raspberry Pi Konfiguration & Smart-Plug-Integration -# Zertifikate prüfen -ls -la backend/app/certs/ -``` +### Frontend-Dokumentation (Torben Haack) +- [`frontend/README.md`](frontend/README.md) - UI-Entwicklung & Analytics +- [`frontend/docs/`](frontend/docs/) - Component-Library & PWA-Features -### Frontend nicht erreichbar -```bash -# Container Status prüfen -docker-compose ps +### Gemeinsame Dokumentation +- [`docs/myp_documentation.md`](docs/myp_documentation.md) - Vollständige Projektdokumentation +- [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) - Production-Deployment-Guide -# Netzwerk prüfen -docker network ls +## 🤝 Projektphilosophie -# Zertifikate prüfen -ls -la frontend/certs/ -``` +### Cyber-Physische Vernetzung +MYP stellt eine **cyber-physische Lösung** dar, die **IT-System (Reservierungsplattform) und Hardware (Smart-Plugs und Drucker) eng vernetzt**. Das System überbrückt die digitale und physische Welt durch intelligente Automatisierung. -### Verbindungsprobleme -```bash -# DNS auflösen -nslookup raspberrypi -nslookup m040tbaraspi001.de040.corpintra.net +### Komplementäre Expertisen +- **Till Tomczak**: Spezialist für Hardware-Integration und cyber-physische Vernetzung +- **Torben Haack**: Spezialist für Frontend-Entwicklung und Datenanalyse -# Ports prüfen -netstat -tlnp | grep :443 -``` +### Gemeinsame Ziele +- **Digitalisierung**: Modernisierung des Reservierungsprozesses +- **Automatisierung**: Zeitgesteuerte Hardware-Steuerung ohne manuelle Eingriffe +- **Benutzerfreundlichkeit**: Intuitive Bedienung für alle Stakeholder +- **Effizienz**: Optimierte Ressourcennutzung und Energieeinsparung -## 📋 Version +## 👥 Entwicklerteam -- **Version**: 3.2-final -- **Control Center**: v4.0 mit v3.2 Integration -- **Build**: Production -- **Installer**: MYP Control Center +### Till Tomczak - **Backend-Infrastruktur & Hardware-Integration** +- **Cyber-Physische Systeme**: Smart-Plug-Integration und Hardware-Steuerung +- **System-Architektur**: Flask-APIs und SQLite-Datenbank-Design +- **DevOps**: Raspberry Pi Services und Produktions-Deployment +- **Offline-Systeme**: Autonomer Betrieb ohne Internet-Abhängigkeiten -## 👥 Support +### Torben Haack - **Frontend-Entwicklung & Analytics** +- **Progressive Web Apps**: Moderne Browser-Technologien und Offline-Features +- **User Interface**: React-Komponenten und responsive Design +- **Datenvisualisierung**: Charts, Dashboards und Analytics +- **API-Integration**: Nahtlose Backend-Anbindung und Real-time Updates -Bei Problemen verwenden Sie das MYP Control Center oder wenden Sie sich an das IT-Team des Mercedes-Benz Werk 040 Berlin. +## 📄 Lizenz -## 🖥️ Zwei-Server-Setup (Produktions-Architektur) +Dieses Projekt wurde für den internen Gebrauch bei Mercedes-Benz entwickelt. -Das MYP-System ist für eine **Zwei-Server-Architektur** optimiert: +--- -### Server-Architektur - -| Server | Hostname | Komponenten | URL | -|--------|----------|-------------|-----| -| **Frontend-Server** | `m040tbaraspi001.de040.corpintra.net` | Next.js + Docker + Caddy | `https://m040tbaraspi001.de040.corpintra.net` | -| **Backend-Server** | `raspberrypi` | Flask API + Web Interface + Kiosk | `https://raspberrypi` | - -### 🚀 Server-spezifische Installation (Empfohlen) - -Der `myp_installer.sh` erkennt automatisch den Server-Typ und bietet passende Installationsoptionen: - -```bash -./myp_installer.sh -# Wählen Sie: "1. Server-spezifische Installation (Empfohlen)" -``` - -#### Frontend-Server (m040tbaraspi001) -```bash -# Automatische Erkennung: m040tbaraspi001.de040.corpintra.net -# Verfügbare Optionen: -# 1. Frontend installieren (Next.js + Docker) -# 2. Frontend Produktions-Deployment (Port 443 mit SSL) -# 3. Nur Docker & Dependencies installieren -``` - -#### Backend-Server (Raspberry Pi) -```bash -# Automatische Erkennung: raspberrypi -# Verfügbare Optionen: -# 1. Backend installieren (Flask API + Web Interface) -# 2. Kiosk-Modus installieren (Touch-Interface) -# 3. Produktions-Setup (Backend + Kiosk + Services) -# 4. Nur Python & Dependencies installieren -``` - -### 🔧 Manuelle Installation - -#### Frontend-Server Setup -```bash -# Auf m040tbaraspi001.de040.corpintra.net -cd frontend/ -npm install -docker-compose up -d -``` - -#### Backend-Server Setup -```bash -# Auf raspberrypi -cd backend/app/ -python3.11 -m pip install -r requirements.txt -sudo systemctl start myp.service -sudo systemctl start myp-kiosk-browser.service # Optional: Kiosk-Modus -``` - -### 🌐 URLs & Zugriff - -#### Frontend (m040tbaraspi001) -- **Haupt-URL**: `https://m040tbaraspi001.de040.corpintra.net` -- **Entwicklung**: `http://localhost:3000` (npm run dev) -- **Produktion**: `https://localhost:443` (Docker) - -#### Backend (Raspberry Pi) -- **API**: `https://raspberrypi/api` -- **Web Interface**: `https://raspberrypi` -- **Kiosk-Modus**: `https://raspberrypi` (Vollbild-Touch-Interface) - -### 🔗 Server-Kommunikation - -Die Server kommunizieren über HTTPS: -- **Frontend → Backend**: `https://raspberrypi/api` -- **OAuth Callbacks**: `https://m040tbaraspi001.de040.corpintra.net/auth/login/callback` -- **Cross-Origin**: Automatisch konfiguriert für beide Domains - -### 🛠️ Konfiguration - -#### Frontend (.env.local) -```bash -# Backend-Verbindung -NEXT_PUBLIC_API_URL=https://raspberrypi -NEXT_PUBLIC_BACKEND_HOST=raspberrypi - -# Frontend-URLs -NEXT_PUBLIC_FRONTEND_URL=https://m040tbaraspi001.de040.corpintra.net -NEXTAUTH_URL=https://m040tbaraspi001.de040.corpintra.net -``` - -#### Backend (config/settings.py) -```python -# Frontend-Verbindung -FRONTEND_URL = "https://m040tbaraspi001.de040.corpintra.net" -CORS_ORIGINS = ["https://m040tbaraspi001.de040.corpintra.net"] - -# Kiosk-Konfiguration -KIOSK_MODE = True -KIOSK_AUTO_LOGIN = True -``` +**Backend-System**: Till Tomczak (Cyber-Physische Vernetzung & Hardware-Integration) +**Frontend-System**: Torben Haack (Progressive Web App & Analytics) +**Architektur**: Microservices mit REST-API-Integration +**Technologie**: Flask + SQLite (Backend) + Next.js + React (Frontend) +**Hardware**: Raspberry Pi + TP-Link Tapo P110 Smart-Plugs +**Entwickelt für**: Mercedes-Benz Werk 040 Berlin MYP diff --git a/backend/__pycache__/models.cpython-313.pyc b/backend/__pycache__/models.cpython-313.pyc index 68cc4d4b..1e98837a 100644 Binary files a/backend/__pycache__/models.cpython-313.pyc and b/backend/__pycache__/models.cpython-313.pyc differ diff --git a/backend/build-kiosk-css.ps1 b/backend/build-kiosk-css.ps1 new file mode 100644 index 00000000..8fa275c7 --- /dev/null +++ b/backend/build-kiosk-css.ps1 @@ -0,0 +1,151 @@ +# MYP Platform - Kiosk CSS Build Script (PowerShell) +# Optimiert und minifiziert CSS für maximale Performance im Offline-Kiosk-Modus + +Write-Host "Starte Kiosk CSS Build..." -ForegroundColor Green + +# Erstelle Build-Verzeichnis +if (!(Test-Path "static\css\build")) { + New-Item -ItemType Directory -Path "static\css\build" -Force | Out-Null +} + +# Prüfe ob Quelldateien existieren +if (!(Test-Path "static\css\kiosk-optimized.css")) { + Write-Host "FEHLER: static\css\kiosk-optimized.css nicht gefunden!" -ForegroundColor Red + exit 1 +} + +if (!(Test-Path "static\css\icons-minimal.css")) { + Write-Host "FEHLER: static\css\icons-minimal.css nicht gefunden!" -ForegroundColor Red + exit 1 +} + +if (!(Test-Path "static\css\critical-inline.css")) { + Write-Host "FEHLER: static\css\critical-inline.css nicht gefunden!" -ForegroundColor Red + exit 1 +} + +# Erstelle eine kombinierte CSS-Datei in korrekter Reihenfolge +Write-Host "Kombiniere CSS-Dateien..." -ForegroundColor Yellow + +$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" +$header = @" +/* MYP Platform - Kiosk Optimierte CSS Bundle */ +/* Generiert am: $timestamp */ + + +"@ + +# CSS-Dateien in optimaler Reihenfolge kombinieren +Write-Host "Lade kiosk-optimized.css..." -ForegroundColor Gray +$kioskCSS = Get-Content "static\css\kiosk-optimized.css" -Raw -Encoding UTF8 + +Write-Host "Lade icons-minimal.css..." -ForegroundColor Gray +$iconsCSS = Get-Content "static\css\icons-minimal.css" -Raw -Encoding UTF8 + +# Kombiniere CSS-Inhalte +$combinedCSS = $header + $kioskCSS + "`r`n`r`n" + $iconsCSS + +# TailwindCSS mit aggressivem Purging bauen +Write-Host "Purge TailwindCSS..." -ForegroundColor Yellow +try { + $process = Start-Process "npx" -ArgumentList "tailwindcss", "-i", "static\css\input.css", "-o", "static\css\build\tailwind-purged.css", "--minify" -Wait -PassThru -NoNewWindow + if ($process.ExitCode -eq 0) { + Write-Host "TailwindCSS erfolgreich gebaut" -ForegroundColor Green + $tailwindCSS = Get-Content "static\css\build\tailwind-purged.css" -Raw -Encoding UTF8 + $combinedCSS += "`r`n`r`n" + $tailwindCSS + } else { + Write-Host "TailwindCSS Build fehlgeschlagen, verwende ohne Tailwind" -ForegroundColor Yellow + } +} catch { + Write-Host "TailwindCSS Build fehlgeschlagen: $($_.Exception.Message)" -ForegroundColor Yellow +} + +# Kombinierte CSS speichern +Write-Host "Speichere kombinierte CSS..." -ForegroundColor Gray +$combinedCSS | Out-File -FilePath "static\css\build\kiosk-combined.css" -Encoding UTF8 + +# CSS minifizieren (falls cssnano verfügbar) +Write-Host "Minifiziere CSS..." -ForegroundColor Yellow +try { + $process = Start-Process "npx" -ArgumentList "cssnano", "static\css\build\kiosk-combined.css", "static\css\build\kiosk-production.css", "--no-map" -Wait -PassThru -NoNewWindow + if ($process.ExitCode -eq 0) { + Write-Host "CSS erfolgreich minifiziert" -ForegroundColor Green + } else { + Write-Host "cssnano fehlgeschlagen, verwende unminifizierte Version" -ForegroundColor Yellow + Copy-Item "static\css\build\kiosk-combined.css" "static\css\build\kiosk-production.css" + } +} catch { + Write-Host "cssnano nicht verfügbar, verwende unminifizierte Version" -ForegroundColor Yellow + Copy-Item "static\css\build\kiosk-combined.css" "static\css\build\kiosk-production.css" +} + +# Erstelle kritisches CSS für Inline-Verwendung +Write-Host "Erstelle kritisches CSS..." -ForegroundColor Yellow +Copy-Item "static\css\critical-inline.css" "static\css\build\critical.css" + +# Erstelle eine Version ohne FontAwesome +Write-Host "Entferne FontAwesome-Abhängigkeiten..." -ForegroundColor Yellow +if (Test-Path "static\css\build\kiosk-production.css") { + $content = Get-Content "static\css\build\kiosk-production.css" | Where-Object { $_ -notmatch "fontawesome" } + $content | Out-File -FilePath "static\css\build\kiosk-no-fa.css" -Encoding UTF8 +} + +# Dateigröße-Analyse +Write-Host "" +Write-Host "Dateigröße-Analyse:" -ForegroundColor Cyan + +if (Test-Path "static\css\build\critical.css") { + $criticalSize = (Get-Item "static\css\build\critical.css").Length + Write-Host "Critical CSS (inline): $criticalSize bytes" +} + +if (Test-Path "static\css\build\kiosk-production.css") { + $kioskSize = (Get-Item "static\css\build\kiosk-production.css").Length + Write-Host "Kiosk Production CSS: $kioskSize bytes" +} + +if (Test-Path "static\css\build\kiosk-no-fa.css") { + $noFaSize = (Get-Item "static\css\build\kiosk-no-fa.css").Length + Write-Host "Ohne FontAwesome: $noFaSize bytes" +} + +# Vergleich mit Original-Dateien +if ((Test-Path "static\css\tailwind.min.css") -and (Test-Path "static\css\build\kiosk-production.css")) { + $originalSize = (Get-Item "static\css\tailwind.min.css").Length + $optimizedSize = (Get-Item "static\css\build\kiosk-production.css").Length + if ($originalSize -gt 0) { + $reduction = [math]::Round((1 - ($optimizedSize / $originalSize)) * 100, 1) + Write-Host "TailwindCSS Original: $originalSize bytes" + Write-Host "Größenreduktion: $reduction%" -ForegroundColor Green + } +} + +# Cache-Busting Hash generieren +if (Test-Path "static\css\build\kiosk-production.css") { + $hash = (Get-FileHash "static\css\build\kiosk-production.css" -Algorithm SHA256).Hash.Substring(0, 8).ToLower() + Copy-Item "static\css\build\kiosk-production.css" "static\css\build\kiosk-$hash.css" + + Write-Host "" + Write-Host "Build abgeschlossen!" -ForegroundColor Green + Write-Host "" + Write-Host "Generierte Dateien:" -ForegroundColor Cyan + Write-Host " • static\css\build\critical.css (inline verwenden)" + Write-Host " • static\css\build\kiosk-production.css (Haupt-CSS)" + Write-Host " • static\css\build\kiosk-$hash.css (mit Cache-Busting)" + Write-Host "" + Write-Host "HTML-Integration:" -ForegroundColor Cyan + Write-Host ' ' + Write-Host ' ' + Write-Host "" + Write-Host "Für maximale Performance: Service Worker entfernen und nur statische CSS verwenden" -ForegroundColor Blue +} + +# Cleanup temporärer Dateien +if (Test-Path "static\css\build\kiosk-combined.css") { + Remove-Item "static\css\build\kiosk-combined.css" +} +if (Test-Path "static\css\build\tailwind-purged.css") { + Remove-Item "static\css\build\tailwind-purged.css" +} + +Write-Host "Cleanup abgeschlossen" -ForegroundColor Green \ No newline at end of file diff --git a/backend/build-kiosk-css.sh b/backend/build-kiosk-css.sh new file mode 100644 index 00000000..bf43af24 --- /dev/null +++ b/backend/build-kiosk-css.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# MYP Platform - Kiosk CSS Build Script +# Optimiert und minifiziert CSS für maximale Performance im Offline-Kiosk-Modus + +echo "🚀 Starte Kiosk CSS Build..." + +# Erstelle Build-Verzeichnis +mkdir -p static/css/build + +# Erstelle eine kombinierte CSS-Datei in korrekter Reihenfolge +echo "📦 Kombiniere CSS-Dateien..." + +cat > static/css/build/kiosk-combined.css << 'EOF' +/* MYP Platform - Kiosk Optimierte CSS Bundle */ +/* Generiert am: $(date) */ + +EOF + +# CSS-Dateien in optimaler Reihenfolge kombinieren +cat static/css/kiosk-optimized.css >> static/css/build/kiosk-combined.css +echo "" >> static/css/build/kiosk-combined.css +cat static/css/icons-minimal.css >> static/css/build/kiosk-combined.css + +# TailwindCSS mit aggressivem Purging bauen +echo "🎯 Purge TailwindCSS..." +npx tailwindcss -i static/css/input.css -o static/css/build/tailwind-purged.css --minify + +# Kombiniere mit Tailwind +echo "" >> static/css/build/kiosk-combined.css +cat static/css/build/tailwind-purged.css >> static/css/build/kiosk-combined.css + +# CSS minifizieren (falls cssnano verfügbar) +if command -v npx &> /dev/null; then + echo "🗜️ Minifiziere CSS..." + npx cssnano static/css/build/kiosk-combined.css static/css/build/kiosk-production.css --no-map +else + echo "⚠️ cssnano nicht verfügbar, verwende unminifizierte Version" + cp static/css/build/kiosk-combined.css static/css/build/kiosk-production.css +fi + +# Erstelle kritisches CSS für Inline-Verwendung +echo "⚡ Erstelle kritisches CSS..." +cp static/css/critical-inline.css static/css/build/critical.css + +# Erstelle eine Version ohne FontAwesome +echo "🚫 Entferne FontAwesome-Abhängigkeiten..." +grep -v "fontawesome" static/css/build/kiosk-production.css > static/css/build/kiosk-no-fa.css + +# Dateigröße-Analyse +echo "" +echo "📊 Dateigröße-Analyse:" +echo "Critical CSS (inline): $(wc -c < static/css/build/critical.css) bytes" +echo "Kiosk Production CSS: $(wc -c < static/css/build/kiosk-production.css) bytes" +echo "Ohne FontAwesome: $(wc -c < static/css/build/kiosk-no-fa.css) bytes" + +# Vergleich mit Original-Dateien +if [ -f "static/css/tailwind.min.css" ]; then + original_size=$(wc -c < static/css/tailwind.min.css) + optimized_size=$(wc -c < static/css/build/kiosk-production.css) + reduction=$((100 - (optimized_size * 100 / original_size))) + echo "TailwindCSS Original: $original_size bytes" + echo "Größenreduktion: $reduction%" +fi + +# Cache-Busting Hash generieren +hash=$(sha256sum static/css/build/kiosk-production.css | cut -d' ' -f1 | cut -c1-8) +cp static/css/build/kiosk-production.css static/css/build/kiosk-$hash.css + +echo "" +echo "✅ Build abgeschlossen!" +echo "" +echo "📁 Generierte Dateien:" +echo " • static/css/build/critical.css (inline verwenden)" +echo " • static/css/build/kiosk-production.css (Haupt-CSS)" +echo " • static/css/build/kiosk-$hash.css (mit Cache-Busting)" +echo "" +echo "🔧 HTML-Integration:" +echo ' ' +echo ' ' +echo "" +echo "💡 Für maximale Performance: Service Worker entfernen und nur statische CSS verwenden" + +# Cleanup temporärer Dateien +rm -f static/css/build/kiosk-combined.css +rm -f static/css/build/tailwind-purged.css + +echo "🧹 Cleanup abgeschlossen" \ No newline at end of file diff --git a/backend/database/myp.db b/backend/database/myp.db index 831cc97e..52da9dcb 100644 Binary files a/backend/database/myp.db and b/backend/database/myp.db differ diff --git a/backend/docs/CSS_OPTIMIERUNGEN.md b/backend/docs/CSS_OPTIMIERUNGEN.md new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/backend/docs/CSS_OPTIMIERUNGEN.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/docs/KIOSK_OPTIMIERUNG.md b/backend/docs/KIOSK_OPTIMIERUNG.md new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/backend/docs/KIOSK_OPTIMIERUNG.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/docs/OPTIMIERUNG_ZUSAMMENFASSUNG.md b/backend/docs/OPTIMIERUNG_ZUSAMMENFASSUNG.md new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/backend/docs/OPTIMIERUNG_ZUSAMMENFASSUNG.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/docs/ROADMAP_UPDATE_KIOSK.md b/backend/docs/ROADMAP_UPDATE_KIOSK.md new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/backend/docs/ROADMAP_UPDATE_KIOSK.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/logs/analytics/analytics.log b/backend/logs/analytics/analytics.log index d63cfac5..67acb4be 100644 --- a/backend/logs/analytics/analytics.log +++ b/backend/logs/analytics/analytics.log @@ -87,3 +87,7 @@ 2025-06-01 22:39:54 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert 2025-06-01 22:39:56 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert 2025-06-01 22:40:09 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert +2025-06-01 23:08:24 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert +2025-06-01 23:10:01 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert +2025-06-01 23:16:55 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert +2025-06-01 23:32:06 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert diff --git a/backend/logs/app/app.log b/backend/logs/app/app.log index 4083fd52..7ba66dbd 100644 --- a/backend/logs/app/app.log +++ b/backend/logs/app/app.log @@ -2099,3 +2099,123 @@ WHERE jobs.status = ?) AS anon_1] 2025-06-01 22:40:14 - [app] app - [INFO] INFO - Job-Scheduler gestartet 2025-06-01 22:40:14 - [app] app - [INFO] INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP) 2025-06-01 22:40:14 - [app] app - [INFO] INFO - Windows-Debug-Modus: Auto-Reload deaktiviert +2025-06-01 23:08:24 - [app] app - [INFO] INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\database\myp.db +2025-06-01 23:08:25 - [app] app - [INFO] INFO - SQLite für Raspberry Pi optimiert (reduzierte Cache-Größe, SD-Karten I/O) +2025-06-01 23:08:25 - [app] app - [INFO] INFO - ✅ Timeout Force-Quit Manager geladen +2025-06-01 23:08:25 - [app] app - [INFO] INFO - ✅ Zentraler Shutdown-Manager initialisiert +2025-06-01 23:08:25 - [app] app - [INFO] INFO - 🔄 Starte Datenbank-Setup und Migrationen... +2025-06-01 23:08:25 - [app] app - [INFO] INFO - Datenbank mit Optimierungen initialisiert +2025-06-01 23:08:25 - [app] app - [INFO] INFO - ✅ JobOrder-Tabelle bereits vorhanden +2025-06-01 23:08:25 - [app] app - [INFO] INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt. +2025-06-01 23:08:25 - [app] app - [INFO] INFO - ✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen +2025-06-01 23:08:25 - [app] app - [INFO] INFO - 🖨️ Starte automatische Steckdosen-Initialisierung... +2025-06-01 23:08:29 - [app] app - [INFO] INFO - ✅ Steckdosen-Initialisierung: 0/2 Drucker erfolgreich +2025-06-01 23:08:29 - [app] app - [WARNING] WARNING - ⚠️ 2 Drucker konnten nicht initialisiert werden +2025-06-01 23:08:29 - [app] app - [INFO] INFO - 🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung +2025-06-01 23:08:29 - [app] app - [INFO] INFO - Job-Scheduler gestartet +2025-06-01 23:08:29 - [app] app - [INFO] INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP) +2025-06-01 23:08:29 - [app] app - [INFO] INFO - Windows-Debug-Modus: Auto-Reload deaktiviert +2025-06-01 23:10:01 - [app] app - [INFO] INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\database\myp.db +2025-06-01 23:10:02 - [app] app - [INFO] INFO - SQLite für Raspberry Pi optimiert (reduzierte Cache-Größe, SD-Karten I/O) +2025-06-01 23:10:02 - [app] app - [INFO] INFO - ✅ Timeout Force-Quit Manager geladen +2025-06-01 23:10:02 - [app] app - [INFO] INFO - ✅ Zentraler Shutdown-Manager initialisiert +2025-06-01 23:10:02 - [app] app - [INFO] INFO - 🔄 Starte Datenbank-Setup und Migrationen... +2025-06-01 23:10:02 - [app] app - [INFO] INFO - Datenbank mit Optimierungen initialisiert +2025-06-01 23:10:02 - [app] app - [INFO] INFO - ✅ JobOrder-Tabelle bereits vorhanden +2025-06-01 23:10:03 - [app] app - [INFO] INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt. +2025-06-01 23:10:03 - [app] app - [INFO] INFO - ✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen +2025-06-01 23:10:03 - [app] app - [INFO] INFO - 🖨️ Starte automatische Steckdosen-Initialisierung... +2025-06-01 23:10:07 - [app] app - [INFO] INFO - ✅ Steckdosen-Initialisierung: 0/2 Drucker erfolgreich +2025-06-01 23:10:07 - [app] app - [WARNING] WARNING - ⚠️ 2 Drucker konnten nicht initialisiert werden +2025-06-01 23:10:07 - [app] app - [INFO] INFO - 🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung +2025-06-01 23:10:07 - [app] app - [INFO] INFO - Job-Scheduler gestartet +2025-06-01 23:10:07 - [app] app - [INFO] INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP) +2025-06-01 23:10:07 - [app] app - [INFO] INFO - Windows-Debug-Modus: Auto-Reload deaktiviert +2025-06-01 23:10:11 - [app] app - [INFO] INFO - Admin-Check für Funktion admin_page: User authenticated: True, User ID: 1, Is Admin: True +2025-06-01 23:10:11 - [app] app - [INFO] INFO - Admin-Check für Funktion api_admin_system_health: User authenticated: True, User ID: 1, Is Admin: True +2025-06-01 23:16:55 - [app] app - [INFO] INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\database\myp.db +2025-06-01 23:16:56 - [app] app - [INFO] INFO - SQLite für Raspberry Pi optimiert (reduzierte Cache-Größe, SD-Karten I/O) +2025-06-01 23:16:56 - [app] app - [INFO] INFO - ✅ Timeout Force-Quit Manager geladen +2025-06-01 23:16:56 - [app] app - [INFO] INFO - ✅ Zentraler Shutdown-Manager initialisiert +2025-06-01 23:16:56 - [app] app - [INFO] INFO - 🔄 Starte Datenbank-Setup und Migrationen... +2025-06-01 23:16:56 - [app] app - [INFO] INFO - Datenbank mit Optimierungen initialisiert +2025-06-01 23:16:56 - [app] app - [INFO] INFO - ✅ JobOrder-Tabelle bereits vorhanden +2025-06-01 23:16:56 - [app] app - [INFO] INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt. +2025-06-01 23:16:56 - [app] app - [INFO] INFO - ✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen +2025-06-01 23:16:56 - [app] app - [INFO] INFO - 🖨️ Starte automatische Steckdosen-Initialisierung... +2025-06-01 23:17:00 - [app] app - [INFO] INFO - ✅ Steckdosen-Initialisierung: 0/2 Drucker erfolgreich +2025-06-01 23:17:00 - [app] app - [WARNING] WARNING - ⚠️ 2 Drucker konnten nicht initialisiert werden +2025-06-01 23:17:00 - [app] app - [INFO] INFO - 🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung +2025-06-01 23:17:00 - [app] app - [INFO] INFO - Job-Scheduler gestartet +2025-06-01 23:17:00 - [app] app - [INFO] INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP) +2025-06-01 23:17:00 - [app] app - [INFO] INFO - Windows-Debug-Modus: Auto-Reload deaktiviert +2025-06-01 23:17:28 - [app] app - [INFO] INFO - Admin-Check für Funktion admin_page: User authenticated: True, User ID: 1, Is Admin: True +2025-06-01 23:17:29 - [app] app - [INFO] INFO - Admin-Check für Funktion api_admin_system_health: User authenticated: True, User ID: 1, Is Admin: True +2025-06-01 23:18:51 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:18:51 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:18:51 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:18:51 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:19:46 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:19:46 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:19:47 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:19:47 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:20:34 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:20:34 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:20:34 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:20:34 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:21:18 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:21:18 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:21:18 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:21:18 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:21:58 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:21:58 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:21:58 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:21:58 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:22:39 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:22:39 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:22:39 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:22:39 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:32:05 - [app] app - [INFO] INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\database\myp.db +2025-06-01 23:32:07 - [app] app - [INFO] INFO - SQLite für Raspberry Pi optimiert (reduzierte Cache-Größe, SD-Karten I/O) +2025-06-01 23:32:07 - [app] app - [INFO] INFO - ✅ Timeout Force-Quit Manager geladen +2025-06-01 23:32:07 - [app] app - [INFO] INFO - ✅ Zentraler Shutdown-Manager initialisiert +2025-06-01 23:32:07 - [app] app - [INFO] INFO - 🔄 Starte Datenbank-Setup und Migrationen... +2025-06-01 23:32:07 - [app] app - [INFO] INFO - Datenbank mit Optimierungen initialisiert +2025-06-01 23:32:07 - [app] app - [INFO] INFO - ✅ JobOrder-Tabelle bereits vorhanden +2025-06-01 23:32:08 - [app] app - [INFO] INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt. +2025-06-01 23:32:08 - [app] app - [INFO] INFO - ✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen +2025-06-01 23:32:08 - [app] app - [INFO] INFO - 🖨️ Starte automatische Steckdosen-Initialisierung... +2025-06-01 23:32:12 - [app] app - [INFO] INFO - ✅ Steckdosen-Initialisierung: 0/2 Drucker erfolgreich +2025-06-01 23:32:12 - [app] app - [WARNING] WARNING - ⚠️ 2 Drucker konnten nicht initialisiert werden +2025-06-01 23:32:12 - [app] app - [INFO] INFO - 🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung +2025-06-01 23:32:12 - [app] app - [INFO] INFO - Job-Scheduler gestartet +2025-06-01 23:32:12 - [app] app - [INFO] INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP) +2025-06-01 23:32:12 - [app] app - [INFO] INFO - Windows-Debug-Modus: Auto-Reload deaktiviert +2025-06-01 23:33:27 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:33:27 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:33:27 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:33:27 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:34:10 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:34:10 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:34:10 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:34:10 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:34:56 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:34:56 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:34:57 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:34:57 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:35:55 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:35:55 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:35:55 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:35:55 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:36:45 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:36:45 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:36:45 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:36:45 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:37:25 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:37:25 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:37:25 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:37:25 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:38:06 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:38:06 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} +2025-06-01 23:38:06 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-01 23:38:06 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2} diff --git a/backend/logs/backup/backup.log b/backend/logs/backup/backup.log index 179a0d99..999c9b39 100644 --- a/backend/logs/backup/backup.log +++ b/backend/logs/backup/backup.log @@ -91,3 +91,7 @@ 2025-06-01 22:39:54 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) 2025-06-01 22:39:56 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) 2025-06-01 22:40:09 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) +2025-06-01 23:08:24 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) +2025-06-01 23:10:01 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) +2025-06-01 23:16:55 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) +2025-06-01 23:32:06 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) diff --git a/backend/logs/calendar/calendar.log b/backend/logs/calendar/calendar.log index 6161fb2e..7cf0cfba 100644 --- a/backend/logs/calendar/calendar.log +++ b/backend/logs/calendar/calendar.log @@ -37,3 +37,4 @@ 2025-06-01 19:09:10 - [calendar] calendar - [INFO] INFO - 📅 Kalender-Events abgerufen: 16 Einträge für Zeitraum 2025-06-01 00:00:00 bis 2025-06-08 00:00:00 2025-06-01 21:14:20 - [calendar] calendar - [INFO] INFO - 📅 Kalender-Events abgerufen: 16 Einträge für Zeitraum 2025-06-01 00:00:00 bis 2025-06-08 00:00:00 2025-06-01 21:14:43 - [calendar] calendar - [INFO] INFO - 📅 Kalender-Events abgerufen: 16 Einträge für Zeitraum 2025-06-01 00:00:00 bis 2025-06-08 00:00:00 +2025-06-01 23:32:26 - [calendar] calendar - [INFO] INFO - 📅 Kalender-Events abgerufen: 16 Einträge für Zeitraum 2025-06-01 00:00:00 bis 2025-06-08 00:00:00 diff --git a/backend/logs/dashboard/dashboard.log b/backend/logs/dashboard/dashboard.log index 60cf01be..e61cb60f 100644 --- a/backend/logs/dashboard/dashboard.log +++ b/backend/logs/dashboard/dashboard.log @@ -349,3 +349,19 @@ 2025-06-01 22:40:10 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet 2025-06-01 22:40:10 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server wird mit threading initialisiert (eventlet-Fallback) 2025-06-01 22:40:10 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server initialisiert (async_mode: threading) +2025-06-01 23:08:25 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet +2025-06-01 23:08:25 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet +2025-06-01 23:08:25 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server wird mit threading initialisiert (eventlet-Fallback) +2025-06-01 23:08:25 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server initialisiert (async_mode: threading) +2025-06-01 23:10:02 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet +2025-06-01 23:10:02 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet +2025-06-01 23:10:02 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server wird mit threading initialisiert (eventlet-Fallback) +2025-06-01 23:10:02 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server initialisiert (async_mode: threading) +2025-06-01 23:16:56 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet +2025-06-01 23:16:56 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet +2025-06-01 23:16:56 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server wird mit threading initialisiert (eventlet-Fallback) +2025-06-01 23:16:56 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server initialisiert (async_mode: threading) +2025-06-01 23:32:07 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet +2025-06-01 23:32:07 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet +2025-06-01 23:32:07 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server wird mit threading initialisiert (eventlet-Fallback) +2025-06-01 23:32:07 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server initialisiert (async_mode: threading) diff --git a/backend/logs/database/database.log b/backend/logs/database/database.log index e98f192d..174c68fc 100644 --- a/backend/logs/database/database.log +++ b/backend/logs/database/database.log @@ -87,3 +87,7 @@ 2025-06-01 22:39:54 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet 2025-06-01 22:39:56 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet 2025-06-01 22:40:09 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet +2025-06-01 23:08:24 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet +2025-06-01 23:10:01 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet +2025-06-01 23:16:55 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet +2025-06-01 23:32:06 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet diff --git a/backend/logs/email_notification/email_notification.log b/backend/logs/email_notification/email_notification.log index bb406f17..7f396a3a 100644 --- a/backend/logs/email_notification/email_notification.log +++ b/backend/logs/email_notification/email_notification.log @@ -84,3 +84,7 @@ 2025-06-01 22:39:54 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) 2025-06-01 22:39:57 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) 2025-06-01 22:40:10 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) +2025-06-01 23:08:25 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) +2025-06-01 23:10:02 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) +2025-06-01 23:16:56 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) +2025-06-01 23:32:07 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) diff --git a/backend/logs/jobs/jobs.log b/backend/logs/jobs/jobs.log index 9c69c6c6..aeb2a558 100644 --- a/backend/logs/jobs/jobs.log +++ b/backend/logs/jobs/jobs.log @@ -124,3 +124,6 @@ WHERE printers.id = ?] 2025-06-01 19:04:21 - [jobs] jobs - [INFO] INFO - Jobs abgerufen: 16 von 16 (Seite 1) 2025-06-01 21:14:21 - [jobs] jobs - [INFO] INFO - Jobs abgerufen: 16 von 16 (Seite 1) 2025-06-01 21:14:25 - [jobs] jobs - [INFO] INFO - Jobs abgerufen: 16 von 16 (Seite 1) +2025-06-01 23:10:14 - [jobs] jobs - [INFO] INFO - Jobs abgerufen: 16 von 16 (Seite 1) +2025-06-01 23:17:20 - [jobs] jobs - [INFO] INFO - Jobs abgerufen: 16 von 16 (Seite 1) +2025-06-01 23:32:17 - [jobs] jobs - [INFO] INFO - Jobs abgerufen: 16 von 16 (Seite 1) diff --git a/backend/logs/maintenance/maintenance.log b/backend/logs/maintenance/maintenance.log index 3f7b52cf..3df99f55 100644 --- a/backend/logs/maintenance/maintenance.log +++ b/backend/logs/maintenance/maintenance.log @@ -172,3 +172,11 @@ 2025-06-01 22:39:57 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet 2025-06-01 22:40:10 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet 2025-06-01 22:40:10 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet +2025-06-01 23:08:25 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet +2025-06-01 23:08:25 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet +2025-06-01 23:10:02 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet +2025-06-01 23:10:02 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet +2025-06-01 23:16:56 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet +2025-06-01 23:16:56 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet +2025-06-01 23:32:07 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet +2025-06-01 23:32:07 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet diff --git a/backend/logs/multi_location/multi_location.log b/backend/logs/multi_location/multi_location.log index 97548603..125ab053 100644 --- a/backend/logs/multi_location/multi_location.log +++ b/backend/logs/multi_location/multi_location.log @@ -172,3 +172,11 @@ 2025-06-01 22:39:57 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt 2025-06-01 22:40:10 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt 2025-06-01 22:40:10 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt +2025-06-01 23:08:25 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt +2025-06-01 23:08:25 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt +2025-06-01 23:10:02 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt +2025-06-01 23:10:02 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt +2025-06-01 23:16:56 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt +2025-06-01 23:16:56 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt +2025-06-01 23:32:07 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt +2025-06-01 23:32:07 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt diff --git a/backend/logs/permissions/permissions.log b/backend/logs/permissions/permissions.log index ae6bd848..55d8f750 100644 --- a/backend/logs/permissions/permissions.log +++ b/backend/logs/permissions/permissions.log @@ -86,3 +86,7 @@ 2025-06-01 22:39:55 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert 2025-06-01 22:39:57 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert 2025-06-01 22:40:10 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert +2025-06-01 23:08:25 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert +2025-06-01 23:10:02 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert +2025-06-01 23:16:56 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert +2025-06-01 23:32:07 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert diff --git a/backend/logs/printer_monitor/printer_monitor.log b/backend/logs/printer_monitor/printer_monitor.log index 133b04f0..8552f55e 100644 --- a/backend/logs/printer_monitor/printer_monitor.log +++ b/backend/logs/printer_monitor/printer_monitor.log @@ -2536,3 +2536,120 @@ 2025-06-01 22:40:23 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 3/6: 192.168.0.100 2025-06-01 22:40:28 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 6/6: 192.168.0.105 2025-06-01 22:40:29 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 4/6: 192.168.0.101 +2025-06-01 23:08:24 - [printer_monitor] printer_monitor - [INFO] INFO - 🖨️ Drucker-Monitor initialisiert +2025-06-01 23:08:24 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-06-01 23:08:25 - [printer_monitor] printer_monitor - [INFO] INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart... +2025-06-01 23:08:26 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Starte automatische Tapo-Steckdosenerkennung... +2025-06-01 23:08:26 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Teste 6 Standard-IPs aus der Konfiguration +2025-06-01 23:08:26 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 1/6: 192.168.0.103 +2025-06-01 23:08:27 - [printer_monitor] printer_monitor - [WARNING] WARNING - ❌ Tapo P110 (192.168.0.103): Steckdose konnte nicht ausgeschaltet werden +2025-06-01 23:08:29 - [printer_monitor] printer_monitor - [WARNING] WARNING - ❌ Tapo P110 (192.168.0.104): Steckdose konnte nicht ausgeschaltet werden +2025-06-01 23:08:29 - [printer_monitor] printer_monitor - [INFO] INFO - 🎯 Steckdosen-Initialisierung abgeschlossen: 0/2 erfolgreich +2025-06-01 23:08:32 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 2/6: 192.168.0.104 +2025-06-01 23:08:38 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 3/6: 192.168.0.100 +2025-06-01 23:08:44 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 4/6: 192.168.0.101 +2025-06-01 23:08:50 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 5/6: 192.168.0.102 +2025-06-01 23:08:56 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 6/6: 192.168.0.105 +2025-06-01 23:09:02 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Steckdosen-Erkennung abgeschlossen: 0/6 Steckdosen gefunden in 36.2s +2025-06-01 23:10:01 - [printer_monitor] printer_monitor - [INFO] INFO - 🖨️ Drucker-Monitor initialisiert +2025-06-01 23:10:01 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-06-01 23:10:03 - [printer_monitor] printer_monitor - [INFO] INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart... +2025-06-01 23:10:03 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Starte automatische Tapo-Steckdosenerkennung... +2025-06-01 23:10:03 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Teste 6 Standard-IPs aus der Konfiguration +2025-06-01 23:10:03 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 1/6: 192.168.0.103 +2025-06-01 23:10:05 - [printer_monitor] printer_monitor - [WARNING] WARNING - ❌ Tapo P110 (192.168.0.103): Steckdose konnte nicht ausgeschaltet werden +2025-06-01 23:10:07 - [printer_monitor] printer_monitor - [WARNING] WARNING - ❌ Tapo P110 (192.168.0.104): Steckdose konnte nicht ausgeschaltet werden +2025-06-01 23:10:07 - [printer_monitor] printer_monitor - [INFO] INFO - 🎯 Steckdosen-Initialisierung abgeschlossen: 0/2 erfolgreich +2025-06-01 23:10:09 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 2/6: 192.168.0.104 +2025-06-01 23:10:11 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:10:11 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:10:15 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:10:15 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:10:15 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 3/6: 192.168.0.100 +2025-06-01 23:10:17 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:10:17 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:10:18 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:10:18 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:10:20 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:10:20 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:10:20 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker +2025-06-01 23:10:21 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 4/6: 192.168.0.101 +2025-06-01 23:10:24 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:10:24 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:10:24 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker +2025-06-01 23:10:26 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:10:26 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:10:26 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker +2025-06-01 23:10:27 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:10:27 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:10:27 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker +2025-06-01 23:10:28 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 5/6: 192.168.0.102 +2025-06-01 23:10:34 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 6/6: 192.168.0.105 +2025-06-01 23:10:40 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Steckdosen-Erkennung abgeschlossen: 0/6 Steckdosen gefunden in 36.1s +2025-06-01 23:16:55 - [printer_monitor] printer_monitor - [INFO] INFO - 🖨️ Drucker-Monitor initialisiert +2025-06-01 23:16:55 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-06-01 23:16:56 - [printer_monitor] printer_monitor - [INFO] INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart... +2025-06-01 23:16:57 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Starte automatische Tapo-Steckdosenerkennung... +2025-06-01 23:16:57 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Teste 6 Standard-IPs aus der Konfiguration +2025-06-01 23:16:57 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 1/6: 192.168.0.103 +2025-06-01 23:16:58 - [printer_monitor] printer_monitor - [WARNING] WARNING - ❌ Tapo P110 (192.168.0.103): Steckdose konnte nicht ausgeschaltet werden +2025-06-01 23:17:00 - [printer_monitor] printer_monitor - [WARNING] WARNING - ❌ Tapo P110 (192.168.0.104): Steckdose konnte nicht ausgeschaltet werden +2025-06-01 23:17:00 - [printer_monitor] printer_monitor - [INFO] INFO - 🎯 Steckdosen-Initialisierung abgeschlossen: 0/2 erfolgreich +2025-06-01 23:17:03 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 2/6: 192.168.0.104 +2025-06-01 23:17:09 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 3/6: 192.168.0.100 +2025-06-01 23:17:15 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 4/6: 192.168.0.101 +2025-06-01 23:17:21 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:17:21 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:17:21 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 5/6: 192.168.0.102 +2025-06-01 23:17:23 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:17:23 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:17:27 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 6/6: 192.168.0.105 +2025-06-01 23:17:30 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:17:30 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:17:30 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker +2025-06-01 23:17:32 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:17:32 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:17:32 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker +2025-06-01 23:17:33 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Steckdosen-Erkennung abgeschlossen: 0/6 Steckdosen gefunden in 36.1s +2025-06-01 23:22:40 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:22:40 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:22:47 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:22:47 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:22:49 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:22:49 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:22:49 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker +2025-06-01 23:32:06 - [printer_monitor] printer_monitor - [INFO] INFO - 🖨️ Drucker-Monitor initialisiert +2025-06-01 23:32:06 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-06-01 23:32:08 - [printer_monitor] printer_monitor - [INFO] INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart... +2025-06-01 23:32:08 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Starte automatische Tapo-Steckdosenerkennung... +2025-06-01 23:32:08 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Teste 6 Standard-IPs aus der Konfiguration +2025-06-01 23:32:08 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 1/6: 192.168.0.103 +2025-06-01 23:32:10 - [printer_monitor] printer_monitor - [WARNING] WARNING - ❌ Tapo P110 (192.168.0.103): Steckdose konnte nicht ausgeschaltet werden +2025-06-01 23:32:12 - [printer_monitor] printer_monitor - [WARNING] WARNING - ❌ Tapo P110 (192.168.0.104): Steckdose konnte nicht ausgeschaltet werden +2025-06-01 23:32:12 - [printer_monitor] printer_monitor - [INFO] INFO - 🎯 Steckdosen-Initialisierung abgeschlossen: 0/2 erfolgreich +2025-06-01 23:32:13 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:32:13 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:32:14 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 2/6: 192.168.0.104 +2025-06-01 23:32:16 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:32:16 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:32:20 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 3/6: 192.168.0.100 +2025-06-01 23:32:22 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:32:22 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:32:22 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker +2025-06-01 23:32:25 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:32:25 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:32:25 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker +2025-06-01 23:32:26 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 4/6: 192.168.0.101 +2025-06-01 23:32:32 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 5/6: 192.168.0.102 +2025-06-01 23:32:38 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Teste IP 6/6: 192.168.0.105 +2025-06-01 23:32:44 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Steckdosen-Erkennung abgeschlossen: 0/6 Steckdosen gefunden in 36.1s +2025-06-01 23:37:25 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:37:25 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:37:34 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-01 23:37:34 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Prüfe Status von 2 aktiven Druckern... +2025-06-01 23:37:34 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:37:34 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:37:34 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker +2025-06-01 23:37:43 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.104): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:37:43 - [printer_monitor] printer_monitor - [WARNING] WARNING - 🔌 Tapo P110 (192.168.0.103): UNREACHABLE (Ping fehlgeschlagen) +2025-06-01 23:37:43 - [printer_monitor] printer_monitor - [INFO] INFO - ✅ Status-Update abgeschlossen für 2 Drucker diff --git a/backend/logs/printers/printers.log b/backend/logs/printers/printers.log index 4f8dfd5c..1ed6222d 100644 --- a/backend/logs/printers/printers.log +++ b/backend/logs/printers/printers.log @@ -5056,3 +5056,131 @@ 2025-06-01 21:14:47 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) 2025-06-01 21:14:47 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker 2025-06-01 21:14:47 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.41ms +2025-06-01 23:10:11 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:10:14 - [printers] printers - [INFO] INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check) +2025-06-01 23:10:15 - [printers] printers - [INFO] INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check) +2025-06-01 23:10:15 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:10:17 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:10:18 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:10:20 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:10:20 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9029.96ms +2025-06-01 23:10:24 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:10:24 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9041.46ms +2025-06-01 23:10:26 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:10:26 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9022.98ms +2025-06-01 23:10:27 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:10:27 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9032.41ms +2025-06-01 23:17:20 - [printers] printers - [INFO] INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check) +2025-06-01 23:17:21 - [printers] printers - [INFO] INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check) +2025-06-01 23:17:21 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:17:23 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:17:30 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:17:30 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9031.02ms +2025-06-01 23:17:32 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:17:32 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9018.02ms +2025-06-01 23:17:32 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:17:32 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:17:32 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.24ms +2025-06-01 23:17:33 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:17:33 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:17:33 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.33ms +2025-06-01 23:18:21 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:18:21 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:18:21 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.31ms +2025-06-01 23:18:51 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:18:51 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:18:51 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.45ms +2025-06-01 23:19:08 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:19:08 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:19:08 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.37ms +2025-06-01 23:19:15 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:19:15 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:19:15 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.27ms +2025-06-01 23:19:47 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:19:47 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:19:47 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.31ms +2025-06-01 23:20:04 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:20:04 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:20:04 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.41ms +2025-06-01 23:20:35 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:20:35 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:20:35 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.32ms +2025-06-01 23:20:48 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:20:48 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:20:48 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.27ms +2025-06-01 23:21:18 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:21:18 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:21:18 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.29ms +2025-06-01 23:21:27 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:21:27 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:21:27 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.49ms +2025-06-01 23:22:09 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:22:09 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:22:09 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.44ms +2025-06-01 23:22:40 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:22:47 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:22:49 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:22:49 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9004.42ms +2025-06-01 23:32:13 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:32:16 - [printers] printers - [INFO] INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check) +2025-06-01 23:32:16 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:32:17 - [printers] printers - [INFO] INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check) +2025-06-01 23:32:22 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:32:22 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9030.61ms +2025-06-01 23:32:25 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:32:25 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9035.13ms +2025-06-01 23:32:31 - [printers] printers - [INFO] INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check) +2025-06-01 23:32:31 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:32:31 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:32:31 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.35ms +2025-06-01 23:32:33 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:32:33 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:32:33 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.66ms +2025-06-01 23:32:35 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:32:35 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:32:35 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 2.22ms +2025-06-01 23:32:57 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:32:57 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:32:57 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.29ms +2025-06-01 23:33:27 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:33:27 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:33:27 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.34ms +2025-06-01 23:33:40 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:33:40 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:33:40 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.38ms +2025-06-01 23:34:10 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:34:10 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:34:10 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.34ms +2025-06-01 23:34:26 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:34:26 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:34:26 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.27ms +2025-06-01 23:34:57 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:34:57 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:34:57 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.33ms +2025-06-01 23:35:10 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:35:10 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:35:10 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.48ms +2025-06-01 23:35:55 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:35:55 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:35:55 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.51ms +2025-06-01 23:36:05 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:36:05 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:36:05 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.39ms +2025-06-01 23:36:45 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:36:45 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:36:45 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.25ms +2025-06-01 23:36:55 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:36:55 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:36:55 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.31ms +2025-06-01 23:37:25 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:37:34 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:37:34 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:37:34 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9033.19ms +2025-06-01 23:37:43 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:37:43 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 9021.61ms +2025-06-01 23:38:06 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:38:06 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:38:06 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.30ms +2025-06-01 23:38:20 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-01 23:38:20 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 2 Drucker +2025-06-01 23:38:20 - [printers] printers - [INFO] INFO - ✅ API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 0.26ms diff --git a/backend/logs/scheduler/scheduler.log b/backend/logs/scheduler/scheduler.log index 781bad14..656611f8 100644 --- a/backend/logs/scheduler/scheduler.log +++ b/backend/logs/scheduler/scheduler.log @@ -11610,3 +11610,1510 @@ 2025-06-01 22:40:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) 2025-06-01 22:40:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten 2025-06-01 22:40:31 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:08:24 - [scheduler] scheduler - [INFO] INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-06-01 23:08:29 - [scheduler] scheduler - [INFO] INFO - Scheduler-Thread gestartet +2025-06-01 23:08:29 - [scheduler] scheduler - [INFO] INFO - Scheduler gestartet +2025-06-01 23:08:29 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:08:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:08:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:08:32 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:08:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:08:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:08:34 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:08:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:08:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:08:36 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:08:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:08:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:08:38 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:08:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:08:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:08:40 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:08:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:08:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:08:42 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:08:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:08:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:08:44 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:08:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:08:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:08:46 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:08:48 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:08:48 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:08:48 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:08:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:08:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:08:51 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:08:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:08:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:08:53 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:08:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:08:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:08:55 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:08:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:08:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:08:57 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:08:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:08:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:08:59 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:09:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:09:01 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:09:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:09:04 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:09:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:09:06 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:09:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:09:08 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:09:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:09:10 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:09:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:09:12 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:09:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:09:14 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:09:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:09:17 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:09:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:09:19 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:09:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:09:21 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:09:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:09:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:09:23 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:09:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:09:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:09:25 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:09:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:09:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:09:27 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:09:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:09:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:09:29 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:09:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:09:31 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:09:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:09:33 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:09:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:09:35 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:09:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:09:39 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:09:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:09:41 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:09:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:09:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:09:43 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:10:01 - [scheduler] scheduler - [INFO] INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-06-01 23:10:07 - [scheduler] scheduler - [INFO] INFO - Scheduler-Thread gestartet +2025-06-01 23:10:07 - [scheduler] scheduler - [INFO] INFO - Scheduler gestartet +2025-06-01 23:10:07 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:10:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:10:09 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:10:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:10:11 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:10:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:10:14 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:10:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:10:16 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:10:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:10:18 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:10:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:10:20 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:10:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:10:22 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:10:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:10:24 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:10:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:10:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:10:27 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:10:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:10:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:10:29 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:10:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:10:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:10:31 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:10:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:10:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:10:33 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:10:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:10:35 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:10:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:10:37 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:10:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:10:39 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:10:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:10:42 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:10:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:10:45 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:10:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:10:47 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:10:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:10:49 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:10:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:10:51 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:10:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:10:53 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:10:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:10:55 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:10:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:10:57 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:10:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:10:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:10:59 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:11:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:11:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:11:01 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:11:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:11:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:11:03 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:11:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:11:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:11:05 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:11:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:11:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:11:07 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:11:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:11:09 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:11:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:11:11 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:11:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:11:14 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:11:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:11:17 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:11:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:11:19 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:11:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:11:21 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:11:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:11:23 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:11:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:11:25 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:11:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:11:27 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:11:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:11:29 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:11:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:11:31 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:11:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:11:33 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:11:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:11:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:11:35 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:11:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:11:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:11:37 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:11:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:11:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:11:40 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:11:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:11:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:11:42 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:11:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:11:44 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:11:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:11:46 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:11:48 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:48 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:11:48 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:11:50 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:50 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:11:51 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:11:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:11:53 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:11:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:11:55 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:11:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:11:57 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:11:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:11:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:11:59 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:12:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:12:01 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:12:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:12:04 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:12:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:12:06 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:12:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:12:08 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:12:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:12:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:12:10 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:12:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:12:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:12:12 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:12:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:12:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:12:14 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:12:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:12:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:12:16 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:12:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:12:18 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:12:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:12:20 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:12:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:12:23 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:12:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:12:26 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:12:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:12:28 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:12:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:12:30 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:12:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:12:32 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:12:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:12:34 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:12:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:12:36 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:12:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:12:38 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:12:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:12:40 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:12:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:12:42 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:12:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:12:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:12:44 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:12:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:12:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:12:46 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:12:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:12:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:12:49 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:12:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:12:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:12:51 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:12:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:12:53 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:12:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:12:55 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:12:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:12:57 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:12:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:12:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:13:00 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:13:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:13:02 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:13:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:13:04 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:13:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:13:06 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:13:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:13:08 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:13:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:13:10 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:13:13 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:13 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:13:13 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:13:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:13:15 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:13:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:13:17 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:13:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:13:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:13:19 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:13:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:13:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:13:21 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:13:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:13:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:13:23 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:13:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:13:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:13:25 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:13:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:13:27 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:13:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:13:29 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:13:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:13:32 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:13:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:13:35 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:13:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:13:37 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:13:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:13:39 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:13:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:13:41 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:13:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:13:43 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:13:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:13:45 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:13:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:13:47 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:13:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:13:49 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:13:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:13:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:13:52 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:13:54 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:13:54 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:13:54 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:13:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:13:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:13:56 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:13:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:13:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:13:58 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:14:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:14:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:14:00 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:14:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:14:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:14:02 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:14:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:14:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:14:04 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:14:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:14:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:14:06 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:14:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:14:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:14:09 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:14:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:14:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:14:12 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:14:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:14:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:14:14 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:14:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:14:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:14:16 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:14:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:14:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:14:18 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:14:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:14:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:14:20 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:14:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:14:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:14:22 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:16:55 - [scheduler] scheduler - [INFO] INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-06-01 23:17:00 - [scheduler] scheduler - [INFO] INFO - Scheduler-Thread gestartet +2025-06-01 23:17:00 - [scheduler] scheduler - [INFO] INFO - Scheduler gestartet +2025-06-01 23:17:00 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:17:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:17:03 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:17:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:17:05 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:17:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:17:07 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:17:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:17:09 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:17:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:17:11 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:17:13 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:13 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:17:13 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:17:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:17:15 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:17:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:17:17 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:17:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:17:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:17:19 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:17:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:17:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:17:21 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:17:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:17:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:17:24 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:17:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:17:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:17:26 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:17:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:17:28 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:17:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:17:30 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:17:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:17:32 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:17:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:17:35 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:17:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:17:37 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:17:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:17:39 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:17:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:17:42 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:17:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:17:44 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:17:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:17:46 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:17:48 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:48 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:17:48 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:17:50 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:50 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:17:50 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:17:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:17:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:17:52 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:17:54 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:17:54 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:17:54 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:17:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:17:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:17:56 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:17:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:17:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:17:58 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:18:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:18:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:18:00 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:18:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:18:03 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:18:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:18:05 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:18:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:18:07 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:18:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:18:10 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:18:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:18:12 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:18:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:18:14 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:18:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:18:16 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:18:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:18:18 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:18:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:18:20 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:18:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:18:22 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:18:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:18:24 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:18:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:18:27 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:18:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:18:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:18:29 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:18:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:18:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:18:31 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:18:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:18:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:18:33 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:18:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:18:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:18:35 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:18:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:18:37 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:18:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:18:39 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:18:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:18:41 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:18:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:18:45 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:18:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:18:47 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:18:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:18:49 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:18:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:18:51 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:18:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:18:53 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:18:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:18:55 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:18:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:18:57 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:18:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:18:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:18:59 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:19:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:19:01 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:19:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:19:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:19:03 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:19:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:19:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:19:06 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:19:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:19:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:19:08 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:19:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:19:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:19:10 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:19:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:19:12 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:19:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:19:14 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:19:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:19:16 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:19:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:19:19 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:19:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:19:21 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:19:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:19:23 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:19:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:19:25 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:19:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:19:28 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:19:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:19:30 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:19:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:19:32 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:19:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:19:34 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:19:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:19:36 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:19:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:19:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:19:38 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:19:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:19:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:19:40 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:19:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:19:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:19:43 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:19:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:19:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:19:45 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:19:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:19:47 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:19:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:19:49 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:19:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:19:51 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:19:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:19:54 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:19:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:19:56 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:19:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:19:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:19:58 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:20:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:20:00 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:20:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:20:02 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:20:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:20:05 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:20:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:20:07 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:20:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:20:09 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:20:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:20:11 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:20:13 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:20:13 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:20:13 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:20:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:20:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:20:15 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:20:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:20:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:20:17 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:20:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:20:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:20:19 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:20:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:20:21 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:20:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:20:24 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:20:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:20:26 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:20:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:20:29 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:20:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:20:31 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:20:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:20:33 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:20:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:20:35 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:20:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:20:37 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:20:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:20:39 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:20:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:20:41 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:20:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:20:43 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:20:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:20:45 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:20:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:20:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:20:47 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:20:50 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:20:50 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:20:50 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:20:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:20:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:20:52 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:20:54 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:20:54 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:20:54 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:20:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:20:56 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:20:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:20:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:20:59 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:21:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:21:01 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:21:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:21:04 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:21:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:21:06 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:21:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:21:08 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:21:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:21:10 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:21:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:21:12 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:21:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:21:14 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:21:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:21:16 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:21:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:21:19 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:21:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:21:21 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:21:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:21:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:21:23 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:21:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:21:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:21:25 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:21:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:21:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:21:27 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:21:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:21:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:21:29 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:21:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:21:31 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:21:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:21:33 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:21:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:21:35 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:21:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:21:38 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:21:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:21:40 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:21:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:21:43 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:21:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:21:45 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:21:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:21:47 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:21:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:21:49 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:21:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:21:51 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:21:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:21:53 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:21:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:21:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:21:55 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:21:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:21:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:21:58 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:22:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:22:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:22:00 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:22:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:22:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:22:02 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:22:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:22:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:22:04 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:22:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:22:06 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:22:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:22:08 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:22:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:22:10 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:22:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:22:13 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:22:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:22:16 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:22:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:22:18 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:22:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:22:20 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:22:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:22:22 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:22:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:22:24 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:22:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:22:26 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:22:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:22:28 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:22:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:22:30 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:22:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:22:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:22:33 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:22:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:22:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:22:35 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:22:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:22:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:22:37 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:22:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:22:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:22:39 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:22:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:22:41 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:22:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:22:43 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:22:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:22:45 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:22:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:22:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:22:48 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:32:06 - [scheduler] scheduler - [INFO] INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-06-01 23:32:12 - [scheduler] scheduler - [INFO] INFO - Scheduler-Thread gestartet +2025-06-01 23:32:12 - [scheduler] scheduler - [INFO] INFO - Scheduler gestartet +2025-06-01 23:32:12 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:32:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:32:14 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:32:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:32:16 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:32:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:32:19 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:32:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:32:21 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:32:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:32:23 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:32:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:32:25 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:32:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:32:27 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:32:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:32:29 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:32:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:32:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:32:31 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:32:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:32:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:32:33 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:32:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:32:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:32:36 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:32:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:32:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:32:38 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:32:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:32:40 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:32:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:32:42 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:32:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:32:44 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:32:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:32:47 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:32:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:32:49 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:32:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:32:51 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:32:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:32:53 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:32:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:32:55 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:32:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:32:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:32:58 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:33:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:33:00 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:33:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:33:02 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:33:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:33:04 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:33:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:33:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:33:06 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:33:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:33:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:33:08 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:33:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:33:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:33:10 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:33:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:33:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:33:12 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:33:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:33:15 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:33:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:33:17 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:33:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:33:19 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:33:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:33:22 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:33:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:33:24 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:33:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:33:26 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:33:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:33:28 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:33:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:33:30 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:33:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:33:32 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:33:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:33:35 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:33:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:33:37 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:33:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:33:39 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:33:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:33:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:33:41 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:33:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:33:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:33:43 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:33:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:33:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:33:45 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:33:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:33:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:33:47 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:33:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:33:49 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:33:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:33:51 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:33:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:33:53 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:33:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:33:57 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:33:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:33:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:33:59 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:34:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:34:01 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:34:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:34:03 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:34:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:34:05 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:34:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:34:07 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:34:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:34:09 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:34:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:34:11 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:34:13 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:13 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:34:13 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:34:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:34:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:34:15 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:34:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:34:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:34:18 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:34:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:34:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:34:20 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:34:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:34:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:34:22 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:34:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:34:24 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:34:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:34:26 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:34:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:34:28 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:34:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:34:31 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:34:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:34:33 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:34:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:34:36 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:34:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:34:38 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:34:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:34:40 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:34:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:34:42 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:34:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:34:44 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:34:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:34:46 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:34:48 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:48 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:34:48 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:34:50 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:34:50 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:34:50 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:34:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:34:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:34:52 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:34:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:34:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:34:55 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:34:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:34:57 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:34:57 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:34:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:34:59 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:34:59 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:35:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:35:01 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:35:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:35:03 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:35:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:35:06 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:35:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:35:08 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:35:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:35:10 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:35:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:35:12 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:35:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:35:14 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:35:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:35:16 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:35:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:35:18 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:35:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:21 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:35:21 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:35:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:23 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:35:23 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:35:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:35:25 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:35:25 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:35:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:35:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:35:27 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:35:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:35:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:35:29 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:35:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:35:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:35:31 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:35:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:35:33 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:35:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:35:35 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:35:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:35:38 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:35:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:35:41 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:35:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:35:43 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:35:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:35:45 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:35:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:35:47 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:35:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:35:49 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:35:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:35:51 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:35:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:35:53 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:35:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:55 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:35:55 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:35:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:35:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:35:58 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:36:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:36:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:36:00 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:36:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:36:02 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:36:02 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:36:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:36:04 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:36:04 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:36:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:36:06 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:36:06 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:36:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:08 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:36:08 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:36:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:36:10 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:36:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:36:12 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:36:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:36:15 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:36:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:36:18 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:36:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:36:20 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:36:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:36:22 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:36:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:36:24 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:36:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:36:26 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:36:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:36:28 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:36:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:36:30 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:36:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:32 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:36:32 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:36:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:36:34 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:36:34 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:36:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:36:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:36:36 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:36:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:36:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:36:38 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:36:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:36:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:36:40 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:36:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:43 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:36:43 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:36:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:45 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:36:45 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:36:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:36:47 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:36:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:36:50 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:36:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:36:52 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:36:54 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:54 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:36:54 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:36:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:36:56 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:36:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:36:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:36:58 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:37:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:00 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:37:00 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:37:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:37:03 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:37:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:37:05 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:37:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:37:07 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:37:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:37:09 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:37:09 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:37:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:37:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:37:11 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:37:13 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:37:13 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:37:13 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:37:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:37:15 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:37:15 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:37:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:17 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:37:17 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:37:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:19 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:37:19 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:37:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:37:22 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:37:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:37:25 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:37:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:27 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:37:27 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:37:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:29 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:37:29 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:37:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:31 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:37:31 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:37:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:37:33 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:37:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:35 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:37:35 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:37:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:37 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:37:37 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:37:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:39 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:37:39 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:37:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:41 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:37:41 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:37:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:37:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:37:44 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:37:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:37:46 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:37:46 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:37:48 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:37:48 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:37:48 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:37:50 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:37:50 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:37:50 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:37:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:52 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:37:52 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:37:54 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:54 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:37:54 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:37:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:56 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:37:56 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:37:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:37:58 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:37:59 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:38:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:01 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:38:01 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:38:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:03 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:38:03 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:38:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:05 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:38:05 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:38:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:07 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:38:07 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:38:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:10 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:38:10 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:38:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:12 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:38:12 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:38:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:14 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:38:14 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:38:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:16 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:38:16 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:38:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:38:18 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:38:18 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi +2025-06-01 23:38:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:38:20 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 10 nicht einschalten +2025-06-01 23:38:20 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 11: fee +2025-06-01 23:38:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:38:22 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 11 nicht einschalten +2025-06-01 23:38:22 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 12: fee +2025-06-01 23:38:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:38:24 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 12 nicht einschalten +2025-06-01 23:38:24 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 13: e2 +2025-06-01 23:38:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:26 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 13 nicht einschalten +2025-06-01 23:38:26 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 14: e2 +2025-06-01 23:38:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:28 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten +2025-06-01 23:38:28 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test +2025-06-01 23:38:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:30 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 15 nicht einschalten +2025-06-01 23:38:30 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 16: test +2025-06-01 23:38:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:33 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 16 nicht einschalten +2025-06-01 23:38:34 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 7: test +2025-06-01 23:38:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:36 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 7 nicht einschalten +2025-06-01 23:38:36 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 8: test +2025-06-01 23:38:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:38 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 8 nicht einschalten +2025-06-01 23:38:38 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 1: test +2025-06-01 23:38:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:40 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 1 nicht einschalten +2025-06-01 23:38:40 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 2: test +2025-06-01 23:38:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:42 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 2 nicht einschalten +2025-06-01 23:38:42 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 3: test +2025-06-01 23:38:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:44 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 3 nicht einschalten +2025-06-01 23:38:44 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 4: test +2025-06-01 23:38:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:47 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 4 nicht einschalten +2025-06-01 23:38:47 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 5: test +2025-06-01 23:38:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:49 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 5 nicht einschalten +2025-06-01 23:38:49 - [scheduler] scheduler - [INFO] INFO - 🚀 Starte geplanten Job 6: test +2025-06-01 23:38:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.103 timed out. (connect timeout=2)')) +2025-06-01 23:38:51 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Job 6 nicht einschalten +2025-06-01 23:38:51 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 9: zi +2025-06-01 23:38:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.104: HTTPConnectionPool(host='192.168.0.104', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(, 'Connection to 192.168.0.104 timed out. (connect timeout=2)')) +2025-06-01 23:38:53 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 9 nicht einschalten +2025-06-01 23:38:53 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 10: zi diff --git a/backend/logs/security/security.log b/backend/logs/security/security.log index 1d16b707..a8607118 100644 --- a/backend/logs/security/security.log +++ b/backend/logs/security/security.log @@ -86,3 +86,7 @@ 2025-06-01 22:39:55 - [security] security - [INFO] INFO - 🔒 Security System initialisiert 2025-06-01 22:39:57 - [security] security - [INFO] INFO - 🔒 Security System initialisiert 2025-06-01 22:40:10 - [security] security - [INFO] INFO - 🔒 Security System initialisiert +2025-06-01 23:08:25 - [security] security - [INFO] INFO - 🔒 Security System initialisiert +2025-06-01 23:10:02 - [security] security - [INFO] INFO - 🔒 Security System initialisiert +2025-06-01 23:16:56 - [security] security - [INFO] INFO - 🔒 Security System initialisiert +2025-06-01 23:32:07 - [security] security - [INFO] INFO - 🔒 Security System initialisiert diff --git a/backend/logs/shutdown_manager/shutdown_manager.log b/backend/logs/shutdown_manager/shutdown_manager.log index e34a77a8..355fc043 100644 --- a/backend/logs/shutdown_manager/shutdown_manager.log +++ b/backend/logs/shutdown_manager/shutdown_manager.log @@ -172,3 +172,7 @@ 2025-06-01 22:39:55 - [shutdown_manager] shutdown_manager - [INFO] INFO - 🏁 System wird beendet... 2025-06-01 22:39:57 - [shutdown_manager] shutdown_manager - [INFO] INFO - 🔧 Shutdown-Manager initialisiert 2025-06-01 22:40:10 - [shutdown_manager] shutdown_manager - [INFO] INFO - 🔧 Shutdown-Manager initialisiert +2025-06-01 23:08:25 - [shutdown_manager] shutdown_manager - [INFO] INFO - 🔧 Shutdown-Manager initialisiert +2025-06-01 23:10:02 - [shutdown_manager] shutdown_manager - [INFO] INFO - 🔧 Shutdown-Manager initialisiert +2025-06-01 23:16:56 - [shutdown_manager] shutdown_manager - [INFO] INFO - 🔧 Shutdown-Manager initialisiert +2025-06-01 23:32:07 - [shutdown_manager] shutdown_manager - [INFO] INFO - 🔧 Shutdown-Manager initialisiert diff --git a/backend/logs/startup/startup.log b/backend/logs/startup/startup.log index d31d6210..a628461a 100644 --- a/backend/logs/startup/startup.log +++ b/backend/logs/startup/startup.log @@ -782,3 +782,39 @@ 2025-06-01 22:40:10 - [startup] startup - [INFO] INFO - 🪟 Windows-Modus: Aktiviert 2025-06-01 22:40:10 - [startup] startup - [INFO] INFO - 🔒 Windows-sichere Log-Rotation: Aktiviert 2025-06-01 22:40:10 - [startup] startup - [INFO] INFO - ================================================== +2025-06-01 23:08:25 - [startup] startup - [INFO] INFO - ================================================== +2025-06-01 23:08:25 - [startup] startup - [INFO] INFO - 🚀 MYP Platform Backend wird gestartet... +2025-06-01 23:08:25 - [startup] startup - [INFO] INFO - 🐍 Python Version: 3.13.3 (tags/v3.13.3:6280bb5, Apr 8 2025, 14:47:33) [MSC v.1943 64 bit (AMD64)] +2025-06-01 23:08:25 - [startup] startup - [INFO] INFO - 💻 Betriebssystem: nt (win32) +2025-06-01 23:08:25 - [startup] startup - [INFO] INFO - 📁 Arbeitsverzeichnis: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend +2025-06-01 23:08:25 - [startup] startup - [INFO] INFO - ⏰ Startzeit: 2025-06-01T23:08:25.318047 +2025-06-01 23:08:25 - [startup] startup - [INFO] INFO - 🪟 Windows-Modus: Aktiviert +2025-06-01 23:08:25 - [startup] startup - [INFO] INFO - 🔒 Windows-sichere Log-Rotation: Aktiviert +2025-06-01 23:08:25 - [startup] startup - [INFO] INFO - ================================================== +2025-06-01 23:10:02 - [startup] startup - [INFO] INFO - ================================================== +2025-06-01 23:10:02 - [startup] startup - [INFO] INFO - 🚀 MYP Platform Backend wird gestartet... +2025-06-01 23:10:02 - [startup] startup - [INFO] INFO - 🐍 Python Version: 3.13.3 (tags/v3.13.3:6280bb5, Apr 8 2025, 14:47:33) [MSC v.1943 64 bit (AMD64)] +2025-06-01 23:10:02 - [startup] startup - [INFO] INFO - 💻 Betriebssystem: nt (win32) +2025-06-01 23:10:02 - [startup] startup - [INFO] INFO - 📁 Arbeitsverzeichnis: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend +2025-06-01 23:10:02 - [startup] startup - [INFO] INFO - ⏰ Startzeit: 2025-06-01T23:10:02.765896 +2025-06-01 23:10:02 - [startup] startup - [INFO] INFO - 🪟 Windows-Modus: Aktiviert +2025-06-01 23:10:02 - [startup] startup - [INFO] INFO - 🔒 Windows-sichere Log-Rotation: Aktiviert +2025-06-01 23:10:02 - [startup] startup - [INFO] INFO - ================================================== +2025-06-01 23:16:56 - [startup] startup - [INFO] INFO - ================================================== +2025-06-01 23:16:56 - [startup] startup - [INFO] INFO - 🚀 MYP Platform Backend wird gestartet... +2025-06-01 23:16:56 - [startup] startup - [INFO] INFO - 🐍 Python Version: 3.13.3 (tags/v3.13.3:6280bb5, Apr 8 2025, 14:47:33) [MSC v.1943 64 bit (AMD64)] +2025-06-01 23:16:56 - [startup] startup - [INFO] INFO - 💻 Betriebssystem: nt (win32) +2025-06-01 23:16:56 - [startup] startup - [INFO] INFO - 📁 Arbeitsverzeichnis: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend +2025-06-01 23:16:56 - [startup] startup - [INFO] INFO - ⏰ Startzeit: 2025-06-01T23:16:56.191249 +2025-06-01 23:16:56 - [startup] startup - [INFO] INFO - 🪟 Windows-Modus: Aktiviert +2025-06-01 23:16:56 - [startup] startup - [INFO] INFO - 🔒 Windows-sichere Log-Rotation: Aktiviert +2025-06-01 23:16:56 - [startup] startup - [INFO] INFO - ================================================== +2025-06-01 23:32:07 - [startup] startup - [INFO] INFO - ================================================== +2025-06-01 23:32:07 - [startup] startup - [INFO] INFO - 🚀 MYP Platform Backend wird gestartet... +2025-06-01 23:32:07 - [startup] startup - [INFO] INFO - 🐍 Python Version: 3.13.3 (tags/v3.13.3:6280bb5, Apr 8 2025, 14:47:33) [MSC v.1943 64 bit (AMD64)] +2025-06-01 23:32:07 - [startup] startup - [INFO] INFO - 💻 Betriebssystem: nt (win32) +2025-06-01 23:32:07 - [startup] startup - [INFO] INFO - 📁 Arbeitsverzeichnis: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend +2025-06-01 23:32:07 - [startup] startup - [INFO] INFO - ⏰ Startzeit: 2025-06-01T23:32:07.643798 +2025-06-01 23:32:07 - [startup] startup - [INFO] INFO - 🪟 Windows-Modus: Aktiviert +2025-06-01 23:32:07 - [startup] startup - [INFO] INFO - 🔒 Windows-sichere Log-Rotation: Aktiviert +2025-06-01 23:32:07 - [startup] startup - [INFO] INFO - ================================================== diff --git a/backend/logs/windows_fixes/windows_fixes.log b/backend/logs/windows_fixes/windows_fixes.log index 9a0b342a..d1846c96 100644 --- a/backend/logs/windows_fixes/windows_fixes.log +++ b/backend/logs/windows_fixes/windows_fixes.log @@ -375,3 +375,19 @@ 2025-06-01 22:40:09 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen) 2025-06-01 22:40:09 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Globaler subprocess-Patch angewendet 2025-06-01 22:40:09 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Alle Windows-Fixes erfolgreich angewendet +2025-06-01 23:08:24 - [windows_fixes] windows_fixes - [INFO] INFO - 🔧 Wende Windows-spezifische Fixes an... +2025-06-01 23:08:24 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen) +2025-06-01 23:08:24 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Globaler subprocess-Patch angewendet +2025-06-01 23:08:24 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Alle Windows-Fixes erfolgreich angewendet +2025-06-01 23:10:01 - [windows_fixes] windows_fixes - [INFO] INFO - 🔧 Wende Windows-spezifische Fixes an... +2025-06-01 23:10:01 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen) +2025-06-01 23:10:01 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Globaler subprocess-Patch angewendet +2025-06-01 23:10:01 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Alle Windows-Fixes erfolgreich angewendet +2025-06-01 23:16:55 - [windows_fixes] windows_fixes - [INFO] INFO - 🔧 Wende Windows-spezifische Fixes an... +2025-06-01 23:16:55 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen) +2025-06-01 23:16:55 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Globaler subprocess-Patch angewendet +2025-06-01 23:16:55 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Alle Windows-Fixes erfolgreich angewendet +2025-06-01 23:32:05 - [windows_fixes] windows_fixes - [INFO] INFO - 🔧 Wende Windows-spezifische Fixes an... +2025-06-01 23:32:05 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen) +2025-06-01 23:32:05 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Globaler subprocess-Patch angewendet +2025-06-01 23:32:05 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Alle Windows-Fixes erfolgreich angewendet diff --git a/backend/performance-report.txt b/backend/performance-report.txt new file mode 100644 index 00000000..d0b09c72 --- /dev/null +++ b/backend/performance-report.txt @@ -0,0 +1,148 @@ + +MYP Platform - Performance Optimization Report +============================================== + +CSS-Dateien (39): + - all.css: 106,394 Bytes + - all.min.css: 73,890 Bytes + - app-bundle.min.css: 8,714 Bytes + - brands.css: 19,852 Bytes + - brands.min.css: 14,574 Bytes + - caching-optimizations.css: 5,613 Bytes + - caching-optimizations.min.css: 3,565 Bytes + - components.css: 17,232 Bytes + - components.min.css: 13,062 Bytes + - critical-inline.css: 1,473 Bytes + - critical-inline.min.css: 1,399 Bytes + - fontawesome.css: 83,677 Bytes + - fontawesome.min.css: 56,777 Bytes + - glassmorphism.css: 6,211 Bytes + - glassmorphism.min.css: 4,715 Bytes + - input.css: 99,414 Bytes + - input.min.css: 76,568 Bytes + - main.min.css: 115 Bytes + - optimization-animations.css: 2,675 Bytes + - optimization-animations.min.css: 1,443 Bytes + - output.css: 248,934 Bytes + - output.min.css: 210,598 Bytes + - printers.css: 3,952 Bytes + - printers.min.css: 2,502 Bytes + - professional-theme.css: 24,631 Bytes + - professional-theme.min.css: 18,762 Bytes + - regular.css: 633 Bytes + - regular.min.css: 580 Bytes + - solid.css: 625 Bytes + - solid.min.css: 572 Bytes + - svg-with-js.css: 12,554 Bytes + - svg-with-js.min.css: 10,197 Bytes + - tailwind.min.css: 221,943 Bytes + - v4-font-face.css: 1,831 Bytes + - v4-font-face.min.css: 1,736 Bytes + - v4-shims.css: 38,514 Bytes + - v4-shims.min.css: 21,211 Bytes + - v5-font-face.css: 871 Bytes + - v5-font-face.min.css: 794 Bytes + +JavaScript-Dateien (84): + - admin-guest-requests.js: 32,046 Bytes + - admin-guest-requests.min.js: 23,162 Bytes + - admin-panel.js: 42,961 Bytes + - admin-panel.min.js: 27,749 Bytes + - admin-unified.js: 56,717 Bytes + - admin-unified.min.js: 35,087 Bytes + - advanced-components.js: 29,859 Bytes + - advanced-components.min.js: 16,393 Bytes + - all.js: 1,627,440 Bytes + - all.min.js: 1,530,755 Bytes + - apexcharts.min.js: 524,387 Bytes + - auto-logout.js: 4,661 Bytes + - auto-logout.min.js: 3,099 Bytes + - brands.js: 510,493 Bytes + - brands.min.js: 499,125 Bytes + - chart-adapter.js: 8,486 Bytes + - chart-config.js: 9,767 Bytes + - chart-renderer.js: 10,396 Bytes + - chart.min.js: 181,743 Bytes + - charts.js: 13,704 Bytes + - charts.min.js: 7,671 Bytes + - conflict-detection.js: 38,929 Bytes + - conflict-detection.min.js: 15,853 Bytes + - core.min.js: 181,411 Bytes + - countdown-timer.js: 35,228 Bytes + - countdown-timer.min.js: 19,014 Bytes + - csp-violation-handler.js: 10,509 Bytes + - csp-violation-handler.min.js: 6,622 Bytes + - css-cache-manager.js: 3,454 Bytes + - css-cache-manager.min.js: 2,471 Bytes + - css-cache-service-worker.js: 10,648 Bytes + - css-cache-service-worker.min.js: 6,895 Bytes + - dark-mode-fix.js: 7,630 Bytes + - dark-mode-fix.min.js: 4,183 Bytes + - dark-mode.js: 11,717 Bytes + - dark-mode.min.js: 7,659 Bytes + - dashboard.js: 11,344 Bytes + - dashboard.min.js: 8,164 Bytes + - daygrid.min.js: 26,955 Bytes + - debug-fix.js: 7,419 Bytes + - debug-fix.min.js: 4,146 Bytes + - event-handlers.js: 16,020 Bytes + - event-handlers.min.js: 8,399 Bytes + - fontawesome.js: 106,548 Bytes + - fontawesome.min.js: 49,856 Bytes + - glassmorphism-notifications.js: 62,643 Bytes + - glassmorphism-notifications.min.js: 35,476 Bytes + - global-refresh-functions.js: 26,547 Bytes + - global-refresh-functions.min.js: 14,785 Bytes + - interaction.min.js: 35,636 Bytes + - job-manager.js: 31,172 Bytes + - job-manager.min.js: 16,598 Bytes + - jobs-safety-fix.js: 10,729 Bytes + - jobs-safety-fix.min.js: 5,270 Bytes + - list.min.js: 9,361 Bytes + - notifications.js: 26,568 Bytes + - notifications.min.js: 16,192 Bytes + - offline-app.js: 20,435 Bytes + - offline-app.min.js: 10,758 Bytes + - optimization-features.js: 33,307 Bytes + - optimization-features.min.js: 19,873 Bytes + - performance-service-worker.js: 10,657 Bytes + - performance-service-worker.min.js: 7,730 Bytes + - printer_monitor.js: 15,887 Bytes + - printer_monitor.min.js: 7,574 Bytes + - regular.js: 126,991 Bytes + - regular.min.js: 119,408 Bytes + - service-worker.js: 2,205 Bytes + - service-worker.min.js: 1,361 Bytes + - session-manager.js: 19,582 Bytes + - session-manager.min.js: 10,369 Bytes + - solid.js: 884,065 Bytes + - solid.min.js: 863,023 Bytes + - sw.js: 12,735 Bytes + - sw.min.js: 7,745 Bytes + - timegrid.min.js: 31,540 Bytes + - ui-components.js: 711 Bytes + - ui-components.min.js: 305 Bytes + - user-dropdown.js: 1 Bytes + - user-dropdown.min.js: 0 Bytes + - v4-shims.js: 35,459 Bytes + - v4-shims.min.js: 28,077 Bytes + - validation-fix.js: 9,064 Bytes + - validation-fix.min.js: 5,182 Bytes + +Bilder (0): + + +Optimierungen durchgeführt: +✅ CSS-Minifizierung +✅ JavaScript-Minifizierung +✅ Gzip-Komprimierung +✅ WebP-Bildkonvertierung +✅ Critical CSS +✅ Service Worker +✅ PWA-Manifest + +Empfohlene nächste Schritte: +1. Performance-optimiertes Template verwenden +2. Service Worker aktivieren +3. HTTP/2 Push für kritische Assets +4. CDN für statische Assets konfigurieren diff --git a/backend/static/css/app-bundle.min.css.gz b/backend/static/css/app-bundle.min.css.gz new file mode 100644 index 00000000..75139b11 Binary files /dev/null and b/backend/static/css/app-bundle.min.css.gz differ diff --git a/backend/static/css/build/critical.css b/backend/static/css/build/critical.css new file mode 100644 index 00000000..74c190ef --- /dev/null +++ b/backend/static/css/build/critical.css @@ -0,0 +1,31 @@ +/* CRITICAL INLINE CSS - Sollte im HTML stehen */ +/* Nur die absolut notwendigen Styles für First Paint */ + +:root{--primary:#0073ce;--bg:#fafbfc;--surface:#fff;--text:#111827;--border:#e5e7eb;--shadow:0 2px 4px rgba(0,0,0,.05)} +*{box-sizing:border-box;margin:0;padding:0;contain:layout style} +body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.5;text-rendering:optimizeSpeed;-webkit-font-smoothing:antialiased} +.header{background:var(--surface);border-bottom:1px solid var(--border);padding:1rem;position:sticky;top:0;z-index:1000} +.nav{display:flex;gap:1rem} +.nav-item{padding:.5rem 1rem;border-radius:6px;text-decoration:none;color:#6b7280;transition:background .1s} +.nav-item:hover{background:var(--bg);color:var(--text)} +.nav-item.active{background:var(--primary);color:#fff} +.container{max-width:1200px;margin:0 auto;padding:0 1rem} +.btn{background:var(--primary);color:#fff;border:none;border-radius:6px;padding:.75rem 1.5rem;font-weight:600;cursor:pointer;transition:background .1s} +.btn:hover{background:#005a9f} +.card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1rem;box-shadow:var(--shadow)} +.flex{display:flex} +.grid{display:grid;gap:1rem} +.hidden{display:none} +.w-full{width:100%} +.text-center{text-align:center} +.p-4{padding:1rem} +.mb-4{margin-bottom:1rem} +.status{display:inline-block;padding:.25rem .75rem;border-radius:999px;font-size:.75rem;font-weight:600;text-transform:uppercase} +.status-online{background:#d1fae5;color:#065f46} +.status-offline{background:#fee2e2;color:#991b1b} +.status-printing{background:#dbeafe;color:#1e40af} +.input{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:.75rem;width:100%} +.input:focus{outline:none;border-color:var(--primary)} +@media (prefers-color-scheme:dark){:root{--bg:#1e293b;--surface:#334155;--text:#f8fafc;--border:#475569;--shadow:0 2px 4px rgba(0,0,0,.3)}} +@media (max-width:768px){.nav{flex-direction:column;gap:.5rem}} +@media (prefers-reduced-motion:reduce){*{transition:none!important}} \ No newline at end of file diff --git a/backend/static/css/build/kiosk-1656af86.css b/backend/static/css/build/kiosk-1656af86.css new file mode 100644 index 00000000..ed3463f3 --- /dev/null +++ b/backend/static/css/build/kiosk-1656af86.css @@ -0,0 +1,498 @@ +/* MYP Platform - Kiosk Optimierte CSS Bundle */ +/* Generiert am: 2025-06-01 23:40:46 */ + +/** + * MYP Platform - Kiosk-Optimierte CSS + * Maximale Performance für Offline-Kiosk-Umgebung + * Keine Touch-Events, minimale Animationen, optimierte Rendering-Performance + */ + +/* ===== CRITICAL INLINE STYLES (sollten im HTML-Head stehen) ===== */ +:root { + /* Reduzierte Farbvariablen für bessere Performance */ + --primary: #0073ce; + --primary-dark: #005a9f; + --bg: #fafbfc; + --surface: #ffffff; + --text: #111827; + --text-muted: #6b7280; + --border: #e5e7eb; + --shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +/* CSS Containment für bessere Performance */ +* { + contain: layout style; +} + +/* Basis-Reset ohne aufwendige Normalisierung */ +* { box-sizing: border-box; margin: 0; padding: 0; } +body { + font-family: system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; + contain: layout style paint; + /* Optimiert für Kiosk-Rendering */ + text-rendering: optimizeSpeed; + -webkit-font-smoothing: antialiased; +} + +/* ===== LAYOUT OPTIMIERUNGEN ===== */ +.header { + background: var(--surface); + border-bottom: 1px solid var(--border); + padding: 1rem; + contain: layout style; +} + +.nav { + display: flex; + gap: 1rem; + contain: layout; +} + +.nav-item { + padding: 0.5rem 1rem; + border-radius: 6px; + text-decoration: none; + color: var(--text-muted); + transition: background-color 0.1s ease; /* Minimale Transition */ +} + +.nav-item:hover { + background: var(--bg); + color: var(--text); +} + +.nav-item.active { + background: var(--primary); + color: white; +} + +/* ===== OPTIMIERTE KARTEN ===== */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + box-shadow: var(--shadow); + contain: layout style paint; +} + +.card:hover { + transform: translateY(-1px); /* Minimaler Hover-Effekt */ +} + +/* ===== BUTTONS OHNE KOMPLEXE ANIMATIONEN ===== */ +.btn { + background: var(--primary); + color: white; + border: none; + border-radius: 6px; + padding: 0.75rem 1.5rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.1s ease; + contain: layout style; +} + +.btn:hover { + background: var(--primary-dark); +} + +.btn-secondary { + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--bg); +} + +/* ===== INPUTS OPTIMIERT ===== */ +.input { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.75rem; + width: 100%; + transition: border-color 0.1s ease; + contain: layout style; +} + +.input:focus { + outline: none; + border-color: var(--primary); +} + +/* ===== TABELLEN OHNE KOMPLEXE EFFEKTE ===== */ +.table { + width: 100%; + border-collapse: collapse; + background: var(--surface); + border-radius: 8px; + overflow: hidden; + contain: layout; +} + +.table th { + background: var(--bg); + padding: 1rem; + text-align: left; + font-weight: 600; + border-bottom: 1px solid var(--border); +} + +.table td { + padding: 1rem; + border-bottom: 1px solid var(--border); +} + +.table tr:hover { + background: var(--bg); +} + +/* ===== STATUS BADGES VEREINFACHT ===== */ +.status { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.status-online { background: #d1fae5; color: #065f46; } +.status-offline { background: #fee2e2; color: #991b1b; } +.status-printing { background: #dbeafe; color: #1e40af; } + +/* ===== GRID LAYOUTS OPTIMIERT ===== */ +.grid { + display: grid; + gap: 1rem; + contain: layout; +} + +.grid-2 { grid-template-columns: repeat(2, 1fr); } +.grid-3 { grid-template-columns: repeat(3, 1fr); } +.grid-4 { grid-template-columns: repeat(4, 1fr); } + +/* ===== UTILITIES MINIMAL ===== */ +.flex { display: flex; contain: layout; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-1 { gap: 0.25rem; } +.gap-2 { gap: 0.5rem; } +.gap-4 { gap: 1rem; } + +.p-1 { padding: 0.25rem; } +.p-2 { padding: 0.5rem; } +.p-4 { padding: 1rem; } +.p-6 { padding: 1.5rem; } + +.m-1 { margin: 0.25rem; } +.m-2 { margin: 0.5rem; } +.m-4 { margin: 1rem; } + +.mb-2 { margin-bottom: 0.5rem; } +.mb-4 { margin-bottom: 1rem; } +.mb-6 { margin-bottom: 1.5rem; } + +.text-sm { font-size: 0.875rem; } +.text-lg { font-size: 1.125rem; } +.text-xl { font-size: 1.25rem; } +.text-2xl { font-size: 1.5rem; } + +.font-semibold { font-weight: 600; } +.font-bold { font-weight: 700; } + +.text-center { text-align: center; } +.text-right { text-align: right; } + +.w-full { width: 100%; } +.h-full { height: 100%; } + +.rounded { border-radius: 6px; } +.rounded-lg { border-radius: 8px; } + +.border { border: 1px solid var(--border); } +.border-t { border-top: 1px solid var(--border); } +.border-b { border-bottom: 1px solid var(--border); } + +.bg-white { background: var(--surface); } +.bg-gray-50 { background: var(--bg); } + +.text-gray-600 { color: var(--text-muted); } +.text-blue-600 { color: var(--primary); } + +/* ===== DARK MODE MINIMAL ===== */ +@media (prefers-color-scheme: dark) { + :root { + --bg: #1e293b; + --surface: #334155; + --text: #f8fafc; + --text-muted: #94a3b8; + --border: #475569; + --shadow: 0 2px 4px rgba(0,0,0,0.3); + } +} + +/* ===== RESPONSIVE FÜR KIOSK-DISPLAYS ===== */ +@media (max-width: 1024px) { + .grid-4 { grid-template-columns: repeat(2, 1fr); } + .grid-3 { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 768px) { + .grid-4, + .grid-3, + .grid-2 { grid-template-columns: 1fr; } + + .nav { + flex-direction: column; + gap: 0.5rem; + } +} + +/* ===== PERFORMANCE OPTIMIERUNGEN ===== */ +/* Deaktivierung nicht benötigter Features für Kiosk */ +* { + /* Keine Touch-Events */ + touch-action: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; +} + +/* Nur notwendige Elemente selektierbar */ +input, +textarea { + -webkit-user-select: text; + user-select: text; + touch-action: manipulation; +} + +/* Optimierte Scrolling-Performance */ +* { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; +} + +/* GPU-Acceleration nur wo nötig */ +.card:hover, +.btn:hover { + will-change: transform; +} + +/* Minimale Animationen für bessere Performance */ +@media (prefers-reduced-motion: reduce) { + * { + transition: none !important; + animation: none !important; + } +} + +/* Print-Optimierung (falls Kiosk drucken kann) */ +@media print { + .nav, + .btn { + display: none; + } + + .card { + box-shadow: none; + border: 1px solid #000; + } +} + +/* ===== LAYOUT SHIFT PREVENTION ===== */ +img { + height: auto; + max-width: 100%; + vertical-align: middle; +} + +/* Container für stabile Layouts */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + contain: layout; +} + +/* ===== KIOSK-SPEZIFISCHE OPTIMIERUNGEN ===== */ +/* Vollbildmodus-Unterstützung */ +html, +body { + height: 100%; + overflow-x: hidden; +} + +/* Fokus-Management für Keyboard-Navigation */ +:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Kein Selection-Highlighting */ +::selection { + background: transparent; +} + +/* Optimierte Font-Loading */ +@font-face { + font-family: 'system-ui'; + font-display: swap; +} + +/** + * Minimales Icon-Set für Kiosk-Modus + * SVG-basierte Icons als CSS-Pseudo-Elemente für beste Performance + * Ersetzt FontAwesome für deutlich kleinere Bundle-Größe + */ + +/* Icon-Basis-Klasse */ +.icon { + display: inline-block; + width: 1rem; + height: 1rem; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + vertical-align: middle; +} + +.icon-lg { width: 1.5rem; height: 1.5rem; } +.icon-xl { width: 2rem; height: 2rem; } + +/* ===== DRUCKER ICONS ===== */ +.icon-printer { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z'/%3E%3C/svg%3E"); +} + +.icon-print { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z'/%3E%3C/svg%3E"); +} + +/* ===== STATUS ICONS ===== */ +.icon-check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E"); +} + +.icon-warning { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b'%3E%3Cpath d='M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z'/%3E%3C/svg%3E"); +} + +.icon-error { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ef4444'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/%3E%3C/svg%3E"); +} + +.icon-offline { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%236b7280'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/%3E%3C/svg%3E"); +} + +/* ===== NAVIGATION ICONS ===== */ +.icon-home { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/%3E%3C/svg%3E"); +} + +.icon-settings { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z'/%3E%3C/svg%3E"); +} + +.icon-users { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A3.012 3.012 0 0 0 16.43 6c-.68 0-1.3.27-1.77.72L12 8.5l-2.66-1.78C8.87 6.27 8.25 6 7.57 6c-1.31 0-2.42.83-2.83 2L2.5 16H5v6h2v-6h2v6h2zm-6.5-10.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5S12 9.17 12 10s.67 1.5 1.5 1.5z'/%3E%3C/svg%3E"); +} + +.icon-dashboard { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z'/%3E%3C/svg%3E"); +} + +/* ===== ACTION ICONS ===== */ +.icon-plus { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'/%3E%3C/svg%3E"); +} + +.icon-edit { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E"); +} + +.icon-delete { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ef4444'%3E%3Cpath d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'/%3E%3C/svg%3E"); +} + +.icon-refresh { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z'/%3E%3C/svg%3E"); +} + +/* ===== POWER/CONNECTION ICONS ===== */ +.icon-power { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z'/%3E%3C/svg%3E"); +} + +.icon-wifi { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z'/%3E%3C/svg%3E"); +} + +/* ===== DOKUMENT ICONS ===== */ +.icon-file { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z'/%3E%3C/svg%3E"); +} + +.icon-queue { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 9H9V9h10v2zm-4 4H9v-2h6v2zm4-8H9V5h10v2z'/%3E%3C/svg%3E"); +} + +/* ===== INFO ICONS ===== */ +.icon-info { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%233b82f6'%3E%3Cpath d='M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2M13,17H11V11H13M13,9H11V7H13'/%3E%3C/svg%3E"); +} + +.icon-time { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.7L16.2,16.2Z'/%3E%3C/svg%3E"); +} + +/* ===== ARROW ICONS ===== */ +.icon-arrow-right { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z'/%3E%3C/svg%3E"); +} + +.icon-arrow-down { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E"); +} + +/* ===== HOVER-STATES ===== */ +.btn:hover .icon, +.nav-item:hover .icon { + opacity: 0.8; +} + +/* ===== DARK MODE ===== */ +@media (prefers-color-scheme: dark) { + .icon-printer, + .icon-print, + .icon-home, + .icon-settings, + .icon-users, + .icon-dashboard, + .icon-plus, + .icon-edit, + .icon-refresh, + .icon-file, + .icon-queue, + .icon-time, + .icon-arrow-right, + .icon-arrow-down { + filter: brightness(2); + } +} + +/* ===== RESPONSIVE ICON-SIZES ===== */ +@media (max-width: 768px) { + .icon { width: 1.25rem; height: 1.25rem; } + .icon-lg { width: 1.75rem; height: 1.75rem; } + .icon-xl { width: 2.25rem; height: 2.25rem; } +} diff --git a/backend/static/css/build/kiosk-no-fa.css b/backend/static/css/build/kiosk-no-fa.css new file mode 100644 index 00000000..4d322724 --- /dev/null +++ b/backend/static/css/build/kiosk-no-fa.css @@ -0,0 +1,497 @@ +/* MYP Platform - Kiosk Optimierte CSS Bundle */ +/* Generiert am: 2025-06-01 23:40:46 */ + +/** + * MYP Platform - Kiosk-Optimierte CSS + * Maximale Performance für Offline-Kiosk-Umgebung + * Keine Touch-Events, minimale Animationen, optimierte Rendering-Performance + */ + +/* ===== CRITICAL INLINE STYLES (sollten im HTML-Head stehen) ===== */ +:root { + /* Reduzierte Farbvariablen für bessere Performance */ + --primary: #0073ce; + --primary-dark: #005a9f; + --bg: #fafbfc; + --surface: #ffffff; + --text: #111827; + --text-muted: #6b7280; + --border: #e5e7eb; + --shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +/* CSS Containment für bessere Performance */ +* { + contain: layout style; +} + +/* Basis-Reset ohne aufwendige Normalisierung */ +* { box-sizing: border-box; margin: 0; padding: 0; } +body { + font-family: system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; + contain: layout style paint; + /* Optimiert für Kiosk-Rendering */ + text-rendering: optimizeSpeed; + -webkit-font-smoothing: antialiased; +} + +/* ===== LAYOUT OPTIMIERUNGEN ===== */ +.header { + background: var(--surface); + border-bottom: 1px solid var(--border); + padding: 1rem; + contain: layout style; +} + +.nav { + display: flex; + gap: 1rem; + contain: layout; +} + +.nav-item { + padding: 0.5rem 1rem; + border-radius: 6px; + text-decoration: none; + color: var(--text-muted); + transition: background-color 0.1s ease; /* Minimale Transition */ +} + +.nav-item:hover { + background: var(--bg); + color: var(--text); +} + +.nav-item.active { + background: var(--primary); + color: white; +} + +/* ===== OPTIMIERTE KARTEN ===== */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + box-shadow: var(--shadow); + contain: layout style paint; +} + +.card:hover { + transform: translateY(-1px); /* Minimaler Hover-Effekt */ +} + +/* ===== BUTTONS OHNE KOMPLEXE ANIMATIONEN ===== */ +.btn { + background: var(--primary); + color: white; + border: none; + border-radius: 6px; + padding: 0.75rem 1.5rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.1s ease; + contain: layout style; +} + +.btn:hover { + background: var(--primary-dark); +} + +.btn-secondary { + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--bg); +} + +/* ===== INPUTS OPTIMIERT ===== */ +.input { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.75rem; + width: 100%; + transition: border-color 0.1s ease; + contain: layout style; +} + +.input:focus { + outline: none; + border-color: var(--primary); +} + +/* ===== TABELLEN OHNE KOMPLEXE EFFEKTE ===== */ +.table { + width: 100%; + border-collapse: collapse; + background: var(--surface); + border-radius: 8px; + overflow: hidden; + contain: layout; +} + +.table th { + background: var(--bg); + padding: 1rem; + text-align: left; + font-weight: 600; + border-bottom: 1px solid var(--border); +} + +.table td { + padding: 1rem; + border-bottom: 1px solid var(--border); +} + +.table tr:hover { + background: var(--bg); +} + +/* ===== STATUS BADGES VEREINFACHT ===== */ +.status { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.status-online { background: #d1fae5; color: #065f46; } +.status-offline { background: #fee2e2; color: #991b1b; } +.status-printing { background: #dbeafe; color: #1e40af; } + +/* ===== GRID LAYOUTS OPTIMIERT ===== */ +.grid { + display: grid; + gap: 1rem; + contain: layout; +} + +.grid-2 { grid-template-columns: repeat(2, 1fr); } +.grid-3 { grid-template-columns: repeat(3, 1fr); } +.grid-4 { grid-template-columns: repeat(4, 1fr); } + +/* ===== UTILITIES MINIMAL ===== */ +.flex { display: flex; contain: layout; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-1 { gap: 0.25rem; } +.gap-2 { gap: 0.5rem; } +.gap-4 { gap: 1rem; } + +.p-1 { padding: 0.25rem; } +.p-2 { padding: 0.5rem; } +.p-4 { padding: 1rem; } +.p-6 { padding: 1.5rem; } + +.m-1 { margin: 0.25rem; } +.m-2 { margin: 0.5rem; } +.m-4 { margin: 1rem; } + +.mb-2 { margin-bottom: 0.5rem; } +.mb-4 { margin-bottom: 1rem; } +.mb-6 { margin-bottom: 1.5rem; } + +.text-sm { font-size: 0.875rem; } +.text-lg { font-size: 1.125rem; } +.text-xl { font-size: 1.25rem; } +.text-2xl { font-size: 1.5rem; } + +.font-semibold { font-weight: 600; } +.font-bold { font-weight: 700; } + +.text-center { text-align: center; } +.text-right { text-align: right; } + +.w-full { width: 100%; } +.h-full { height: 100%; } + +.rounded { border-radius: 6px; } +.rounded-lg { border-radius: 8px; } + +.border { border: 1px solid var(--border); } +.border-t { border-top: 1px solid var(--border); } +.border-b { border-bottom: 1px solid var(--border); } + +.bg-white { background: var(--surface); } +.bg-gray-50 { background: var(--bg); } + +.text-gray-600 { color: var(--text-muted); } +.text-blue-600 { color: var(--primary); } + +/* ===== DARK MODE MINIMAL ===== */ +@media (prefers-color-scheme: dark) { + :root { + --bg: #1e293b; + --surface: #334155; + --text: #f8fafc; + --text-muted: #94a3b8; + --border: #475569; + --shadow: 0 2px 4px rgba(0,0,0,0.3); + } +} + +/* ===== RESPONSIVE FÜR KIOSK-DISPLAYS ===== */ +@media (max-width: 1024px) { + .grid-4 { grid-template-columns: repeat(2, 1fr); } + .grid-3 { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 768px) { + .grid-4, + .grid-3, + .grid-2 { grid-template-columns: 1fr; } + + .nav { + flex-direction: column; + gap: 0.5rem; + } +} + +/* ===== PERFORMANCE OPTIMIERUNGEN ===== */ +/* Deaktivierung nicht benötigter Features für Kiosk */ +* { + /* Keine Touch-Events */ + touch-action: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; +} + +/* Nur notwendige Elemente selektierbar */ +input, +textarea { + -webkit-user-select: text; + user-select: text; + touch-action: manipulation; +} + +/* Optimierte Scrolling-Performance */ +* { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; +} + +/* GPU-Acceleration nur wo nötig */ +.card:hover, +.btn:hover { + will-change: transform; +} + +/* Minimale Animationen für bessere Performance */ +@media (prefers-reduced-motion: reduce) { + * { + transition: none !important; + animation: none !important; + } +} + +/* Print-Optimierung (falls Kiosk drucken kann) */ +@media print { + .nav, + .btn { + display: none; + } + + .card { + box-shadow: none; + border: 1px solid #000; + } +} + +/* ===== LAYOUT SHIFT PREVENTION ===== */ +img { + height: auto; + max-width: 100%; + vertical-align: middle; +} + +/* Container für stabile Layouts */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + contain: layout; +} + +/* ===== KIOSK-SPEZIFISCHE OPTIMIERUNGEN ===== */ +/* Vollbildmodus-Unterstützung */ +html, +body { + height: 100%; + overflow-x: hidden; +} + +/* Fokus-Management für Keyboard-Navigation */ +:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Kein Selection-Highlighting */ +::selection { + background: transparent; +} + +/* Optimierte Font-Loading */ +@font-face { + font-family: 'system-ui'; + font-display: swap; +} + +/** + * Minimales Icon-Set für Kiosk-Modus + * SVG-basierte Icons als CSS-Pseudo-Elemente für beste Performance + */ + +/* Icon-Basis-Klasse */ +.icon { + display: inline-block; + width: 1rem; + height: 1rem; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + vertical-align: middle; +} + +.icon-lg { width: 1.5rem; height: 1.5rem; } +.icon-xl { width: 2rem; height: 2rem; } + +/* ===== DRUCKER ICONS ===== */ +.icon-printer { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z'/%3E%3C/svg%3E"); +} + +.icon-print { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z'/%3E%3C/svg%3E"); +} + +/* ===== STATUS ICONS ===== */ +.icon-check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E"); +} + +.icon-warning { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b'%3E%3Cpath d='M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z'/%3E%3C/svg%3E"); +} + +.icon-error { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ef4444'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/%3E%3C/svg%3E"); +} + +.icon-offline { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%236b7280'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/%3E%3C/svg%3E"); +} + +/* ===== NAVIGATION ICONS ===== */ +.icon-home { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/%3E%3C/svg%3E"); +} + +.icon-settings { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z'/%3E%3C/svg%3E"); +} + +.icon-users { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A3.012 3.012 0 0 0 16.43 6c-.68 0-1.3.27-1.77.72L12 8.5l-2.66-1.78C8.87 6.27 8.25 6 7.57 6c-1.31 0-2.42.83-2.83 2L2.5 16H5v6h2v-6h2v6h2zm-6.5-10.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5S12 9.17 12 10s.67 1.5 1.5 1.5z'/%3E%3C/svg%3E"); +} + +.icon-dashboard { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z'/%3E%3C/svg%3E"); +} + +/* ===== ACTION ICONS ===== */ +.icon-plus { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'/%3E%3C/svg%3E"); +} + +.icon-edit { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E"); +} + +.icon-delete { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ef4444'%3E%3Cpath d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'/%3E%3C/svg%3E"); +} + +.icon-refresh { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z'/%3E%3C/svg%3E"); +} + +/* ===== POWER/CONNECTION ICONS ===== */ +.icon-power { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z'/%3E%3C/svg%3E"); +} + +.icon-wifi { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z'/%3E%3C/svg%3E"); +} + +/* ===== DOKUMENT ICONS ===== */ +.icon-file { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z'/%3E%3C/svg%3E"); +} + +.icon-queue { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 9H9V9h10v2zm-4 4H9v-2h6v2zm4-8H9V5h10v2z'/%3E%3C/svg%3E"); +} + +/* ===== INFO ICONS ===== */ +.icon-info { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%233b82f6'%3E%3Cpath d='M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2M13,17H11V11H13M13,9H11V7H13'/%3E%3C/svg%3E"); +} + +.icon-time { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.7L16.2,16.2Z'/%3E%3C/svg%3E"); +} + +/* ===== ARROW ICONS ===== */ +.icon-arrow-right { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z'/%3E%3C/svg%3E"); +} + +.icon-arrow-down { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E"); +} + +/* ===== HOVER-STATES ===== */ +.btn:hover .icon, +.nav-item:hover .icon { + opacity: 0.8; +} + +/* ===== DARK MODE ===== */ +@media (prefers-color-scheme: dark) { + .icon-printer, + .icon-print, + .icon-home, + .icon-settings, + .icon-users, + .icon-dashboard, + .icon-plus, + .icon-edit, + .icon-refresh, + .icon-file, + .icon-queue, + .icon-time, + .icon-arrow-right, + .icon-arrow-down { + filter: brightness(2); + } +} + +/* ===== RESPONSIVE ICON-SIZES ===== */ +@media (max-width: 768px) { + .icon { width: 1.25rem; height: 1.25rem; } + .icon-lg { width: 1.75rem; height: 1.75rem; } + .icon-xl { width: 2.25rem; height: 2.25rem; } +} diff --git a/backend/static/css/build/kiosk-production.css b/backend/static/css/build/kiosk-production.css new file mode 100644 index 00000000..ed3463f3 --- /dev/null +++ b/backend/static/css/build/kiosk-production.css @@ -0,0 +1,498 @@ +/* MYP Platform - Kiosk Optimierte CSS Bundle */ +/* Generiert am: 2025-06-01 23:40:46 */ + +/** + * MYP Platform - Kiosk-Optimierte CSS + * Maximale Performance für Offline-Kiosk-Umgebung + * Keine Touch-Events, minimale Animationen, optimierte Rendering-Performance + */ + +/* ===== CRITICAL INLINE STYLES (sollten im HTML-Head stehen) ===== */ +:root { + /* Reduzierte Farbvariablen für bessere Performance */ + --primary: #0073ce; + --primary-dark: #005a9f; + --bg: #fafbfc; + --surface: #ffffff; + --text: #111827; + --text-muted: #6b7280; + --border: #e5e7eb; + --shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +/* CSS Containment für bessere Performance */ +* { + contain: layout style; +} + +/* Basis-Reset ohne aufwendige Normalisierung */ +* { box-sizing: border-box; margin: 0; padding: 0; } +body { + font-family: system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; + contain: layout style paint; + /* Optimiert für Kiosk-Rendering */ + text-rendering: optimizeSpeed; + -webkit-font-smoothing: antialiased; +} + +/* ===== LAYOUT OPTIMIERUNGEN ===== */ +.header { + background: var(--surface); + border-bottom: 1px solid var(--border); + padding: 1rem; + contain: layout style; +} + +.nav { + display: flex; + gap: 1rem; + contain: layout; +} + +.nav-item { + padding: 0.5rem 1rem; + border-radius: 6px; + text-decoration: none; + color: var(--text-muted); + transition: background-color 0.1s ease; /* Minimale Transition */ +} + +.nav-item:hover { + background: var(--bg); + color: var(--text); +} + +.nav-item.active { + background: var(--primary); + color: white; +} + +/* ===== OPTIMIERTE KARTEN ===== */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + box-shadow: var(--shadow); + contain: layout style paint; +} + +.card:hover { + transform: translateY(-1px); /* Minimaler Hover-Effekt */ +} + +/* ===== BUTTONS OHNE KOMPLEXE ANIMATIONEN ===== */ +.btn { + background: var(--primary); + color: white; + border: none; + border-radius: 6px; + padding: 0.75rem 1.5rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.1s ease; + contain: layout style; +} + +.btn:hover { + background: var(--primary-dark); +} + +.btn-secondary { + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--bg); +} + +/* ===== INPUTS OPTIMIERT ===== */ +.input { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.75rem; + width: 100%; + transition: border-color 0.1s ease; + contain: layout style; +} + +.input:focus { + outline: none; + border-color: var(--primary); +} + +/* ===== TABELLEN OHNE KOMPLEXE EFFEKTE ===== */ +.table { + width: 100%; + border-collapse: collapse; + background: var(--surface); + border-radius: 8px; + overflow: hidden; + contain: layout; +} + +.table th { + background: var(--bg); + padding: 1rem; + text-align: left; + font-weight: 600; + border-bottom: 1px solid var(--border); +} + +.table td { + padding: 1rem; + border-bottom: 1px solid var(--border); +} + +.table tr:hover { + background: var(--bg); +} + +/* ===== STATUS BADGES VEREINFACHT ===== */ +.status { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.status-online { background: #d1fae5; color: #065f46; } +.status-offline { background: #fee2e2; color: #991b1b; } +.status-printing { background: #dbeafe; color: #1e40af; } + +/* ===== GRID LAYOUTS OPTIMIERT ===== */ +.grid { + display: grid; + gap: 1rem; + contain: layout; +} + +.grid-2 { grid-template-columns: repeat(2, 1fr); } +.grid-3 { grid-template-columns: repeat(3, 1fr); } +.grid-4 { grid-template-columns: repeat(4, 1fr); } + +/* ===== UTILITIES MINIMAL ===== */ +.flex { display: flex; contain: layout; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-1 { gap: 0.25rem; } +.gap-2 { gap: 0.5rem; } +.gap-4 { gap: 1rem; } + +.p-1 { padding: 0.25rem; } +.p-2 { padding: 0.5rem; } +.p-4 { padding: 1rem; } +.p-6 { padding: 1.5rem; } + +.m-1 { margin: 0.25rem; } +.m-2 { margin: 0.5rem; } +.m-4 { margin: 1rem; } + +.mb-2 { margin-bottom: 0.5rem; } +.mb-4 { margin-bottom: 1rem; } +.mb-6 { margin-bottom: 1.5rem; } + +.text-sm { font-size: 0.875rem; } +.text-lg { font-size: 1.125rem; } +.text-xl { font-size: 1.25rem; } +.text-2xl { font-size: 1.5rem; } + +.font-semibold { font-weight: 600; } +.font-bold { font-weight: 700; } + +.text-center { text-align: center; } +.text-right { text-align: right; } + +.w-full { width: 100%; } +.h-full { height: 100%; } + +.rounded { border-radius: 6px; } +.rounded-lg { border-radius: 8px; } + +.border { border: 1px solid var(--border); } +.border-t { border-top: 1px solid var(--border); } +.border-b { border-bottom: 1px solid var(--border); } + +.bg-white { background: var(--surface); } +.bg-gray-50 { background: var(--bg); } + +.text-gray-600 { color: var(--text-muted); } +.text-blue-600 { color: var(--primary); } + +/* ===== DARK MODE MINIMAL ===== */ +@media (prefers-color-scheme: dark) { + :root { + --bg: #1e293b; + --surface: #334155; + --text: #f8fafc; + --text-muted: #94a3b8; + --border: #475569; + --shadow: 0 2px 4px rgba(0,0,0,0.3); + } +} + +/* ===== RESPONSIVE FÜR KIOSK-DISPLAYS ===== */ +@media (max-width: 1024px) { + .grid-4 { grid-template-columns: repeat(2, 1fr); } + .grid-3 { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 768px) { + .grid-4, + .grid-3, + .grid-2 { grid-template-columns: 1fr; } + + .nav { + flex-direction: column; + gap: 0.5rem; + } +} + +/* ===== PERFORMANCE OPTIMIERUNGEN ===== */ +/* Deaktivierung nicht benötigter Features für Kiosk */ +* { + /* Keine Touch-Events */ + touch-action: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; +} + +/* Nur notwendige Elemente selektierbar */ +input, +textarea { + -webkit-user-select: text; + user-select: text; + touch-action: manipulation; +} + +/* Optimierte Scrolling-Performance */ +* { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; +} + +/* GPU-Acceleration nur wo nötig */ +.card:hover, +.btn:hover { + will-change: transform; +} + +/* Minimale Animationen für bessere Performance */ +@media (prefers-reduced-motion: reduce) { + * { + transition: none !important; + animation: none !important; + } +} + +/* Print-Optimierung (falls Kiosk drucken kann) */ +@media print { + .nav, + .btn { + display: none; + } + + .card { + box-shadow: none; + border: 1px solid #000; + } +} + +/* ===== LAYOUT SHIFT PREVENTION ===== */ +img { + height: auto; + max-width: 100%; + vertical-align: middle; +} + +/* Container für stabile Layouts */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + contain: layout; +} + +/* ===== KIOSK-SPEZIFISCHE OPTIMIERUNGEN ===== */ +/* Vollbildmodus-Unterstützung */ +html, +body { + height: 100%; + overflow-x: hidden; +} + +/* Fokus-Management für Keyboard-Navigation */ +:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Kein Selection-Highlighting */ +::selection { + background: transparent; +} + +/* Optimierte Font-Loading */ +@font-face { + font-family: 'system-ui'; + font-display: swap; +} + +/** + * Minimales Icon-Set für Kiosk-Modus + * SVG-basierte Icons als CSS-Pseudo-Elemente für beste Performance + * Ersetzt FontAwesome für deutlich kleinere Bundle-Größe + */ + +/* Icon-Basis-Klasse */ +.icon { + display: inline-block; + width: 1rem; + height: 1rem; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + vertical-align: middle; +} + +.icon-lg { width: 1.5rem; height: 1.5rem; } +.icon-xl { width: 2rem; height: 2rem; } + +/* ===== DRUCKER ICONS ===== */ +.icon-printer { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z'/%3E%3C/svg%3E"); +} + +.icon-print { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z'/%3E%3C/svg%3E"); +} + +/* ===== STATUS ICONS ===== */ +.icon-check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E"); +} + +.icon-warning { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b'%3E%3Cpath d='M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z'/%3E%3C/svg%3E"); +} + +.icon-error { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ef4444'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/%3E%3C/svg%3E"); +} + +.icon-offline { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%236b7280'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/%3E%3C/svg%3E"); +} + +/* ===== NAVIGATION ICONS ===== */ +.icon-home { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/%3E%3C/svg%3E"); +} + +.icon-settings { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z'/%3E%3C/svg%3E"); +} + +.icon-users { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A3.012 3.012 0 0 0 16.43 6c-.68 0-1.3.27-1.77.72L12 8.5l-2.66-1.78C8.87 6.27 8.25 6 7.57 6c-1.31 0-2.42.83-2.83 2L2.5 16H5v6h2v-6h2v6h2zm-6.5-10.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5S12 9.17 12 10s.67 1.5 1.5 1.5z'/%3E%3C/svg%3E"); +} + +.icon-dashboard { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z'/%3E%3C/svg%3E"); +} + +/* ===== ACTION ICONS ===== */ +.icon-plus { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'/%3E%3C/svg%3E"); +} + +.icon-edit { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E"); +} + +.icon-delete { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ef4444'%3E%3Cpath d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'/%3E%3C/svg%3E"); +} + +.icon-refresh { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z'/%3E%3C/svg%3E"); +} + +/* ===== POWER/CONNECTION ICONS ===== */ +.icon-power { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z'/%3E%3C/svg%3E"); +} + +.icon-wifi { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z'/%3E%3C/svg%3E"); +} + +/* ===== DOKUMENT ICONS ===== */ +.icon-file { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z'/%3E%3C/svg%3E"); +} + +.icon-queue { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 9H9V9h10v2zm-4 4H9v-2h6v2zm4-8H9V5h10v2z'/%3E%3C/svg%3E"); +} + +/* ===== INFO ICONS ===== */ +.icon-info { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%233b82f6'%3E%3Cpath d='M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2M13,17H11V11H13M13,9H11V7H13'/%3E%3C/svg%3E"); +} + +.icon-time { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.7L16.2,16.2Z'/%3E%3C/svg%3E"); +} + +/* ===== ARROW ICONS ===== */ +.icon-arrow-right { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z'/%3E%3C/svg%3E"); +} + +.icon-arrow-down { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E"); +} + +/* ===== HOVER-STATES ===== */ +.btn:hover .icon, +.nav-item:hover .icon { + opacity: 0.8; +} + +/* ===== DARK MODE ===== */ +@media (prefers-color-scheme: dark) { + .icon-printer, + .icon-print, + .icon-home, + .icon-settings, + .icon-users, + .icon-dashboard, + .icon-plus, + .icon-edit, + .icon-refresh, + .icon-file, + .icon-queue, + .icon-time, + .icon-arrow-right, + .icon-arrow-down { + filter: brightness(2); + } +} + +/* ===== RESPONSIVE ICON-SIZES ===== */ +@media (max-width: 768px) { + .icon { width: 1.25rem; height: 1.25rem; } + .icon-lg { width: 1.75rem; height: 1.75rem; } + .icon-xl { width: 2.25rem; height: 2.25rem; } +} diff --git a/backend/static/css/caching-optimizations.css b/backend/static/css/caching-optimizations.css new file mode 100644 index 00000000..98570b2a --- /dev/null +++ b/backend/static/css/caching-optimizations.css @@ -0,0 +1,288 @@ +/** + * MYP Platform - CSS Caching-Optimierungen + * Performance-optimierte Styles für besseres Caching und schnellere Ladezeiten + */ + +/* ===== KRITISCHE ABOVE-THE-FOLD STYLES ===== */ +/* Diese Styles sollten inline im HTML-Head geladen werden */ +.critical-header { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + position: sticky; + top: 0; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.critical-nav { + display: flex; + align-items: center; + padding: 1rem; + max-width: 1200px; + margin: 0 auto; +} + +.critical-logo { + font-weight: 700; + font-size: 1.25rem; + color: #0073ce; +} + +.critical-main { + min-height: 100vh; + background: linear-gradient(135deg, #fafbfc 0%, #f5f7f9 100%); +} + +/* ===== LAZY LOADING PLACEHOLDER ===== */ +.lazy-placeholder { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: skeleton-loading 1.5s infinite ease-in-out; + border-radius: 4px; +} + +@keyframes skeleton-loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.dark .lazy-placeholder { + background: linear-gradient(90deg, #374151 25%, #4b5563 50%, #374151 75%); + background-size: 200% 100%; +} + +/* ===== PRELOAD HINTS ===== */ +.preload-image { + content-visibility: auto; + contain-intrinsic-size: 300px 200px; +} + +.preload-component { + content-visibility: auto; + contain-intrinsic-size: 100px; +} + +/* ===== CACHE-OPTIMIERTE KOMPONENTEN ===== */ +.cache-card { + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(229, 231, 235, 0.5); + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; + transition: transform 0.2s ease; + contain: layout style paint; +} + +.cache-card:hover { + transform: translateY(-1px); +} + +.cache-button { + background: #0073ce; + color: white; + border: none; + border-radius: 6px; + padding: 0.5rem 1rem; + font-weight: 600; + transition: background-color 0.2s ease; + contain: layout style; +} + +.cache-button:hover { + background: #005a9f; +} + +.cache-input { + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(229, 231, 235, 0.8); + border-radius: 4px; + padding: 0.5rem; + transition: border-color 0.2s ease; + contain: layout style; +} + +.cache-input:focus { + outline: none; + border-color: #0073ce; + box-shadow: 0 0 0 2px rgba(0, 115, 206, 0.1); +} + +/* ===== LAYOUT SHIFT PREVENTION ===== */ +.prevent-cls { + min-height: 200px; /* Prevent layout shift */ + contain: layout; +} + +.image-container { + aspect-ratio: 16/9; + overflow: hidden; + border-radius: 8px; + background: #f3f4f6; +} + +.image-container img { + width: 100%; + height: 100%; + object-fit: cover; + transition: opacity 0.3s ease; +} + +.image-container img[loading="lazy"] { + opacity: 0; +} + +.image-container img[loading="lazy"].loaded { + opacity: 1; +} + +/* ===== OPTIMIERTE TYPOGRAPHY ===== */ +.text-heading { + font-weight: 700; + line-height: 1.2; + color: #111827; + contain: layout style; +} + +.text-body { + line-height: 1.6; + color: #374151; + contain: layout style; +} + +.text-muted { + color: #6b7280; + contain: layout style; +} + +/* ===== DARK MODE CACHE OPTIMIZATIONS ===== */ +.dark .cache-card { + background: rgba(30, 41, 59, 0.9); + border-color: rgba(100, 116, 139, 0.3); +} + +.dark .cache-input { + background: rgba(30, 41, 59, 0.9); + border-color: rgba(100, 116, 139, 0.5); + color: #e2e8f0; +} + +.dark .text-heading { + color: #f8fafc; +} + +.dark .text-body { + color: #e2e8f0; +} + +.dark .text-muted { + color: #94a3b8; +} + +.dark .image-container { + background: #374151; +} + +/* ===== RESPONSIVE OPTIMIZATIONS ===== */ +@media (max-width: 768px) { + .critical-nav { + padding: 0.75rem; + } + + .cache-card { + padding: 0.75rem; + margin-bottom: 0.75rem; + } + + .cache-button { + padding: 0.75rem 1.25rem; + font-size: 0.875rem; + } +} + +/* ===== PRINT OPTIMIZATIONS ===== */ +@media print { + .critical-header, + .cache-button { + display: none; + } + + .cache-card { + box-shadow: none; + border: 1px solid #000; + break-inside: avoid; + } +} + +/* ===== GPU ACCELERATION HINTS ===== */ +.gpu-accelerated { + transform: translateZ(0); + will-change: transform; +} + +/* ===== EFFICIENT ANIMATIONS ===== */ +.fade-in-optimized { + opacity: 0; + transform: translateY(10px); + transition: opacity 0.3s ease, transform 0.3s ease; +} + +.fade-in-optimized.visible { + opacity: 1; + transform: translateY(0); +} + +/* ===== CONTAINER QUERIES SUPPORT ===== */ +@container (min-width: 300px) { + .cache-card { + display: grid; + grid-template-columns: 1fr auto; + gap: 1rem; + align-items: center; + } +} + +/* ===== CONTENT VISIBILITY OPTIMIZATIONS ===== */ +.auto-height { + content-visibility: auto; + contain-intrinsic-size: 0 200px; +} + +.hidden-content { + content-visibility: hidden; +} + +/* ===== REDUCED MOTION PREFERENCES ===== */ +@media (prefers-reduced-motion: reduce) { + .fade-in-optimized, + .cache-card, + .cache-button, + .cache-input { + transition: none !important; + } + + .lazy-placeholder { + animation: none !important; + background: #f0f0f0 !important; + } +} + +/* ===== HIGH CONTRAST MODE ===== */ +@media (prefers-contrast: high) { + .cache-card { + border-width: 2px; + border-color: #000; + } + + .cache-button { + border: 2px solid #000; + } + + .cache-input { + border-width: 2px; + border-color: #000; + } +} \ No newline at end of file diff --git a/backend/static/css/caching-optimizations.css.gz b/backend/static/css/caching-optimizations.css.gz new file mode 100644 index 00000000..591c4629 Binary files /dev/null and b/backend/static/css/caching-optimizations.css.gz differ diff --git a/backend/static/css/caching-optimizations.min.css b/backend/static/css/caching-optimizations.min.css new file mode 100644 index 00000000..0033727b --- /dev/null +++ b/backend/static/css/caching-optimizations.min.css @@ -0,0 +1 @@ +.critical-header{background:rgba(255,255,255,0.95);backdrop-filter:blur(8px);position:sticky;top:0;z-index:1000;box-shadow:0 2px 8px rgba(0,0,0,0.06)}.critical-nav{display:flex;align-items:center;padding:1rem;max-width:1200px;margin:0 auto}.critical-logo{font-weight:700;font-size:1.25rem;color:#0073ce}.critical-main{min-height:100vh;background:linear-gradient(135deg,#fafbfc 0,#f5f7f9 100%)}.lazy-placeholder{background:linear-gradient(90deg,#f0f0f0 25%,#e0e0e0 50%,#f0f0f0 75%);background-size:200% 100%;animation:skeleton-loading 1.5s infinite ease-in-out;border-radius:4px}@keyframes skeleton-loading{0%{background-position:200% 0}100%{background-position:-200% 0}}.dark .lazy-placeholder{background:linear-gradient(90deg,#374151 25%,#4b5563 50%,#374151 75%);background-size:200% 100%}.preload-image{content-visibility:auto;contain-intrinsic-size:300px 200px}.preload-component{content-visibility:auto;contain-intrinsic-size:100px}.cache-card{background:rgba(255,255,255,0.9);border:1px solid rgba(229,231,235,0.5);border-radius:8px;padding:1rem;margin-bottom:1rem;transition:transform .2s ease;contain:layout style paint}.cache-card:hover{transform:translateY(-1px)}.cache-button{background:#0073ce;color:white;border:0;border-radius:6px;padding:.5rem 1rem;font-weight:600;transition:background-color .2s ease;contain:layout style}.cache-button:hover{background:#005a9f}.cache-input{background:rgba(255,255,255,0.9);border:1px solid rgba(229,231,235,0.8);border-radius:4px;padding:.5rem;transition:border-color .2s ease;contain:layout style}.cache-input:focus{outline:0;border-color:#0073ce;box-shadow:0 0 0 2px rgba(0,115,206,0.1)}.prevent-cls{min-height:200px;contain:layout}.image-container{aspect-ratio:16/9;overflow:hidden;border-radius:8px;background:#f3f4f6}.image-container img{width:100%;height:100%;object-fit:cover;transition:opacity .3s ease}.image-container img[loading="lazy"]{opacity:0}.image-container img[loading="lazy"].loaded{opacity:1}.text-heading{font-weight:700;line-height:1.2;color:#111827;contain:layout style}.text-body{line-height:1.6;color:#374151;contain:layout style}.text-muted{color:#6b7280;contain:layout style}.dark .cache-card{background:rgba(30,41,59,0.9);border-color:rgba(100,116,139,0.3)}.dark .cache-input{background:rgba(30,41,59,0.9);border-color:rgba(100,116,139,0.5);color:#e2e8f0}.dark .text-heading{color:#f8fafc}.dark .text-body{color:#e2e8f0}.dark .text-muted{color:#94a3b8}.dark .image-container{background:#374151}@media(max-width:768px){.critical-nav{padding:.75rem}.cache-card{padding:.75rem;margin-bottom:.75rem}.cache-button{padding:.75rem 1.25rem;font-size:.875rem}}@media print{.critical-header,.cache-button{display:none}.cache-card{box-shadow:none;border:1px solid #000;break-inside:avoid}}.gpu-accelerated{transform:translateZ(0);will-change:transform}.fade-in-optimized{opacity:0;transform:translateY(10px);transition:opacity .3s ease,transform .3s ease}.fade-in-optimized.visible{opacity:1;transform:translateY(0)}@container(min-width:300px){.cache-card{display:grid;grid-template-columns:1fr auto;gap:1rem;align-items:center}}.auto-height{content-visibility:auto;contain-intrinsic-size:0 200px}.hidden-content{content-visibility:hidden}@media(prefers-reduced-motion:reduce){.fade-in-optimized,.cache-card,.cache-button,.cache-input{transition:none !important}.lazy-placeholder{animation:none !important;background:#f0f0f0 !important}}@media(prefers-contrast:high){.cache-card{border-width:2px;border-color:#000}.cache-button{border:2px solid #000}.cache-input{border-width:2px;border-color:#000}} \ No newline at end of file diff --git a/backend/static/css/caching-optimizations.min.css.gz b/backend/static/css/caching-optimizations.min.css.gz new file mode 100644 index 00000000..96144b87 Binary files /dev/null and b/backend/static/css/caching-optimizations.min.css.gz differ diff --git a/backend/static/css/components.css.gz b/backend/static/css/components.css.gz new file mode 100644 index 00000000..66026fa7 Binary files /dev/null and b/backend/static/css/components.css.gz differ diff --git a/backend/static/css/components.min.css b/backend/static/css/components.min.css new file mode 100644 index 00000000..49917fed --- /dev/null +++ b/backend/static/css/components.min.css @@ -0,0 +1 @@ +@layer components{.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}.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}.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}.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}.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}.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}.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}.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}.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}.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}.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}.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}.mercedes-glass{background:rgba(255,255,255,0.9);backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.2);transition:all .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)}.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{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}.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)}.professional-button{background:linear-gradient(135deg,#3b82f6 0,#1d4ed8 100%);transition:all .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 .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)}.input-field{transition:all .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)}.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 .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)}.nav-item{position:relative;transition:all .3s ease;border-radius:.75rem}.nav-item::after{content:'';position:absolute;bottom:-2px;left:50%;width:0;height:2px;background:linear-gradient(90deg,#3b82f6,#1d4ed8);transition:all .3s ease;transform:translateX(-50%)}.nav-item:hover::after,.nav-item.active::after{width:100%}.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)}.main-container{background:rgba(248,250,252,0.8);backdrop-filter:blur(10px)}.dark .main-container{background:rgba(15,23,42,0.8)}.status-badge{display:inline-flex;align-items:center;padding:.5rem .75rem;font-size:.75rem;font-weight:700;border-radius:9999px;transition:all .2s ease;border:1px solid transparent;text-transform:uppercase;letter-spacing:.025em}.status-badge:hover{transform:scale(1.05)}*{transition:background-color .3s ease,border-color .3s ease,color .3s ease,box-shadow .3s ease,transform .3s ease}.interactive-hover{transition:all .3s cubic-bezier(0.4,0,0.2,1)}.interactive-hover:hover{transform:translateY(-2px)}.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)}.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-ring:focus{outline:3px solid #3b82f6;outline-offset:2px}.dark .focus-ring:focus{outline:3px solid #60a5fa}.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:-.025em}.dark .professional-title{background:linear-gradient(135deg,#f8fafc 0,#e2e8f0 100%);background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent}@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)}}.fade-in{animation:fadeIn .5s ease-in-out}.slide-up{animation:slideUp .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{--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)}} \ No newline at end of file diff --git a/backend/static/css/components.min.css.gz b/backend/static/css/components.min.css.gz new file mode 100644 index 00000000..ce0ed5cc Binary files /dev/null and b/backend/static/css/components.min.css.gz differ diff --git a/backend/static/css/critical-inline.css b/backend/static/css/critical-inline.css new file mode 100644 index 00000000..74c190ef --- /dev/null +++ b/backend/static/css/critical-inline.css @@ -0,0 +1,31 @@ +/* CRITICAL INLINE CSS - Sollte im HTML stehen */ +/* Nur die absolut notwendigen Styles für First Paint */ + +:root{--primary:#0073ce;--bg:#fafbfc;--surface:#fff;--text:#111827;--border:#e5e7eb;--shadow:0 2px 4px rgba(0,0,0,.05)} +*{box-sizing:border-box;margin:0;padding:0;contain:layout style} +body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.5;text-rendering:optimizeSpeed;-webkit-font-smoothing:antialiased} +.header{background:var(--surface);border-bottom:1px solid var(--border);padding:1rem;position:sticky;top:0;z-index:1000} +.nav{display:flex;gap:1rem} +.nav-item{padding:.5rem 1rem;border-radius:6px;text-decoration:none;color:#6b7280;transition:background .1s} +.nav-item:hover{background:var(--bg);color:var(--text)} +.nav-item.active{background:var(--primary);color:#fff} +.container{max-width:1200px;margin:0 auto;padding:0 1rem} +.btn{background:var(--primary);color:#fff;border:none;border-radius:6px;padding:.75rem 1.5rem;font-weight:600;cursor:pointer;transition:background .1s} +.btn:hover{background:#005a9f} +.card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1rem;box-shadow:var(--shadow)} +.flex{display:flex} +.grid{display:grid;gap:1rem} +.hidden{display:none} +.w-full{width:100%} +.text-center{text-align:center} +.p-4{padding:1rem} +.mb-4{margin-bottom:1rem} +.status{display:inline-block;padding:.25rem .75rem;border-radius:999px;font-size:.75rem;font-weight:600;text-transform:uppercase} +.status-online{background:#d1fae5;color:#065f46} +.status-offline{background:#fee2e2;color:#991b1b} +.status-printing{background:#dbeafe;color:#1e40af} +.input{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:.75rem;width:100%} +.input:focus{outline:none;border-color:var(--primary)} +@media (prefers-color-scheme:dark){:root{--bg:#1e293b;--surface:#334155;--text:#f8fafc;--border:#475569;--shadow:0 2px 4px rgba(0,0,0,.3)}} +@media (max-width:768px){.nav{flex-direction:column;gap:.5rem}} +@media (prefers-reduced-motion:reduce){*{transition:none!important}} \ No newline at end of file diff --git a/backend/static/css/critical-inline.css.gz b/backend/static/css/critical-inline.css.gz new file mode 100644 index 00000000..066738d6 Binary files /dev/null and b/backend/static/css/critical-inline.css.gz differ diff --git a/backend/static/css/critical-inline.min.css b/backend/static/css/critical-inline.min.css new file mode 100644 index 00000000..b7169c7d --- /dev/null +++ b/backend/static/css/critical-inline.min.css @@ -0,0 +1 @@ +*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif;line-height:1.6;color:#111827;background:#fafbfc}.critical-header{position:sticky;top:0;z-index:1000;background:rgba(255,255,255,0.95);backdrop-filter:blur(8px);border-bottom:1px solid #e5e7eb}.critical-nav{display:flex;align-items:center;justify-content:space-between;padding:1rem;max-width:1200px;margin:0 auto}.critical-logo{font-weight:700;font-size:1.25rem;color:#0073ce;text-decoration:none}.critical-main{min-height:100vh;padding:2rem 1rem}.critical-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1.5rem;max-width:1200px;margin:0 auto}.critical-card{background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:1.5rem;box-shadow:0 1px 3px rgba(0,0,0,0.1)}.btn-primary{background:#0073ce;color:#fff;border:0;border-radius:6px;padding:.75rem 1.5rem;font-weight:600;cursor:pointer;text-decoration:none;display:inline-block}.loading{opacity:.6;pointer-events:none}.hidden{display:none}@media(max-width:768px){.critical-nav{padding:.75rem}.critical-main{padding:1rem}.critical-grid{grid-template-columns:1fr}.critical-card{padding:1rem}}@media(prefers-color-scheme:dark){body{background:#0f172a;color:#f8fafc}.critical-header{background:rgba(15,23,42,0.95);border-bottom-color:#334155}.critical-card{background:#1e293b;border-color:#334155;color:#f8fafc}} \ No newline at end of file diff --git a/backend/static/css/critical-inline.min.css.gz b/backend/static/css/critical-inline.min.css.gz new file mode 100644 index 00000000..52cc51a3 Binary files /dev/null and b/backend/static/css/critical-inline.min.css.gz differ diff --git a/backend/static/css/glassmorphism.css b/backend/static/css/glassmorphism.css index 1c75a2b0..4b8f3eb8 100644 --- a/backend/static/css/glassmorphism.css +++ b/backend/static/css/glassmorphism.css @@ -1,485 +1,255 @@ -/* Enhanced Glassmorphism Effects for MYP Application */ +/* Vereinfachte Glassmorphism-Effekte für MYP Application */ -/* Base Glass Effects */ +/* ===== BASIS GLASS EFFEKTE ===== */ .glass-base { - backdrop-filter: blur(24px) saturate(200%) brightness(115%); - -webkit-backdrop-filter: blur(24px) saturate(200%) brightness(115%); - border: 1px solid rgba(255, 255, 255, 0.25); - box-shadow: - 0 25px 50px rgba(0, 0, 0, 0.12), - 0 8px 16px rgba(0, 0, 0, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.3); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + background: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; } .glass-strong { - backdrop-filter: blur(28px) saturate(220%) brightness(125%); - -webkit-backdrop-filter: blur(28px) saturate(220%) brightness(125%); - box-shadow: - 0 35px 70px rgba(0, 0, 0, 0.15), - 0 12px 24px rgba(0, 0, 0, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.4); + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.4); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12); } .glass-subtle { - backdrop-filter: blur(20px) saturate(180%) brightness(110%); - -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(110%); - box-shadow: - 0 15px 35px rgba(0, 0, 0, 0.08), - 0 4px 8px rgba(0, 0, 0, 0.05), - inset 0 1px 0 rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); } -/* Light Mode Glass */ -.glass-light { - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.4); - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.1), - 0 8px 16px rgba(0, 115, 206, 0.05), - inset 0 1px 0 rgba(255, 255, 255, 0.6); -} - -.glass-light-strong { - background: rgba(255, 255, 255, 0.75); - border: 1px solid rgba(255, 255, 255, 0.5); - box-shadow: - 0 25px 50px rgba(0, 0, 0, 0.12), - 0 10px 20px rgba(0, 115, 206, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.7); -} - -/* Light Mode Glass Premium */ -.glass-light-premium { - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.9) 0%, - rgba(248, 250, 252, 0.8) 50%, - rgba(255, 255, 255, 0.85) 100%); - border: 1px solid rgba(226, 232, 240, 0.6); - box-shadow: - 0 25px 50px rgba(0, 0, 0, 0.08), - 0 10px 20px rgba(0, 115, 206, 0.06), - inset 0 2px 0 rgba(255, 255, 255, 0.8), - inset 0 0 20px rgba(255, 255, 255, 0.3); -} - -.glass-light-card { - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.95) 0%, - rgba(250, 251, 252, 0.9) 100%); - border: 1px solid rgba(226, 232, 240, 0.4); - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.06), - 0 8px 16px rgba(0, 115, 206, 0.04), - inset 0 1px 0 rgba(255, 255, 255, 0.9); -} - -/* Dark Mode Glass */ -.glass-dark { +/* ===== DARK MODE GLASS ===== */ +.dark .glass-base { background: rgba(15, 23, 42, 0.8); border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: - 0 25px 50px rgba(0, 0, 0, 0.4), - 0 8px 16px rgba(0, 0, 0, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.05); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); } -.glass-dark-strong { +.dark .glass-strong { background: rgba(30, 41, 59, 0.85); border: 1px solid rgba(255, 255, 255, 0.15); - box-shadow: - 0 35px 70px rgba(0, 0, 0, 0.5), - 0 12px 24px rgba(0, 0, 0, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.08); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4); } -/* Interactive Glass Elements */ -.glass-interactive { - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - cursor: pointer; - position: relative; - overflow: hidden; +.dark .glass-subtle { + background: rgba(15, 23, 42, 0.7); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } -.glass-interactive::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.6s ease; - z-index: 1; -} - -.glass-interactive:hover { - transform: translateY(-3px) scale(1.01); - backdrop-filter: blur(32px) saturate(240%) brightness(130%); - -webkit-backdrop-filter: blur(32px) saturate(240%) brightness(130%); - box-shadow: - 0 40px 80px rgba(0, 0, 0, 0.15), - 0 16px 32px rgba(0, 115, 206, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.4); -} - -.glass-interactive:hover::before { - left: 100%; -} - -.glass-interactive:active { - transform: translateY(-1px) scale(0.99); - transition: transform 0.1s ease; -} - -/* Glass Navigation */ -.glass-nav { - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.9) 0%, - rgba(248, 250, 252, 0.85) 50%, - rgba(255, 255, 255, 0.9) 100%); - backdrop-filter: blur(24px) saturate(200%) brightness(115%); - -webkit-backdrop-filter: blur(24px) saturate(200%) brightness(115%); - border: 1px solid rgba(226, 232, 240, 0.5); - border-bottom: 1px solid rgba(203, 213, 225, 0.6); - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.08), - 0 4px 12px rgba(0, 115, 206, 0.05), - inset 0 1px 0 rgba(255, 255, 255, 0.8); -} - -.dark .glass-nav { - background: rgba(15, 23, 42, 0.85); - border-color: rgba(51, 65, 85, 0.6); - border-bottom-color: rgba(71, 85, 105, 0.7); - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.1); -} - -/* Glass Cards */ +/* ===== GLASS KARTEN ===== */ .glass-card { - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.95) 0%, - rgba(250, 251, 252, 0.9) 100%); - backdrop-filter: blur(20px) saturate(180%) brightness(110%); - -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(110%); - border: 1px solid rgba(229, 231, 235, 0.6); - border-radius: 16px; - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.08), - 0 8px 16px rgba(0, 115, 206, 0.04), - inset 0 1px 0 rgba(255, 255, 255, 0.9); + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(229, 231, 235, 0.5); + border-radius: 12px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08); padding: 1.5rem; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; -} - -.glass-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 1px; - background: linear-gradient(90deg, - transparent 0%, - rgba(226, 232, 240, 0.8) 50%, - transparent 100%); + transition: transform 0.2s ease, box-shadow 0.2s ease; } .glass-card:hover { - transform: translateY(-2px); - box-shadow: - 0 25px 50px rgba(0, 0, 0, 0.12), - 0 12px 24px rgba(0, 115, 206, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.95); + transform: translateY(-1px); + box-shadow: 0 12px 20px rgba(0, 0, 0, 0.12); } .dark .glass-card { background: rgba(30, 41, 59, 0.85); - border-color: rgba(100, 116, 139, 0.4); - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.1); + border: 1px solid rgba(100, 116, 139, 0.3); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.25); } .dark .glass-card:hover { - box-shadow: - 0 25px 50px rgba(0, 0, 0, 0.4), - inset 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: 0 12px 20px rgba(0, 0, 0, 0.35); } -/* Glass Buttons */ +/* ===== GLASS NAVIGATION ===== */ +.glass-nav { + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(226, 232, 240, 0.4); + border-bottom: 1px solid rgba(203, 213, 225, 0.5); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); +} + +.dark .glass-nav { + background: rgba(15, 23, 42, 0.85); + border: 1px solid rgba(51, 65, 85, 0.5); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +/* ===== GLASS BUTTONS ===== */ .glass-btn { - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.9) 0%, - rgba(248, 250, 252, 0.8) 100%); - backdrop-filter: blur(16px) saturate(180%); - -webkit-backdrop-filter: blur(16px) saturate(180%); - border: 1px solid rgba(226, 232, 240, 0.6); - border-radius: 12px; + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid rgba(226, 232, 240, 0.5); + border-radius: 8px; padding: 0.75rem 1.5rem; color: #0f172a; font-weight: 600; - text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8); - box-shadow: - 0 4px 12px rgba(0, 0, 0, 0.08), - 0 2px 4px rgba(0, 115, 206, 0.05), - inset 0 1px 0 rgba(255, 255, 255, 0.8); - transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + transition: transform 0.2s ease, background 0.2s ease; } .glass-btn:hover { transform: translateY(-1px); - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.95) 0%, - rgba(248, 250, 252, 0.85) 100%); - box-shadow: - 0 8px 20px rgba(0, 0, 0, 0.12), - 0 4px 8px rgba(0, 115, 206, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.9); -} - -.glass-btn:active { - transform: translateY(0); - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.7); + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .glass-btn-primary { - background: linear-gradient(135deg, - rgba(0, 115, 206, 0.9) 0%, - rgba(0, 90, 159, 0.85) 100%); + background: rgba(0, 115, 206, 0.9); color: white; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); - box-shadow: - 0 4px 12px rgba(0, 115, 206, 0.3), - 0 2px 4px rgba(0, 115, 206, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.2); + box-shadow: 0 2px 8px rgba(0, 115, 206, 0.2); } .glass-btn-primary:hover { - background: linear-gradient(135deg, - rgba(0, 115, 206, 0.95) 0%, - rgba(0, 90, 159, 0.9) 100%); - box-shadow: - 0 8px 20px rgba(0, 115, 206, 0.4), - 0 4px 8px rgba(0, 115, 206, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.3); + background: rgba(0, 115, 206, 0.95); + box-shadow: 0 4px 12px rgba(0, 115, 206, 0.3); } .dark .glass-btn { background: rgba(30, 41, 59, 0.8); color: #e2e8f0; - border-color: rgba(100, 116, 139, 0.6); - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); - box-shadow: - 0 4px 12px rgba(0, 0, 0, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.1); + border: 1px solid rgba(100, 116, 139, 0.5); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } -/* Glass Modals */ +.dark .glass-btn:hover { + background: rgba(30, 41, 59, 0.9); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); +} + +/* ===== GLASS MODALS ===== */ .glass-modal { - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.98) 0%, - rgba(248, 250, 252, 0.95) 50%, - rgba(255, 255, 255, 0.98) 100%); - backdrop-filter: blur(32px) saturate(220%) brightness(120%); - -webkit-backdrop-filter: blur(32px) saturate(220%) brightness(120%); - border: 1px solid rgba(226, 232, 240, 0.7); - border-radius: 20px; - box-shadow: - 0 50px 100px rgba(0, 0, 0, 0.15), - 0 20px 40px rgba(0, 115, 206, 0.08), - inset 0 2px 0 rgba(255, 255, 255, 0.9), - inset 0 0 40px rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(226, 232, 240, 0.6); + border-radius: 16px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12); } .dark .glass-modal { background: rgba(15, 23, 42, 0.95); - border-color: rgba(51, 65, 85, 0.7); - box-shadow: - 0 50px 100px rgba(0, 0, 0, 0.5), - inset 0 2px 0 rgba(255, 255, 255, 0.1); + border: 1px solid rgba(51, 65, 85, 0.6); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); } -/* Glass Form Elements */ +/* ===== GLASS FORM ELEMENTE ===== */ .glass-input { background: rgba(255, 255, 255, 0.8); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border: 1px solid rgba(226, 232, 240, 0.6); - border-radius: 8px; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid rgba(226, 232, 240, 0.5); + border-radius: 6px; padding: 0.75rem 1rem; color: #0f172a; - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.06), - inset 0 1px 0 rgba(255, 255, 255, 0.8); - transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); + transition: border-color 0.2s ease, box-shadow 0.2s ease; } .glass-input:focus { outline: none; - border-color: rgba(0, 115, 206, 0.6); - box-shadow: - 0 4px 12px rgba(0, 115, 206, 0.15), - 0 0 0 3px rgba(0, 115, 206, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.9); + border-color: rgba(0, 115, 206, 0.5); + box-shadow: 0 2px 8px rgba(0, 115, 206, 0.1); } .dark .glass-input { background: rgba(30, 41, 59, 0.8); - border-color: rgba(100, 116, 139, 0.6); + border: 1px solid rgba(100, 116, 139, 0.5); color: #e2e8f0; - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); } -/* Glass Dropdown */ +.dark .glass-input:focus { + border-color: rgba(59, 130, 246, 0.5); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15); +} + +/* ===== 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%); + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); 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); + border-radius: 8px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); } .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); + background: rgba(15, 23, 42, 0.9); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); } -/* 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 */ +/* ===== UTILITY CLASSES ===== */ .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%); + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); } .dark .glass-overlay { - background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.1) 100%); + background: rgba(0, 0, 0, 0.2); } -/* Responsive glass effects */ +/* ===== RESPONSIVE ANPASSUNGEN ===== */ @media (max-width: 768px) { .glass-card { padding: 1rem; - border-radius: 12px; + border-radius: 8px; } .glass-modal { - border-radius: 16px; + border-radius: 12px; + } + + .glass-base, + .glass-strong, + .glass-subtle { + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); } } -/* High contrast mode adjustments */ +/* ===== REDUZIERTE BEWEGUNG ===== */ +@media (prefers-reduced-motion: reduce) { + .glass-card, + .glass-btn, + .glass-input { + transition: none !important; + } +} + +/* ===== HOHER KONTRAST MODUS ===== */ @media (prefers-contrast: high) { .glass-base, .glass-strong, .glass-card { border-width: 2px; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); } } -/* Reduced motion support */ -@media (prefers-reduced-motion: reduce) { - .glass-interactive, - .glass-btn, - .glass-card { - transition: none !important; - } - - .glass-interactive::before { - display: none; - } -} - -/* Glass Loading States */ -.glass-loading { - position: relative; - overflow: hidden; -} - -.glass-loading::after { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, - transparent, - rgba(255, 255, 255, 0.3), - transparent); - animation: glass-shimmer 2s infinite; -} - -.dark .glass-loading::after { - background: linear-gradient(90deg, - transparent, - rgba(255, 255, 255, 0.1), - transparent); -} - -@keyframes glass-shimmer { - 0% { left: -100%; } - 100% { left: 100%; } -} - -/* Premium Glow Effects for Light Mode */ -.glass-glow { - position: relative; -} - -.glass-glow::after { - content: ''; - position: absolute; - top: -2px; - left: -2px; - right: -2px; - bottom: -2px; - background: linear-gradient(45deg, - rgba(0, 115, 206, 0.1), - rgba(0, 90, 159, 0.05), - rgba(0, 115, 206, 0.1)); - border-radius: inherit; - z-index: -1; - opacity: 0; - transition: opacity 0.3s ease; -} - -.glass-glow:hover::after { - opacity: 1; -} - -.dark .glass-glow::after { - background: linear-gradient(45deg, - rgba(59, 130, 246, 0.2), - rgba(29, 78, 216, 0.1), - rgba(59, 130, 246, 0.2)); +/* ===== PERFORMANCE OPTIMIERUNGEN ===== */ +.glass-base, +.glass-strong, +.glass-subtle, +.glass-card, +.glass-btn, +.glass-modal { + will-change: transform; } \ No newline at end of file diff --git a/backend/static/css/glassmorphism.css.gz b/backend/static/css/glassmorphism.css.gz new file mode 100644 index 00000000..c6bea938 Binary files /dev/null and b/backend/static/css/glassmorphism.css.gz differ diff --git a/backend/static/css/glassmorphism.min.css b/backend/static/css/glassmorphism.min.css new file mode 100644 index 00000000..d3643444 --- /dev/null +++ b/backend/static/css/glassmorphism.min.css @@ -0,0 +1 @@ +.glass-base{background:rgba(255,255,255,0.85);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.3);box-shadow:0 8px 20px rgba(0,0,0,0.1);transition:all .2s ease}.glass-strong{background:rgba(255,255,255,0.9);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.4);box-shadow:0 12px 24px rgba(0,0,0,0.12)}.glass-subtle{background:rgba(255,255,255,0.8);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.2);box-shadow:0 4px 12px rgba(0,0,0,0.08)}.dark .glass-base{background:rgba(15,23,42,0.8);border:1px solid rgba(255,255,255,0.1);box-shadow:0 8px 20px rgba(0,0,0,0.3)}.dark .glass-strong{background:rgba(30,41,59,0.85);border:1px solid rgba(255,255,255,0.15);box-shadow:0 12px 24px rgba(0,0,0,0.4)}.dark .glass-subtle{background:rgba(15,23,42,0.7);border:1px solid rgba(255,255,255,0.08);box-shadow:0 4px 12px rgba(0,0,0,0.2)}.glass-card{background:rgba(255,255,255,0.9);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(229,231,235,0.5);border-radius:12px;box-shadow:0 8px 16px rgba(0,0,0,0.08);padding:1.5rem;transition:transform .2s ease,box-shadow .2s ease}.glass-card:hover{transform:translateY(-1px);box-shadow:0 12px 20px rgba(0,0,0,0.12)}.dark .glass-card{background:rgba(30,41,59,0.85);border:1px solid rgba(100,116,139,0.3);box-shadow:0 8px 16px rgba(0,0,0,0.25)}.dark .glass-card:hover{box-shadow:0 12px 20px rgba(0,0,0,0.35)}.glass-nav{background:rgba(255,255,255,0.9);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(226,232,240,0.4);border-bottom:1px solid rgba(203,213,225,0.5);box-shadow:0 4px 12px rgba(0,0,0,0.06)}.dark .glass-nav{background:rgba(15,23,42,0.85);border:1px solid rgba(51,65,85,0.5);box-shadow:0 4px 12px rgba(0,0,0,0.2)}.glass-btn{background:rgba(255,255,255,0.8);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid rgba(226,232,240,0.5);border-radius:8px;padding:.75rem 1.5rem;color:#0f172a;font-weight:600;box-shadow:0 2px 8px rgba(0,0,0,0.06);transition:transform .2s ease,background .2s ease}.glass-btn:hover{transform:translateY(-1px);background:rgba(255,255,255,0.9);box-shadow:0 4px 12px rgba(0,0,0,0.1)}.glass-btn-primary{background:rgba(0,115,206,0.9);color:white;box-shadow:0 2px 8px rgba(0,115,206,0.2)}.glass-btn-primary:hover{background:rgba(0,115,206,0.95);box-shadow:0 4px 12px rgba(0,115,206,0.3)}.dark .glass-btn{background:rgba(30,41,59,0.8);color:#e2e8f0;border:1px solid rgba(100,116,139,0.5);box-shadow:0 2px 8px rgba(0,0,0,0.15)}.dark .glass-btn:hover{background:rgba(30,41,59,0.9);box-shadow:0 4px 12px rgba(0,0,0,0.25)}.glass-modal{background:rgba(255,255,255,0.95);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid rgba(226,232,240,0.6);border-radius:16px;box-shadow:0 20px 40px rgba(0,0,0,0.12)}.dark .glass-modal{background:rgba(15,23,42,0.95);border:1px solid rgba(51,65,85,0.6);box-shadow:0 20px 40px rgba(0,0,0,0.4)}.glass-input{background:rgba(255,255,255,0.8);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid rgba(226,232,240,0.5);border-radius:6px;padding:.75rem 1rem;color:#0f172a;box-shadow:0 2px 4px rgba(0,0,0,0.04);transition:border-color .2s ease,box-shadow .2s ease}.glass-input:focus{outline:0;border-color:rgba(0,115,206,0.5);box-shadow:0 2px 8px rgba(0,115,206,0.1)}.dark .glass-input{background:rgba(30,41,59,0.8);border:1px solid rgba(100,116,139,0.5);color:#e2e8f0;box-shadow:0 2px 4px rgba(0,0,0,0.15)}.dark .glass-input:focus{border-color:rgba(59,130,246,0.5);box-shadow:0 2px 8px rgba(59,130,246,0.15)}.glass-dropdown{background:rgba(255,255,255,0.9);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.3);border-radius:8px;box-shadow:0 8px 20px rgba(0,0,0,0.15)}.dark .glass-dropdown{background:rgba(15,23,42,0.9);border:1px solid rgba(255,255,255,0.1);box-shadow:0 8px 20px rgba(0,0,0,0.3)}.glass-overlay{background:rgba(255,255,255,0.1);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}.dark .glass-overlay{background:rgba(0,0,0,0.2)}@media(max-width:768px){.glass-card{padding:1rem;border-radius:8px}.glass-modal{border-radius:12px}.glass-base,.glass-strong,.glass-subtle{backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}}@media(prefers-reduced-motion:reduce){.glass-card,.glass-btn,.glass-input{transition:none !important}}@media(prefers-contrast:high){.glass-base,.glass-strong,.glass-card{border-width:2px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}}.glass-base,.glass-strong,.glass-subtle,.glass-card,.glass-btn,.glass-modal{will-change:transform} \ No newline at end of file diff --git a/backend/static/css/glassmorphism.min.css.gz b/backend/static/css/glassmorphism.min.css.gz new file mode 100644 index 00000000..d52be497 Binary files /dev/null and b/backend/static/css/glassmorphism.min.css.gz differ diff --git a/backend/static/css/icons-minimal.css b/backend/static/css/icons-minimal.css new file mode 100644 index 00000000..0c684aa1 --- /dev/null +++ b/backend/static/css/icons-minimal.css @@ -0,0 +1,148 @@ +/** + * Minimales Icon-Set für Kiosk-Modus + * SVG-basierte Icons als CSS-Pseudo-Elemente für beste Performance + * Ersetzt FontAwesome für deutlich kleinere Bundle-Größe + */ + +/* Icon-Basis-Klasse */ +.icon { + display: inline-block; + width: 1rem; + height: 1rem; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + vertical-align: middle; +} + +.icon-lg { width: 1.5rem; height: 1.5rem; } +.icon-xl { width: 2rem; height: 2rem; } + +/* ===== DRUCKER ICONS ===== */ +.icon-printer { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z'/%3E%3C/svg%3E"); +} + +.icon-print { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z'/%3E%3C/svg%3E"); +} + +/* ===== STATUS ICONS ===== */ +.icon-check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E"); +} + +.icon-warning { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b'%3E%3Cpath d='M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z'/%3E%3C/svg%3E"); +} + +.icon-error { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ef4444'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/%3E%3C/svg%3E"); +} + +.icon-offline { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%236b7280'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/%3E%3C/svg%3E"); +} + +/* ===== NAVIGATION ICONS ===== */ +.icon-home { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/%3E%3C/svg%3E"); +} + +.icon-settings { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z'/%3E%3C/svg%3E"); +} + +.icon-users { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A3.012 3.012 0 0 0 16.43 6c-.68 0-1.3.27-1.77.72L12 8.5l-2.66-1.78C8.87 6.27 8.25 6 7.57 6c-1.31 0-2.42.83-2.83 2L2.5 16H5v6h2v-6h2v6h2zm-6.5-10.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5S12 9.17 12 10s.67 1.5 1.5 1.5z'/%3E%3C/svg%3E"); +} + +.icon-dashboard { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z'/%3E%3C/svg%3E"); +} + +/* ===== ACTION ICONS ===== */ +.icon-plus { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'/%3E%3C/svg%3E"); +} + +.icon-edit { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'/%3E%3C/svg%3E"); +} + +.icon-delete { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ef4444'%3E%3Cpath d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'/%3E%3C/svg%3E"); +} + +.icon-refresh { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z'/%3E%3C/svg%3E"); +} + +/* ===== POWER/CONNECTION ICONS ===== */ +.icon-power { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z'/%3E%3C/svg%3E"); +} + +.icon-wifi { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2310b981'%3E%3Cpath d='M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z'/%3E%3C/svg%3E"); +} + +/* ===== DOKUMENT ICONS ===== */ +.icon-file { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z'/%3E%3C/svg%3E"); +} + +.icon-queue { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 9H9V9h10v2zm-4 4H9v-2h6v2zm4-8H9V5h10v2z'/%3E%3C/svg%3E"); +} + +/* ===== INFO ICONS ===== */ +.icon-info { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%233b82f6'%3E%3Cpath d='M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2M13,17H11V11H13M13,9H11V7H13'/%3E%3C/svg%3E"); +} + +.icon-time { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.7L16.2,16.2Z'/%3E%3C/svg%3E"); +} + +/* ===== ARROW ICONS ===== */ +.icon-arrow-right { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z'/%3E%3C/svg%3E"); +} + +.icon-arrow-down { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23374151'%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E"); +} + +/* ===== HOVER-STATES ===== */ +.btn:hover .icon, +.nav-item:hover .icon { + opacity: 0.8; +} + +/* ===== DARK MODE ===== */ +@media (prefers-color-scheme: dark) { + .icon-printer, + .icon-print, + .icon-home, + .icon-settings, + .icon-users, + .icon-dashboard, + .icon-plus, + .icon-edit, + .icon-refresh, + .icon-file, + .icon-queue, + .icon-time, + .icon-arrow-right, + .icon-arrow-down { + filter: brightness(2); + } +} + +/* ===== RESPONSIVE ICON-SIZES ===== */ +@media (max-width: 768px) { + .icon { width: 1.25rem; height: 1.25rem; } + .icon-lg { width: 1.75rem; height: 1.75rem; } + .icon-xl { width: 2.25rem; height: 2.25rem; } +} \ No newline at end of file diff --git a/backend/static/css/input.css.gz b/backend/static/css/input.css.gz new file mode 100644 index 00000000..0f00d2a2 Binary files /dev/null and b/backend/static/css/input.css.gz differ diff --git a/backend/static/css/input.min.css b/backend/static/css/input.min.css new file mode 100644 index 00000000..18cd63b2 --- /dev/null +++ b/backend/static/css/input.min.css @@ -0,0 +1 @@ +@tailwind base;@tailwind components;@tailwind utilities;@layer base{:root{--color-bg-primary:#fff;--color-bg-secondary:#fafbfc;--color-bg-tertiary:#f3f5f7;--color-bg-accent:#fbfcfd;--color-text-primary:#111827;--color-text-secondary:#374151;--color-text-muted:#6b7280;--color-text-accent:#0073ce;--color-border-primary:#e5e7eb;--color-border-secondary:#d1d5db;--color-accent:#0073ce;--color-accent-hover:#005a9f;--color-accent-light:#eff6ff;--color-accent-text:#fff;--color-shadow:rgba(0,0,0,0.06);--color-shadow-strong:rgba(0,0,0,0.1);--color-shadow-accent:rgba(0,115,206,0.12);--card-radius:1rem;--gradient-primary:linear-gradient(135deg,#fff 0,#fafbfc 30%,#f8fafc 70%,#f3f5f7 100%);--gradient-card:linear-gradient(135deg,#fff 0,#fcfcfd 50%,#fafbfc 100%);--gradient-hero:linear-gradient(135deg,#fafbfc 0,#f3f5f7 40%,#eef2f5 80%,#f8fafc 100%);--gradient-accent:linear-gradient(135deg,#0073ce 0,#005a9f 100%);--gradient-surface:linear-gradient(135deg,#fff 0,#fbfcfd 50%,#f8fafc 100%);--glass-bg:rgba(255,255,255,0.92);--glass-border:rgba(255,255,255,0.3);--glass-shadow:0 8px 32px rgba(0,0,0,0.04);--glass-blur:blur(20px)}.dark{--color-bg-primary:#000;--color-bg-secondary:#0a0a0a;--color-bg-tertiary:#1a1a1a;--color-text-primary:#fff;--color-text-secondary:#e2e8f0;--color-text-muted:#94a3b8;--color-border-primary:#1a1a1a;--color-border-secondary:#2a2a2a;--color-accent:#fff;--color-accent-hover:#f0f0f0;--color-accent-light:#1e3a8a;--color-accent-text:#000;--color-shadow:rgba(0,0,0,0.8);--color-shadow-strong:rgba(0,0,0,0.9);--mb-black:#000}body{@apply bg-white dark:bg-black text-slate-900 dark:text-white transition-colors duration-300;position:relative;min-height:100vh;background:var(--gradient-primary);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-feature-settings:'cv02','cv03','cv04','cv11';line-height:1.65;font-size:15px}.dark body{background:linear-gradient(135deg,#000 0,#0a0a0a 50%,#000 100%)}body::before{content:'';position:fixed;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at 25% 25%,rgba(0,115,206,0.015) 0,transparent 50%),radial-gradient(circle at 75% 75%,rgba(0,115,206,0.01) 0,transparent 50%),radial-gradient(circle at 50% 10%,rgba(0,115,206,0.008) 0,transparent 50%);pointer-events:none;z-index:-1}.dark body::before{background:radial-gradient(circle at 20% 50%,rgba(59,130,246,0.03) 0,transparent 50%),radial-gradient(circle at 80% 20%,rgba(59,130,246,0.02) 0,transparent 50%)}nav{@apply backdrop-blur-xl border-b transition-all duration-300;background:linear-gradient(135deg,rgba(255,255,255,0.95) 0,rgba(250,251,252,0.92) 30%,rgba(248,250,252,0.9) 70%,rgba(255,255,255,0.95) 100%);border-bottom:1px solid rgba(229,231,235,0.7);backdrop-filter:blur(28px) saturate(200%) brightness(110%);-webkit-backdrop-filter:blur(28px) saturate(200%) brightness(110%);box-shadow:0 4px 20px rgba(0,0,0,0.04),0 2px 8px rgba(0,115,206,0.02),inset 0 1px 0 rgba(255,255,255,0.9)}.dark nav{background:rgba(0,0,0,0.85);border-bottom-color:rgba(255,255,255,0.1);box-shadow:0 8px 32px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.05)}.card-enhanced{background:var(--gradient-card);border:1px solid var(--color-border-primary);border-radius:var(--card-radius);box-shadow:0 2px 12px rgba(0,0,0,0.03),0 1px 4px rgba(0,115,206,0.02),inset 0 1px 0 rgba(255,255,255,0.8);transition:all .3s cubic-bezier(0.4,0,0.2,1);position:relative;overflow:hidden}.card-enhanced::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:var(--gradient-accent);opacity:0;transition:opacity .3s ease}.card-enhanced:hover{transform:translateY(-2px);box-shadow:0 8px 25px rgba(0,0,0,0.06),0 4px 12px rgba(0,115,206,0.04),inset 0 1px 0 rgba(255,255,255,0.9)}.card-enhanced:hover::before{opacity:1}.dark .card-enhanced{background:rgba(10,10,10,0.8);border-color:var(--color-border-primary);box-shadow:0 4px 20px var(--color-shadow)}.btn-enhanced{background:var(--gradient-accent);color:var(--color-accent-text);border:0;border-radius:.5rem;padding:.75rem 1.75rem;font-weight:600;font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;box-shadow:0 2px 8px rgba(0,115,206,0.2),0 1px 4px rgba(0,115,206,0.1);transition:all .2s cubic-bezier(0.4,0,0.2,1);position:relative;overflow:hidden}.btn-enhanced::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 .5s ease}.btn-enhanced:hover{transform:translateY(-1px);box-shadow:0 4px 15px rgba(0,115,206,0.3),0 2px 8px rgba(0,115,206,0.2)}.btn-enhanced:hover::before{left:100%}.btn-enhanced:active{transform:translateY(0)}.btn-secondary{background:var(--gradient-surface);color:var(--color-text-primary);border:1px solid var(--color-border-primary);box-shadow:0 1px 6px rgba(0,0,0,0.03),inset 0 1px 0 rgba(255,255,255,0.8)}.btn-secondary:hover{background:var(--color-bg-secondary);border-color:var(--color-accent);color:var(--color-accent);box-shadow:0 4px 12px rgba(0,115,206,0.08),inset 0 1px 0 rgba(255,255,255,0.9)}.input-enhanced{background:rgba(255,255,255,0.95);border:1px solid var(--color-border-primary);border-radius:.5rem;padding:.75rem 1rem;color:var(--color-text-primary);font-size:.9rem;box-shadow:0 1px 6px rgba(0,0,0,0.02),inset 0 1px 0 rgba(255,255,255,0.9);transition:all .2s ease;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.input-enhanced:focus{outline:0;border-color:var(--color-accent);box-shadow:0 4px 12px rgba(0,115,206,0.1),0 0 0 3px rgba(0,115,206,0.05),inset 0 1px 0 rgba(255,255,255,0.95);background:rgba(255,255,255,0.98)}.input-enhanced::placeholder{color:var(--color-text-muted);opacity:.8}.dark .input-enhanced{background:rgba(10,10,10,0.8);border-color:var(--color-border-primary);color:var(--color-text-primary);box-shadow:0 2px 8px var(--color-shadow),inset 0 1px 0 rgba(255,255,255,0.05)}.dark .input-enhanced:focus{border-color:#60a5fa;box-shadow:0 4px 15px rgba(96,165,250,0.2),0 0 0 3px rgba(96,165,250,0.1)}.alert-enhanced{border-radius:1rem;padding:1.25rem;border:1px solid transparent;position:relative;overflow:hidden;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px)}.alert-enhanced::before{content:'';position:absolute;top:0;left:0;bottom:0;width:4px}.alert-info-enhanced{background:linear-gradient(135deg,rgba(239,246,255,0.95) 0,rgba(219,234,254,0.9) 100%);border-color:rgba(59,130,246,0.2);color:#1e40af}.alert-info-enhanced::before{background:var(--gradient-accent)}.alert-success-enhanced{background:linear-gradient(135deg,rgba(236,253,245,0.95) 0,rgba(167,243,208,0.9) 100%);border-color:rgba(16,185,129,0.2);color:#065f46}.alert-success-enhanced::before{background:linear-gradient(180deg,#10b981 0,#059669 100%)}.alert-warning-enhanced{background:linear-gradient(135deg,rgba(255,251,235,0.95) 0,rgba(254,243,199,0.9) 100%);border-color:rgba(251,191,36,0.2);color:#92400e}.alert-warning-enhanced::before{background:linear-gradient(180deg,#fbbf24 0,#f59e0b 100%)}.alert-error-enhanced{background:linear-gradient(135deg,rgba(254,242,242,0.95) 0,rgba(252,165,165,0.9) 100%);border-color:rgba(239,68,68,0.2);color:#991b1b}.alert-error-enhanced::before{background:linear-gradient(180deg,#ef4444 0,#dc2626 100%)}.flash-message-light{background:linear-gradient(135deg,rgba(255,255,255,0.95) 0,rgba(248,250,252,0.9) 100%);backdrop-filter:blur(32px) saturate(200%) brightness(120%);-webkit-backdrop-filter:blur(32px) saturate(200%) brightness(120%);border:1px solid rgba(226,232,240,0.6);box-shadow:0 25px 50px rgba(0,0,0,0.1),0 12px 24px rgba(0,115,206,0.05),inset 0 1px 0 rgba(255,255,255,0.8);color:var(--color-text-primary)}.flash-message-light.success{border-left:4px solid #10b981;background:linear-gradient(135deg,rgba(236,253,245,0.95) 0,rgba(209,250,229,0.9) 100%)}.flash-message-light.error{border-left:4px solid #ef4444;background:linear-gradient(135deg,rgba(254,242,242,0.95) 0,rgba(252,165,165,0.9) 100%)}.flash-message-light.warning{border-left:4px solid #fbbf24;background:linear-gradient(135deg,rgba(255,251,235,0.95) 0,rgba(254,243,199,0.9) 100%)}.flash-message-light.info{border-left:4px solid #3b82f6;background:linear-gradient(135deg,rgba(239,246,255,0.95) 0,rgba(219,234,254,0.9) 100%)}.table-enhanced{background:var(--gradient-card);border:1px solid var(--color-border-primary);border-radius:var(--card-radius);overflow:hidden;box-shadow:0 4px 20px var(--color-shadow),0 2px 8px rgba(0,115,206,0.04),inset 0 1px 0 rgba(255,255,255,0.6)}.table-enhanced th{background:linear-gradient(135deg,var(--color-bg-secondary) 0,var(--color-bg-tertiary) 100%);color:var(--color-text-primary);font-weight:600;padding:1rem 1.5rem;border-bottom:1px solid var(--color-border-primary);position:relative}.table-enhanced th::after{content:'';position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent 0,var(--color-border-secondary) 50%,transparent 100%)}.table-enhanced td{padding:1rem 1.5rem;border-bottom:1px solid var(--color-border-primary);color:var(--color-text-secondary);transition:all .2s ease}.table-enhanced tbody tr:hover{background:var(--color-bg-secondary);transform:scale(1.002)}.dark .table-enhanced{background:rgba(10,10,10,0.8);border-color:var(--color-border-primary)}.dark .table-enhanced th{background:rgba(26,26,26,0.8);color:var(--color-text-primary)}.dark .table-enhanced tbody tr:hover{background:rgba(26,26,26,0.6)}.modal-enhanced{background:linear-gradient(135deg,rgba(255,255,255,0.98) 0,rgba(248,250,252,0.95) 50%,rgba(255,255,255,0.98) 100%);backdrop-filter:blur(32px) saturate(220%) brightness(120%);-webkit-backdrop-filter:blur(32px) saturate(220%) brightness(120%);border:1px solid rgba(226,232,240,0.7);border-radius:1.5rem;box-shadow:0 50px 100px rgba(0,0,0,0.15),0 20px 40px rgba(0,115,206,0.08),inset 0 2px 0 rgba(255,255,255,0.9);position:relative;overflow:hidden}.modal-enhanced::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent 0,rgba(226,232,240,0.8) 50%,transparent 100%)}.dark .modal-enhanced{background:rgba(0,0,0,0.95);border-color:rgba(42,42,42,0.7);box-shadow:0 50px 100px rgba(0,0,0,0.5),inset 0 2px 0 rgba(255,255,255,0.05)}.status-badge-enhanced{display:inline-flex;align-items:center;padding:.5rem 1rem;font-size:.75rem;font-weight:700;border-radius:9999px;text-transform:uppercase;letter-spacing:.05em;border:1px solid transparent;transition:all .2s ease;position:relative;overflow:hidden}.status-badge-enhanced::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.3),transparent);transition:left .5s ease}.status-badge-enhanced:hover::before{left:100%}.status-online-enhanced{background:linear-gradient(135deg,#ecfdf5 0,#a7f3d0 100%);color:#065f46;border-color:rgba(16,185,129,0.3)}.status-offline-enhanced{background:linear-gradient(135deg,#fef2f2 0,#fca5a5 100%);color:#991b1b;border-color:rgba(239,68,68,0.3)}.status-printing-enhanced{background:linear-gradient(135deg,#eff6ff 0,#bfdbfe 100%);color:#1e40af;border-color:rgba(59,130,246,0.3)}.dark-mode-toggle-new{position:relative;display:flex;cursor:pointer;align-items:center;justify-content:center;border-radius:9999px;padding:.625rem;transition:all .3s cubic-bezier(0.4,0,0.2,1);background:linear-gradient(135deg,rgba(248,250,252,0.9) 0,rgba(241,245,249,0.8) 100%);border:1px solid rgba(226,232,240,0.7);box-shadow:0 4px 12px rgba(0,0,0,0.06),0 2px 4px rgba(0,115,206,0.04),inset 0 1px 0 rgba(255,255,255,0.8);color:var(--color-text-secondary);z-index:100}.dark-mode-toggle-new:hover{transform:translateY(-2px) scale(1.05);background:linear-gradient(135deg,rgba(248,250,252,0.95) 0,rgba(241,245,249,0.85) 100%);box-shadow:0 8px 20px rgba(0,0,0,0.1),0 4px 8px rgba(0,115,206,0.08),inset 0 1px 0 rgba(255,255,255,0.9)}.dark-mode-toggle-new:active{transform:translateY(-1px) scale(0.98);transition:transform .1s}.dark .dark-mode-toggle-new{background:rgba(10,10,10,0.8);border:1px solid rgba(42,42,42,0.6);box-shadow:0 4px 12px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.05);color:var(--color-text-secondary)}.dark .dark-mode-toggle-new:hover{background:rgba(10,10,10,0.9);box-shadow:0 8px 20px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.08)}.dark-mode-toggle-new .sun-icon,.dark-mode-toggle-new .moon-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);transition:all .3s cubic-bezier(0.4,0,0.2,1)}.dark-mode-toggle-new .sun-icon:not(.hidden){animation:icon-appear .5s cubic-bezier(0.25,1,0.5,1) forwards}.dark-mode-toggle-new .moon-icon:not(.hidden){animation:icon-appear .5s cubic-bezier(0.25,1,0.5,1) forwards}@keyframes icon-appear{0%{opacity:0;transform:translate(-50%,-50%) scale(0.5) rotate(-20deg)}100%{opacity:1;transform:translate(-50%,-50%) scale(1) rotate(0)}}.dark .sun-icon{display:none}.dark .moon-icon{display:block}.sun-icon{display:block}.moon-icon{display:none}.user-menu-button-new{display:flex;align-items:center;gap:.5rem;border-radius:.75rem;padding:.5rem;transition:all .3s cubic-bezier(0.4,0,0.2,1);background:linear-gradient(135deg,rgba(248,250,252,0.8) 0,rgba(241,245,249,0.7) 100%);border:1px solid rgba(226,232,240,0.6);box-shadow:0 2px 8px rgba(0,0,0,0.05),inset 0 1px 0 rgba(255,255,255,0.7)}.user-menu-button-new:hover{transform:translateY(-1px);background:linear-gradient(135deg,rgba(248,250,252,0.9) 0,rgba(241,245,249,0.8) 100%);box-shadow:0 4px 12px rgba(0,0,0,0.08),0 2px 4px rgba(0,115,206,0.04),inset 0 1px 0 rgba(255,255,255,0.8)}.dark .user-menu-button-new{background:rgba(10,10,10,0.7);border-color:rgba(42,42,42,0.6);box-shadow:0 2px 8px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.03)}.dark .user-menu-button-new:hover{background:rgba(10,10,10,0.8);box-shadow:0 4px 12px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.05)}.hover-lift-enhanced{transition:all .3s cubic-bezier(0.4,0,0.2,1)}.hover-lift-enhanced:hover{transform:translateY(-3px) scale(1.01);box-shadow:0 12px 30px var(--color-shadow-strong),0 6px 15px var(--color-shadow-accent)}.dark .hover-lift-enhanced:hover{box-shadow:0 12px 30px var(--color-shadow)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--color-bg-secondary);border-radius:4px}::-webkit-scrollbar-thumb{background:linear-gradient(180deg,var(--color-border-secondary) 0,var(--color-border-primary) 100%);border-radius:4px;transition:background .2s ease}::-webkit-scrollbar-thumb:hover{background:linear-gradient(180deg,var(--color-accent) 0,var(--color-accent-hover) 100%)}.dark ::-webkit-scrollbar-track{background:var(--color-bg-secondary)}.dark ::-webkit-scrollbar-thumb{background:var(--color-border-primary)}.dark ::-webkit-scrollbar-thumb:hover{background:#60a5fa}.loading-enhanced{position:relative;overflow:hidden}.loading-enhanced::after{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,115,206,0.1),transparent);animation:loading-shimmer 2s infinite}@keyframes loading-shimmer{0%{left:-100%}100%{left:100%}}.focus-enhanced:focus{outline:2px solid var(--color-accent);outline-offset:2px;box-shadow:0 0 0 4px rgba(0,115,206,0.15),0 4px 12px var(--color-shadow-accent)}.dark .focus-enhanced:focus{outline-color:#60a5fa;box-shadow:0 0 0 4px rgba(96,165,250,0.15),0 4px 12px rgba(96,165,250,0.2)}@media(max-width:768px){.card-enhanced{padding:1rem;border-radius:.75rem}.btn-enhanced{padding:.75rem 1.5rem;font-size:.8rem}.modal-enhanced{border-radius:1rem;margin:1rem}.dark-mode-toggle-new{padding:.5rem}}@media(prefers-reduced-motion:reduce){*{transition:none !important;animation:none !important}}@media(prefers-contrast:high){:root{--color-shadow:rgba(0,0,0,0.2);--color-shadow-strong:rgba(0,0,0,0.3);--color-border-primary:#000}.dark{--color-border-primary:#fff}}}@layer components{.dark .bg-dark-card{@apply bg-dark-surface transition-colors}.bg-dark-surface{background-color:#1e293b}.transition-all-colors{@apply transition-colors duration-300}.admin-container{@apply max-w-7xl mx-auto p-4 md:p-8}.admin-stats{@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8}.stat-card{@apply bg-white/60 dark:bg-black/70 rounded-xl border border-gray-200/60 dark:border-slate-700/30 p-5 relative overflow-hidden shadow-2xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1 backdrop-blur-xl;backdrop-filter:blur(20px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(180%) brightness(110%);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 0 0 1px rgba(255,255,255,0.1)}.stat-icon{@apply absolute top-4 right-4 opacity-15 text-4xl}.stat-title{@apply text-sm text-slate-500 dark:text-slate-400 mb-2 font-medium uppercase}.stat-value{@apply text-2xl font-bold text-slate-900 dark:text-white mb-1}.stat-desc{@apply text-sm text-slate-500 dark:text-slate-400}.nav-tabs{@apply flex border-b border-gray-200 dark:border-slate-700/30 mb-4 overflow-x-auto}.nav-tab{@apply py-4 px-6 text-slate-600 dark:text-slate-300 border-b-2 border-transparent cursor-pointer transition-all duration-200 whitespace-nowrap hover:text-slate-900 dark:hover:text-white hover:bg-slate-50 dark:hover:bg-slate-800/50}.nav-tab.active{@apply text-slate-900 dark:text-white border-b-2 border-black dark:border-white font-medium}.tab-content{@apply mt-8}.tab-pane{@apply hidden}.tab-pane.active{@apply block}.form-group{@apply mb-4}.form-label{@apply block mb-2 text-sm font-medium text-slate-700 dark:text-slate-300}.form-input,.form-select,.form-textarea{@apply w-full px-3 py-2 bg-white/60 dark:bg-slate-800/60 border border-gray-300/60 dark:border-slate-600/60 rounded-lg text-slate-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all duration-200 backdrop-blur-lg;backdrop-filter:blur(16px) saturate(150%);-webkit-backdrop-filter:blur(16px) saturate(150%);box-shadow:0 10px 20px rgba(0,0,0,0.1),0 0 0 1px rgba(255,255,255,0.05)}.admin-table{@apply min-w-full divide-y divide-gray-200 dark:divide-slate-700}.admin-table thead{@apply bg-slate-50 dark:bg-slate-800}.admin-table th{@apply px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider}.admin-table tbody{@apply bg-white dark:bg-dark-surface divide-y divide-gray-200 dark:divide-slate-700}.admin-table tr{@apply hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors}.admin-table td{@apply px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-white}.badge{@apply px-2 inline-flex text-xs leading-5 font-semibold rounded-full}.badge-success{@apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200}.badge-error{@apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200}.badge-warning{@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200}.badge-info{@apply bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200}.printer-card{@apply bg-white/60 dark:bg-black/70 rounded-xl border border-gray-200/60 dark:border-slate-700/30 p-6 shadow-2xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1 backdrop-blur-xl;backdrop-filter:blur(20px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(180%) brightness(110%);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 0 0 1px rgba(255,255,255,0.1)}.printer-header{@apply flex justify-between items-center mb-4}.printer-name{@apply text-xl font-bold text-slate-900 dark:text-white}.printer-actions{@apply flex space-x-2}.printer-info{@apply grid grid-cols-2 gap-4 mb-4}.printer-status{@apply flex items-center mt-4}.status-indicator{@apply w-3 h-3 rounded-full mr-2}.status-running{@apply bg-green-500;animation:pulse 2s infinite}.status-stopped{@apply bg-red-500}@keyframes pulse{0%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.1)}100%{opacity:1;transform:scale(1)}}.log-entry{@apply p-3 border-l-4 mb-2 rounded-r-lg bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors}.log-debug{@apply border-gray-400 dark:border-gray-500}.log-info{@apply border-blue-400 dark:border-blue-500}.log-warning{@apply border-yellow-400 dark:border-yellow-500}.log-error{@apply border-red-400 dark:border-red-500}.log-critical{@apply border-purple-400 dark:border-purple-500}.scheduler-status{@apply flex items-center p-4 bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 shadow-md}.progress-bar{@apply w-full h-2 bg-gray-200 dark:bg-slate-700 rounded-full overflow-hidden}.progress-bar-fill{@apply h-full transition-all duration-300}.progress-bar-fill-blue{@apply bg-blue-500 dark:bg-blue-600}.progress-bar-fill-green{@apply bg-green-500 dark:bg-green-600}.progress-bar-fill-purple{@apply bg-purple-500 dark:bg-purple-600}.notification{@apply fixed top-4 right-4 max-w-md p-4 rounded-2xl shadow-2xl transform translate-x-full opacity-0 transition-all duration-500 z-50;background:rgba(255,255,255,0.08);backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);-webkit-backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 32px 64px rgba(0,0,0,0.25),0 12px 24px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.1);animation:notification-slide-in .6s cubic-bezier(0.4,0,0.2,1)}.dark .notification{background:rgba(0,0,0,0.2);backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(115%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(115%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 32px 64px rgba(0,0,0,0.6),0 12px 24px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}.notification.show{@apply translate-x-0 opacity-100}.notification:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 40px 80px rgba(0,0,0,0.3),0 16px 32px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.5),0 0 0 1px rgba(255,255,255,0.15)}.dark .notification:hover{box-shadow:0 40px 80px rgba(0,0,0,0.7),0 16px 32px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.1)}.notification-success{@apply text-green-100;background:linear-gradient(135deg,rgba(34,197,94,0.25) 0,rgba(134,239,172,0.18) 50%,rgba(34,197,94,0.12) 100%);border:1px solid rgba(34,197,94,0.4);box-shadow:0 32px 64px rgba(34,197,94,0.2),0 12px 24px rgba(34,197,94,0.1),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(34,197,94,0.3)}.notification-error{@apply text-red-100;background:linear-gradient(135deg,rgba(239,68,68,0.25) 0,rgba(252,165,165,0.18) 50%,rgba(239,68,68,0.12) 100%);border:1px solid rgba(239,68,68,0.4);box-shadow:0 32px 64px rgba(239,68,68,0.2),0 12px 24px rgba(239,68,68,0.1),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(239,68,68,0.3)}.notification-warning{@apply text-yellow-100;background:linear-gradient(135deg,rgba(245,158,11,0.25) 0,rgba(252,211,77,0.18) 50%,rgba(245,158,11,0.12) 100%);border:1px solid rgba(245,158,11,0.4);box-shadow:0 32px 64px rgba(245,158,11,0.2),0 12px 24px rgba(245,158,11,0.1),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(245,158,11,0.3)}.notification-info{@apply text-blue-100;background:linear-gradient(135deg,rgba(59,130,246,0.25) 0,rgba(147,197,253,0.18) 50%,rgba(59,130,246,0.12) 100%);border:1px solid rgba(59,130,246,0.4);box-shadow:0 32px 64px rgba(59,130,246,0.2),0 12px 24px rgba(59,130,246,0.1),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(59,130,246,0.3)}.toast-notification{@apply fixed z-50 p-4 rounded-2xl shadow-2xl transform transition-all duration-500 text-sm font-medium;background:rgba(255,255,255,0.08);backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);-webkit-backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 32px 64px rgba(0,0,0,0.25),0 12px 24px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.1)}.dark .toast-notification{background:rgba(0,0,0,0.2);backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(115%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(115%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 32px 64px rgba(0,0,0,0.6),0 12px 24px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}.alert{@apply p-6 rounded-2xl border mb-6 shadow-2xl;background:rgba(255,255,255,0.12);backdrop-filter:blur(30px) saturate(200%) brightness(120%) contrast(110%);-webkit-backdrop-filter:blur(30px) saturate(200%) brightness(120%) contrast(110%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 8px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.1);animation:alert-fade-in .5s ease-out}.dark .alert{background:rgba(0,0,0,0.3);backdrop-filter:blur(30px) saturate(180%) brightness(110%) contrast(120%);-webkit-backdrop-filter:blur(30px) saturate(180%) brightness(110%) contrast(120%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 25px 50px rgba(0,0,0,0.4),0 8px 16px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.15),0 0 0 1px rgba(255,255,255,0.05)}.alert-success{@apply text-green-900 dark:text-green-100;background:linear-gradient(135deg,rgba(34,197,94,0.15) 0,rgba(134,239,172,0.1) 50%,rgba(34,197,94,0.08) 100%);border:1px solid rgba(34,197,94,0.3)}.alert-error{@apply text-red-900 dark:text-red-100;background:linear-gradient(135deg,rgba(239,68,68,0.15) 0,rgba(252,165,165,0.1) 50%,rgba(239,68,68,0.08) 100%);border:1px solid rgba(239,68,68,0.3)}.alert-warning{@apply text-yellow-900 dark:text-yellow-100;background:linear-gradient(135deg,rgba(245,158,11,0.15) 0,rgba(252,211,77,0.1) 50%,rgba(245,158,11,0.08) 100%);border:1px solid rgba(245,158,11,0.3)}.alert-info{@apply text-blue-900 dark:text-blue-100;background:linear-gradient(135deg,rgba(59,130,246,0.15) 0,rgba(147,197,253,0.1) 50%,rgba(59,130,246,0.08) 100%);border:1px solid rgba(59,130,246,0.3)}.browser-notification{@apply fixed top-4 left-4 max-w-sm p-4 rounded-2xl shadow-2xl z-50;background:rgba(255,255,255,0.08);backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);-webkit-backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 32px 64px rgba(0,0,0,0.25),0 12px 24px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.1);animation:notification-slide-left .6s cubic-bezier(0.4,0,0.2,1)}.dark .browser-notification{background:rgba(0,0,0,0.2);backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(115%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(115%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 32px 64px rgba(0,0,0,0.6),0 12px 24px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}@keyframes notification-slide-in{0%{opacity:0;transform:translateX(100%) translateY(-20px) scale(0.9);backdrop-filter:blur(0)}50%{opacity:.8;transform:translateX(20px) translateY(-10px) scale(1.05);backdrop-filter:blur(20px)}100%{opacity:1;transform:translateX(0) translateY(0) scale(1);backdrop-filter:blur(40px)}}@keyframes notification-slide-out{0%{opacity:1;transform:translateX(0) translateY(0) scale(1)}100%{opacity:0;transform:translateX(100%) translateY(-20px) scale(0.9)}}@keyframes notification-slide-left{0%{opacity:0;transform:translateX(-100%) translateY(-20px) scale(0.9);backdrop-filter:blur(0)}50%{opacity:.8;transform:translateX(-20px) translateY(-10px) scale(1.05);backdrop-filter:blur(20px)}100%{opacity:1;transform:translateX(0) translateY(0) scale(1);backdrop-filter:blur(40px)}}@keyframes alert-fade-in{0%{opacity:0;transform:translateY(-20px) scale(0.95)}100%{opacity:1;transform:translateY(0) scale(1)}}.notification.hiding{animation:notification-slide-out .4s cubic-bezier(0.4,0,0.2,1) forwards}.notification-icon{@apply flex items-center justify-center w-8 h-8 rounded-full mr-3 flex-shrink-0;background:rgba(255,255,255,0.2);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.3);box-shadow:0 8px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.4)}.notification-content{@apply flex-1}.notification-title{@apply font-semibold text-sm mb-1}.notification-message{@apply text-sm opacity-90}.notification-close{@apply ml-3 p-1 rounded-lg opacity-70 hover:opacity-100 transition-opacity;background:rgba(255,255,255,0.1);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.2)}.notification-close:hover{background:rgba(255,255,255,0.2);transform:scale(1.1)}.notifications-container{@apply fixed top-4 right-4 z-50 space-y-3 max-w-md}.notifications-container-left{@apply fixed top-4 left-4 z-50 space-y-3 max-w-sm}.flash-message-light{background:linear-gradient(135deg,rgba(255,255,255,0.95) 0,rgba(248,250,252,0.9) 100%);backdrop-filter:blur(32px) saturate(200%) brightness(120%);-webkit-backdrop-filter:blur(32px) saturate(200%) brightness(120%);border:1px solid rgba(226,232,240,0.6);box-shadow:0 25px 50px rgba(0,0,0,0.1),0 12px 24px rgba(0,115,206,0.05),inset 0 1px 0 rgba(255,255,255,0.8);color:var(--color-text-primary)}.flash-message-light.success{border-left:4px solid #10b981;background:linear-gradient(135deg,rgba(236,253,245,0.95) 0,rgba(209,250,229,0.9) 100%)}.flash-message-light.error{border-left:4px solid #ef4444;background:linear-gradient(135deg,rgba(254,242,242,0.95) 0,rgba(252,165,165,0.9) 100%)}.flash-message-light.warning{border-left:4px solid #fbbf24;background:linear-gradient(135deg,rgba(255,251,235,0.95) 0,rgba(254,243,199,0.9) 100%)}.flash-message-light.info{border-left:4px solid #3b82f6;background:linear-gradient(135deg,rgba(239,246,255,0.95) 0,rgba(219,234,254,0.9) 100%)}.table-enhanced{background:var(--gradient-card);border:1px solid var(--color-border-primary);border-radius:var(--card-radius);overflow:hidden;box-shadow:0 4px 20px var(--color-shadow),0 2px 8px rgba(0,115,206,0.04),inset 0 1px 0 rgba(255,255,255,0.6)}.table-enhanced th{background:linear-gradient(135deg,var(--color-bg-secondary) 0,var(--color-bg-tertiary) 100%);color:var(--color-text-primary);font-weight:600;padding:1rem 1.5rem;border-bottom:1px solid var(--color-border-primary);position:relative}.table-enhanced th::after{content:'';position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent 0,var(--color-border-secondary) 50%,transparent 100%)}.table-enhanced td{padding:1rem 1.5rem;border-bottom:1px solid var(--color-border-primary);color:var(--color-text-secondary);transition:all .2s ease}.table-enhanced tbody tr:hover{background:var(--color-bg-secondary);transform:scale(1.002)}.dark .table-enhanced{background:rgba(10,10,10,0.8);border-color:var(--color-border-primary)}.dark .table-enhanced th{background:rgba(26,26,26,0.8);color:var(--color-text-primary)}.dark .table-enhanced tbody tr:hover{background:rgba(26,26,26,0.6)}.modal-enhanced{background:linear-gradient(135deg,rgba(255,255,255,0.98) 0,rgba(248,250,252,0.95) 50%,rgba(255,255,255,0.98) 100%);backdrop-filter:blur(32px) saturate(220%) brightness(120%);-webkit-backdrop-filter:blur(32px) saturate(220%) brightness(120%);border:1px solid rgba(226,232,240,0.7);border-radius:1.5rem;box-shadow:0 50px 100px rgba(0,0,0,0.15),0 20px 40px rgba(0,115,206,0.08),inset 0 2px 0 rgba(255,255,255,0.9);position:relative;overflow:hidden}.modal-enhanced::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent 0,rgba(226,232,240,0.8) 50%,transparent 100%)}.dark .modal-enhanced{background:rgba(0,0,0,0.95);border-color:rgba(42,42,42,0.7);box-shadow:0 50px 100px rgba(0,0,0,0.5),inset 0 2px 0 rgba(255,255,255,0.05)}.status-badge-enhanced{display:inline-flex;align-items:center;padding:.5rem 1rem;font-size:.75rem;font-weight:700;border-radius:9999px;text-transform:uppercase;letter-spacing:.05em;border:1px solid transparent;transition:all .2s ease;position:relative;overflow:hidden}.status-badge-enhanced::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.3),transparent);transition:left .5s ease}.status-badge-enhanced:hover::before{left:100%}.status-online-enhanced{background:linear-gradient(135deg,#ecfdf5 0,#a7f3d0 100%);color:#065f46;border-color:rgba(16,185,129,0.3)}.status-offline-enhanced{background:linear-gradient(135deg,#fef2f2 0,#fca5a5 100%);color:#991b1b;border-color:rgba(239,68,68,0.3)}.status-printing-enhanced{background:linear-gradient(135deg,#eff6ff 0,#bfdbfe 100%);color:#1e40af;border-color:rgba(59,130,246,0.3)}.dark-mode-toggle-new{position:relative;display:flex;cursor:pointer;align-items:center;justify-content:center;border-radius:9999px;padding:.625rem;transition:all .3s cubic-bezier(0.4,0,0.2,1);background:linear-gradient(135deg,rgba(248,250,252,0.9) 0,rgba(241,245,249,0.8) 100%);border:1px solid rgba(226,232,240,0.7);box-shadow:0 4px 12px rgba(0,0,0,0.06),0 2px 4px rgba(0,115,206,0.04),inset 0 1px 0 rgba(255,255,255,0.8);color:var(--color-text-secondary);z-index:100}.dark-mode-toggle-new:hover{transform:translateY(-2px) scale(1.05);background:linear-gradient(135deg,rgba(248,250,252,0.95) 0,rgba(241,245,249,0.85) 100%);box-shadow:0 8px 20px rgba(0,0,0,0.1),0 4px 8px rgba(0,115,206,0.08),inset 0 1px 0 rgba(255,255,255,0.9)}.dark-mode-toggle-new:active{transform:translateY(-1px) scale(0.98);transition:transform .1s}.dark .dark-mode-toggle-new{background:rgba(10,10,10,0.8);border:1px solid rgba(42,42,42,0.6);box-shadow:0 4px 12px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.05);color:var(--color-text-secondary)}.dark .dark-mode-toggle-new:hover{background:rgba(10,10,10,0.9);box-shadow:0 8px 20px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.08)}.dark-mode-toggle-new .sun-icon,.dark-mode-toggle-new .moon-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);transition:all .3s cubic-bezier(0.4,0,0.2,1)}.dark-mode-toggle-new .sun-icon:not(.hidden){animation:icon-appear .5s cubic-bezier(0.25,1,0.5,1) forwards}.dark-mode-toggle-new .moon-icon:not(.hidden){animation:icon-appear .5s cubic-bezier(0.25,1,0.5,1) forwards}@keyframes icon-appear{0%{opacity:0;transform:translate(-50%,-50%) scale(0.5) rotate(-20deg)}100%{opacity:1;transform:translate(-50%,-50%) scale(1) rotate(0)}}.dark .sun-icon{display:none}.dark .moon-icon{display:block}.sun-icon{display:block}.moon-icon{display:none}.user-menu-button-new{display:flex;align-items:center;gap:.5rem;border-radius:.75rem;padding:.5rem;transition:all .3s cubic-bezier(0.4,0,0.2,1);background:linear-gradient(135deg,rgba(248,250,252,0.8) 0,rgba(241,245,249,0.7) 100%);border:1px solid rgba(226,232,240,0.6);box-shadow:0 2px 8px rgba(0,0,0,0.05),inset 0 1px 0 rgba(255,255,255,0.7)}.user-menu-button-new:hover{transform:translateY(-1px);background:linear-gradient(135deg,rgba(248,250,252,0.9) 0,rgba(241,245,249,0.8) 100%);box-shadow:0 4px 12px rgba(0,0,0,0.08),0 2px 4px rgba(0,115,206,0.04),inset 0 1px 0 rgba(255,255,255,0.8)}.dark .user-menu-button-new{background:rgba(10,10,10,0.7);border-color:rgba(42,42,42,0.6);box-shadow:0 2px 8px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.03)}.dark .user-menu-button-new:hover{background:rgba(10,10,10,0.8);box-shadow:0 4px 12px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.05)}.hover-lift-enhanced{transition:all .3s cubic-bezier(0.4,0,0.2,1)}.hover-lift-enhanced:hover{transform:translateY(-3px) scale(1.01);box-shadow:0 12px 30px var(--color-shadow-strong),0 6px 15px var(--color-shadow-accent)}.dark .hover-lift-enhanced:hover{box-shadow:0 12px 30px var(--color-shadow)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--color-bg-secondary);border-radius:4px}::-webkit-scrollbar-thumb{background:linear-gradient(180deg,var(--color-border-secondary) 0,var(--color-border-primary) 100%);border-radius:4px;transition:background .2s ease}::-webkit-scrollbar-thumb:hover{background:linear-gradient(180deg,var(--color-accent) 0,var(--color-accent-hover) 100%)}.dark ::-webkit-scrollbar-track{background:var(--color-bg-secondary)}.dark ::-webkit-scrollbar-thumb{background:var(--color-border-primary)}.dark ::-webkit-scrollbar-thumb:hover{background:#60a5fa}.loading-enhanced{position:relative;overflow:hidden}.loading-enhanced::after{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,115,206,0.1),transparent);animation:loading-shimmer 2s infinite}@keyframes loading-shimmer{0%{left:-100%}100%{left:100%}}.focus-enhanced:focus{outline:2px solid var(--color-accent);outline-offset:2px;box-shadow:0 0 0 4px rgba(0,115,206,0.15),0 4px 12px var(--color-shadow-accent)}.dark .focus-enhanced:focus{outline-color:#60a5fa;box-shadow:0 0 0 4px rgba(96,165,250,0.15),0 4px 12px rgba(96,165,250,0.2)}@media(max-width:768px){.card-enhanced{padding:1rem;border-radius:.75rem}.btn-enhanced{padding:.75rem 1.5rem;font-size:.8rem}.modal-enhanced{border-radius:1rem;margin:1rem}.dark-mode-toggle-new{padding:.5rem}}@media(prefers-reduced-motion:reduce){*{transition:none !important;animation:none !important}}@media(prefers-contrast:high){:root{--color-shadow:rgba(0,0,0,0.2);--color-shadow-strong:rgba(0,0,0,0.3);--color-border-primary:#000}.dark{--color-border-primary:#fff}}}.flash-message{@apply fixed top-4 right-4 px-6 py-4 rounded-2xl text-sm font-medium shadow-2xl transform transition-all duration-500 z-50 border;background:rgba(255,255,255,0.08);backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);-webkit-backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 32px 64px rgba(0,0,0,0.25),0 12px 24px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.1);animation:flash-slide-in .5s cubic-bezier(0.4,0,0.2,1);transition:all .5s cubic-bezier(0.4,0,0.2,1)}.dark .flash-message{background:rgba(0,0,0,0.2);backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(115%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(115%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 32px 64px rgba(0,0,0,0.6),0 12px 24px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}.flash-message:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 40px 80px rgba(0,0,0,0.3),0 16px 32px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.5),0 0 0 1px rgba(255,255,255,0.15)}.dark .flash-message:hover{box-shadow:0 40px 80px rgba(0,0,0,0.7),0 16px 32px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.1)}.flash-message.info{@apply text-blue-100;background:linear-gradient(135deg,rgba(59,130,246,0.2) 0,rgba(147,197,253,0.15) 50%,rgba(59,130,246,0.1) 100%);border:1px solid rgba(59,130,246,0.3)}.flash-message.success{@apply text-green-100;background:linear-gradient(135deg,rgba(34,197,94,0.2) 0,rgba(134,239,172,0.15) 50%,rgba(34,197,94,0.1) 100%);border:1px solid rgba(34,197,94,0.3)}.flash-message.warning{@apply text-yellow-100;background:linear-gradient(135deg,rgba(245,158,11,0.2) 0,rgba(252,211,77,0.15) 50%,rgba(245,158,11,0.1) 100%);border:1px solid rgba(245,158,11,0.3)}.flash-message.error{@apply text-red-100;background:linear-gradient(135deg,rgba(239,68,68,0.2) 0,rgba(252,165,165,0.15) 50%,rgba(239,68,68,0.1) 100%);border:1px solid rgba(239,68,68,0.3)}@keyframes flash-slide-in{0%{opacity:0;transform:translateX(100%) translateY(-20px) scale(0.9);backdrop-filter:blur(0)}50%{opacity:.8;transform:translateX(20px) translateY(-10px) scale(1.05);backdrop-filter:blur(20px)}100%{opacity:1;transform:translateX(0) translateY(0) scale(1);backdrop-filter:blur(40px)}}@keyframes flash-slide-out{0%{opacity:1;transform:translateX(0) translateY(0) scale(1)}100%{opacity:0;transform:translateX(100%) translateY(-20px) scale(0.9)}}.flash-message.hiding{animation:flash-slide-out .4s cubic-bezier(0.4,0,0.2,1) forwards}.dnd-toggle{@apply relative inline-flex items-center h-6 rounded-full w-11 transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;background:rgba(156,163,175,0.3);backdrop-filter:blur(10px);border:1px solid rgba(156,163,175,0.2)}.dnd-toggle.active{background:rgba(239,68,68,0.3);border:1px solid rgba(239,68,68,0.4)}.dnd-toggle-slider{@apply inline-block h-4 w-4 rounded-full shadow-lg transform transition-transform duration-300;background:rgba(255,255,255,0.9);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.3);box-shadow:0 4px 8px rgba(0,0,0,0.2),0 2px 4px rgba(0,0,0,0.1);margin:.125rem}.dnd-toggle.active .dnd-toggle-slider{transform:translateX(1.25rem);background:rgba(255,255,255,1);box-shadow:0 6px 12px rgba(239,68,68,0.3),0 3px 6px rgba(239,68,68,0.2)}.dnd-indicator{@apply fixed top-4 left-4 z-50 flex items-center px-3 py-2 rounded-lg text-sm font-medium transition-all duration-300;background:rgba(239,68,68,0.1);backdrop-filter:blur(20px) saturate(150%);-webkit-backdrop-filter:blur(20px) saturate(150%);border:1px solid rgba(239,68,68,0.3);color:#ef4444;transform:translateY(-100%);opacity:0}.dnd-indicator.active{transform:translateY(0);opacity:1}.dnd-modal{@apply fixed inset-0 z-50 flex items-center justify-center p-4;background:rgba(0,0,0,0.3);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}.dnd-modal-content{@apply w-full max-w-md rounded-2xl p-6 shadow-2xl transform transition-all;background:rgba(255,255,255,0.1);backdrop-filter:blur(40px) saturate(200%) brightness(120%);-webkit-backdrop-filter:blur(40px) saturate(200%) brightness(120%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 25px 50px rgba(0,0,0,0.25),0 8px 16px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.4)}.dark .dnd-modal-content{background:rgba(0,0,0,0.3);backdrop-filter:blur(40px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(110%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 25px 50px rgba(0,0,0,0.6),0 8px 16px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2)}.flash-message.dnd-suppressed{animation:flash-fade-in .3s ease-out;opacity:.3;transform:scale(0.95);pointer-events:none}@keyframes flash-fade-in{0%{opacity:0;transform:scale(0.9)}100%{opacity:.3;transform:scale(0.95)}}.dnd-counter{@apply absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-bold;background:rgba(239,68,68,0.9);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.2);box-shadow:0 2px 4px rgba(0,0,0,0.2);animation:dnd-counter-bounce .5s ease-out}@keyframes dnd-counter-bounce{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}}@keyframes slide-down{0%{opacity:0;transform:translateY(-20px)}100%{opacity:1;transform:translateY(0)}}.mercedes-background::before{content:'';position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;background-image:url("data:image/svg+xml,%3Csvgxmlns='http://www.w3.org/2000/svg'viewBox='008080'width='80'height='80'opacity='0.03'fill='currentColor'%3E%3Cpathd='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.5C27,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.5zM4,40c0-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,40zM58.6,70.8C53.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.9C67.6,63.9,63.4,67.9,58.6,70.8zM58.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,40c0,6.2-1.5,12-4.3,17.1L58.6,46.8z'/%3E%3C/svg%3E");background-position:center;background-repeat:repeat;background-size:120px 120px;pointer-events:none;opacity:.03;transition:opacity .3s ease}.dark .mercedes-background::before{opacity:.015;filter:invert(1) brightness(0.3);background-size:150px 150px}@layer components{.btn-primary{@apply text-white dark:text-slate-900 px-4 py-2 rounded-lg transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 shadow-2xl hover:-translate-y-0.5;background:rgba(0,0,0,0.7);backdrop-filter:blur(20px) saturate(150%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(150%) brightness(110%);border:1px solid rgba(255,255,255,0.2);box-shadow:0 20px 40px rgba(0,0,0,0.3),0 8px 16px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.1)}.btn-primary:hover{background:rgba(0,0,0,0.9);backdrop-filter:blur(25px) saturate(180%) brightness(120%);-webkit-backdrop-filter:blur(25px) saturate(180%) brightness(120%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 25px 50px rgba(0,0,0,0.4),0 10px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.3)}.dark .btn-primary{background:rgba(255,255,255,0.7);border:1px solid rgba(0,0,0,0.1);box-shadow:0 20px 40px rgba(0,0,0,0.2),0 8px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.8),0 0 0 1px rgba(0,0,0,0.05)}.dark .btn-primary:hover{background:rgba(255,255,255,0.9);border:1px solid rgba(0,0,0,0.15);box-shadow:0 25px 50px rgba(0,0,0,0.3),0 10px 20px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.9)}.btn-secondary{@apply text-slate-900 dark:text-white px-4 py-2 rounded-lg transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 shadow-2xl hover:-translate-y-0.5;background:rgba(255,255,255,0.3);backdrop-filter:blur(20px) saturate(150%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(150%) brightness(110%);border:1px solid rgba(255,255,255,0.4);box-shadow:0 20px 40px rgba(0,0,0,0.15),0 8px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.5),0 0 0 1px rgba(255,255,255,0.2)}.btn-secondary:hover{background:rgba(255,255,255,0.5);backdrop-filter:blur(25px) saturate(180%) brightness(120%);-webkit-backdrop-filter:blur(25px) saturate(180%) brightness(120%);border:1px solid rgba(255,255,255,0.6);box-shadow:0 25px 50px rgba(0,0,0,0.2),0 10px 20px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.7)}.dark .btn-secondary{background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.2);box-shadow:0 20px 40px rgba(0,0,0,0.3),0 8px 16px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.1)}.dark .btn-secondary:hover{background:rgba(0,0,0,0.6);border:1px solid rgba(255,255,255,0.3);box-shadow:0 25px 50px rgba(0,0,0,0.4),0 10px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.3)}.btn-outline{@apply border-2 border-black/70 hover:bg-black/70 dark:border-white/70 dark:hover:bg-white/70 text-black hover:text-white dark:text-white dark:hover:text-slate-900 px-4 py-2 rounded-lg transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 backdrop-blur-lg;backdrop-filter:blur(16px) saturate(150%);-webkit-backdrop-filter:blur(16px) saturate(150%);box-shadow:0 15px 30px rgba(0,0,0,0.1),0 0 0 1px rgba(255,255,255,0.05)}.glass-card{@apply rounded-xl p-6 shadow-2xl transition-all duration-300;background:rgba(255,255,255,0.15);backdrop-filter:blur(30px) saturate(200%) brightness(120%) contrast(110%);-webkit-backdrop-filter:blur(30px) saturate(200%) brightness(120%) contrast(110%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 8px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.1);border-radius:var(--card-radius)}.dark .glass-card{background:rgba(0,0,0,0.3);backdrop-filter:blur(30px) saturate(180%) brightness(110%) contrast(120%);-webkit-backdrop-filter:blur(30px) saturate(180%) brightness(110%) contrast(120%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 25px 50px rgba(0,0,0,0.4),0 8px 16px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.15),0 0 0 1px rgba(255,255,255,0.05)}.dashboard-card{@apply rounded-xl p-6 shadow-2xl transition-all duration-300 hover:-translate-y-1;background:rgba(255,255,255,0.12);backdrop-filter:blur(35px) saturate(200%) brightness(125%) contrast(115%);-webkit-backdrop-filter:blur(35px) saturate(200%) brightness(125%) contrast(115%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 8px 16px rgba(0,0,0,0.08),inset 0 1px 0 rgba(255,255,255,0.25),0 0 0 1px rgba(255,255,255,0.1);border-radius:var(--card-radius)}.dark .dashboard-card{background:rgba(0,0,0,0.35);backdrop-filter:blur(35px) saturate(180%) brightness(115%) contrast(125%);-webkit-backdrop-filter:blur(35px) saturate(180%) brightness(115%) contrast(125%);border:1px solid rgba(255,255,255,0.12);box-shadow:0 25px 50px rgba(0,0,0,0.5),0 8px 16px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.12),0 0 0 1px rgba(255,255,255,0.05)}.nav-link{@apply flex items-center px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50 hover:shadow-md}.nav-link.active{@apply text-slate-900 dark:text-white bg-slate-100 dark:bg-black shadow-sm}.navbar{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;background:rgba(255,255,255,0.1);backdrop-filter:blur(10px);border-radius:10px;box-shadow:0 4px 6px rgba(0,0,0,0.1);transition:all .3s ease}.navbar-button{padding:.25rem .5rem;font-size:.875rem;border-radius:5px;transition:background-color .3s ease}.navbar-button:hover{background-color:rgba(255,255,255,0.2)}@media(max-width:768px){.navbar{flex-direction:column;padding:.25rem}.navbar-button{margin:.25rem 0}}.dark .navbar{background:rgba(0,0,0,0.25);backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(120%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(120%);box-shadow:0 8px 32px rgba(0,0,0,0.6),0 2px 8px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.1),0 0 0 1px rgba(255,255,255,0.05);border-bottom:1px solid rgba(255,255,255,0.1)}.navbar-brand{@apply flex items-center space-x-2 transition-transform duration-300 hover:scale-105}.navbar-menu{@apply flex items-center justify-center space-x-1 md:space-x-3 lg:space-x-6 p-3 mx-4 rounded-2xl border;background:rgba(255,255,255,0.25);backdrop-filter:blur(20px) saturate(150%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(150%) brightness(110%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 4px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.1)}.dark .navbar-menu{background:rgba(0,0,0,0.4);backdrop-filter:blur(20px) saturate(150%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(150%) brightness(110%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 4px 16px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}.navbar-button{@apply p-2 rounded-full transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2}.user-menu-button{@apply flex items-center space-x-2 rounded-lg p-1 transition-all duration-300 hover:bg-gray-100/80 dark:hover:bg-slate-700/60 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2}.user-avatar{@apply w-10 h-10 bg-black dark:bg-white text-white dark:text-slate-900 rounded-full flex items-center justify-center font-bold text-sm shadow-md transition-all duration-300 hover:shadow-lg}.avatar-large{@apply w-14 h-14 bg-black dark:bg-white text-white dark:text-slate-900 rounded-full flex items-center justify-center font-bold text-lg shadow-md}.user-dropdown-item{@apply flex items-center px-4 py-3 text-sm text-slate-700 dark:text-slate-300 hover:bg-gray-100/80 dark:hover:bg-slate-700/60 hover:text-slate-900 dark:hover:text-white transition-all duration-300 focus:outline-none focus:bg-gray-100/80 dark:focus:bg-slate-700/60}.user-dropdown-separator{@apply border-t border-gray-200/80 dark:border-slate-700/30 my-1}.menu-item{@apply flex items-center space-x-2 px-4 py-2.5 text-slate-700 dark:text-slate-300 rounded-xl transition-all duration-300;background:rgba(255,255,255,0.1);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.2);box-shadow:0 2px 8px rgba(0,0,0,0.05)}.menu-item:hover{@apply text-slate-900 dark:text-white;background:rgba(255,255,255,0.3);backdrop-filter:blur(15px) saturate(150%);-webkit-backdrop-filter:blur(15px) saturate(150%);border:1px solid rgba(255,255,255,0.4);box-shadow:0 4px 16px rgba(0,0,0,0.1);transform:translateY(-1px)}.dark .menu-item{background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.1);box-shadow:0 2px 8px rgba(0,0,0,0.2)}.dark .menu-item:hover{background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.2);box-shadow:0 4px 16px rgba(0,0,0,0.3)}.menu-item.active{@apply text-slate-900 dark:text-white font-medium;background:rgba(255,255,255,0.5);backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border:1px solid rgba(255,255,255,0.6);box-shadow:0 4px 16px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.5)}.dark .menu-item.active{background:rgba(0,0,0,0.6);border:1px solid rgba(255,255,255,0.3);box-shadow:0 4px 16px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2)}.user-dropdown{@apply absolute right-0 mt-2 w-64 rounded-xl shadow-2xl z-50 overflow-hidden;background:rgba(255,255,255,0.1);backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);-webkit-backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 25px 50px rgba(0,0,0,0.25),0 8px 16px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.1);animation:fadeIn .2s ease-out forwards}.dark .user-dropdown{background:rgba(0,0,0,0.4);backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(120%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(120%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 25px 50px rgba(0,0,0,0.6),0 8px 16px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}.dropdown-header{@apply flex items-center p-4 border-b border-gray-200/80 dark:border-slate-700/30}.dropdown-item{@apply flex items-center gap-3 px-4 py-3 text-sm text-slate-700 dark:text-slate-300 hover:bg-gray-100/80 dark:hover:bg-slate-700/60 hover:text-slate-900 dark:hover:text-white transition-all duration-300}.dropdown-divider{@apply border-t border-gray-200/80 dark:border-slate-700/30}@keyframes mercedes-rotate{0%{transform:rotate(0)}25%{transform:rotate(90deg)}50%{transform:rotate(180deg)}75%{transform:rotate(270deg)}100%{transform:rotate(360deg)}}.navbar-brand:hover svg{animation:mercedes-rotate 5s infinite linear;transform-origin:center}}.navbar{position:-webkit-sticky !important;position:sticky !important;top:0 !important;z-index:50 !important;width:100% !important;left:0 !important;right:0 !important;--navbar-blur:40px;--navbar-opacity:.15;background:rgba(255,255,255,var(--navbar-opacity,0.15)) !important;backdrop-filter:blur(var(--navbar-blur,40px)) saturate(200%) brightness(110%) contrast(105%) !important;-webkit-backdrop-filter:blur(var(--navbar-blur,40px)) saturate(200%) brightness(110%) contrast(105%) !important;box-shadow:0 8px 32px rgba(0,0,0,0.12),0 2px 8px rgba(0,0,0,0.08),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.15) !important;border-bottom:1px solid rgba(255,255,255,0.2) !important;transition:all .3s cubic-bezier(0.4,0,0.2,1) !important}.dark .navbar{--navbar-dark-opacity:.25;background:rgba(0,0,0,var(--navbar-dark-opacity,0.25)) !important;backdrop-filter:blur(calc(var(--navbar-blur, 40px) + 5px)) saturate(180%) brightness(120%) contrast(115%) !important;-webkit-backdrop-filter:blur(calc(var(--navbar-blur, 40px) + 5px)) saturate(180%) brightness(120%) contrast(115%) !important;box-shadow:0 8px 32px rgba(0,0,0,0.4),0 2px 8px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.15),0 0 0 1px rgba(255,255,255,0.08) !important;border-bottom:1px solid rgba(255,255,255,0.1) !important}.navbar.scrolled{--navbar-blur:50px;--navbar-opacity:.25;background:rgba(255,255,255,var(--navbar-opacity,0.25)) !important;backdrop-filter:blur(var(--navbar-blur,50px)) saturate(220%) brightness(115%) contrast(110%) !important;-webkit-backdrop-filter:blur(var(--navbar-blur,50px)) saturate(220%) brightness(115%) contrast(110%) !important;box-shadow:0 12px 40px rgba(0,0,0,0.15),0 4px 12px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.2) !important}.dark .navbar.scrolled{--navbar-dark-opacity:.35;background:rgba(0,0,0,var(--navbar-dark-opacity,0.35)) !important;backdrop-filter:blur(calc(var(--navbar-blur, 50px) + 5px)) saturate(200%) brightness(125%) contrast(120%) !important;-webkit-backdrop-filter:blur(calc(var(--navbar-blur, 50px) + 5px)) saturate(200%) brightness(125%) contrast(120%) !important;box-shadow:0 12px 40px rgba(0,0,0,0.5),0 4px 12px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.1) !important}.navbar-menu-new{@apply flex items-center justify-center space-x-0.5 md:space-x-1;max-width:100%;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;background:rgba(255,255,255,0.1);backdrop-filter:blur(25px) saturate(170%) brightness(108%);-webkit-backdrop-filter:blur(25px) saturate(170%) brightness(108%);border-radius:16px;padding:8px;margin:0 16px;border:1px solid rgba(255,255,255,0.15);box-shadow:0 6px 20px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05);transition:all .3s cubic-bezier(0.4,0,0.2,1)}.dark .navbar-menu-new{background:rgba(0,0,0,0.2);backdrop-filter:blur(30px) saturate(150%) brightness(115%);-webkit-backdrop-filter:blur(30px) saturate(150%) brightness(115%);border:1px solid rgba(255,255,255,0.1);box-shadow:0 6px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.1),0 0 0 1px rgba(255,255,255,0.03)}.navbar-menu-new::-webkit-scrollbar{display:none}.navbar-menu-new:hover{backdrop-filter:blur(35px) saturate(190%) brightness(112%);-webkit-backdrop-filter:blur(35px) saturate(190%) brightness(112%);box-shadow:0 8px 25px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.1);transform:translateY(-1px)}.dark .navbar-menu-new:hover{backdrop-filter:blur(40px) saturate(170%) brightness(120%);-webkit-backdrop-filter:blur(40px) saturate(170%) brightness(120%);box-shadow:0 8px 25px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.15),0 0 0 1px rgba(255,255,255,0.05)}.nav-item{@apply flex items-center space-x-1.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-300;color:rgba(15,23,42,0.85);background:rgba(255,255,255,0.08);backdrop-filter:blur(15px) saturate(140%);-webkit-backdrop-filter:blur(15px) saturate(140%);border:1px solid rgba(255,255,255,0.1);box-shadow:0 4px 12px rgba(0,0,0,0.05),inset 0 1px 0 rgba(255,255,255,0.15);position:relative;overflow:hidden;animation:nav-item-entrance .6s ease-out}.nav-item::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 .5s}.nav-item:hover::before{left:100%}.nav-item::after{content:'';position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:conic-gradient(from 0 at 50% 50%,transparent 0,rgba(255,255,255,0.1) 30deg,transparent 60deg);opacity:0;transition:opacity .3s ease;pointer-events:none;animation:rotate 3s linear infinite}.nav-item:hover::after{opacity:1}.dark .nav-item{color:rgba(255,255,255,0.85);background:rgba(0,0,0,0.15);backdrop-filter:blur(20px) saturate(130%);-webkit-backdrop-filter:blur(20px) saturate(130%);border:1px solid rgba(255,255,255,0.08);box-shadow:0 4px 12px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.08)}.nav-item:hover{color:rgba(15,23,42,1);background:rgba(255,255,255,0.2);backdrop-filter:blur(25px) saturate(160%) brightness(110%);-webkit-backdrop-filter:blur(25px) saturate(160%) brightness(110%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 8px 20px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.1);transform:translateY(-2px) scale(1.02)}.dark .nav-item:hover{color:rgba(255,255,255,1);background:rgba(0,0,0,0.25);backdrop-filter:blur(30px) saturate(150%) brightness(120%);-webkit-backdrop-filter:blur(30px) saturate(150%) brightness(120%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 8px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.15),0 0 0 1px rgba(255,255,255,0.05)}.nav-item.active{color:rgba(15,23,42,1);background:rgba(255,255,255,0.35);backdrop-filter:blur(35px) saturate(180%) brightness(115%);-webkit-backdrop-filter:blur(35px) saturate(180%) brightness(115%);border:1px solid rgba(255,255,255,0.4);box-shadow:0 12px 24px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.5),0 0 0 1px rgba(59,130,246,0.3);transform:translateY(-1px);animation:nav-item-active-glow 2s ease-in-out infinite alternate}.dark .nav-item.active{color:rgba(255,255,255,1);background:rgba(0,0,0,0.4);backdrop-filter:blur(40px) saturate(160%) brightness(125%);-webkit-backdrop-filter:blur(40px) saturate(160%) brightness(125%);border:1px solid rgba(255,255,255,0.2);box-shadow:0 12px 24px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(59,130,246,0.2)}@keyframes nav-item-entrance{0%{opacity:0;transform:translateY(10px) scale(0.95);backdrop-filter:blur(5px)}100%{opacity:1;transform:translateY(0) scale(1);backdrop-filter:blur(15px) saturate(140%)}}@keyframes nav-item-active-glow{0%{box-shadow:0 12px 24px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.5),0 0 0 1px rgba(59,130,246,0.3)}100%{box-shadow:0 16px 32px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.6),0 0 0 2px rgba(59,130,246,0.5)}}@keyframes rotate{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.navbar::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at 20% 50%,rgba(255,255,255,0.1) 1px,transparent 1px),radial-gradient(circle at 80% 50%,rgba(255,255,255,0.1) 1px,transparent 1px),radial-gradient(circle at 40% 20%,rgba(255,255,255,0.05) 1px,transparent 1px),radial-gradient(circle at 60% 80%,rgba(255,255,255,0.05) 1px,transparent 1px);opacity:0;animation:glassmorphism-particles 8s ease-in-out infinite;pointer-events:none}.dark .navbar::before{background:radial-gradient(circle at 20% 50%,rgba(255,255,255,0.05) 1px,transparent 1px),radial-gradient(circle at 80% 50%,rgba(255,255,255,0.05) 1px,transparent 1px),radial-gradient(circle at 40% 20%,rgba(255,255,255,0.03) 1px,transparent 1px),radial-gradient(circle at 60% 80%,rgba(255,255,255,0.03) 1px,transparent 1px)}@keyframes glassmorphism-particles{0%,100%{opacity:0;transform:scale(1)}50%{opacity:1;transform:scale(1.1)}}.dark-mode-toggle-new{@apply relative p-2 rounded-full flex items-center justify-center transition-all duration-300 cursor-pointer;background:rgba(241,245,249,0.8);border:1px solid rgba(255,255,255,0.7);box-shadow:0 2px 8px rgba(0,0,0,0.05),0 1px 2px rgba(0,0,0,0.04);color:#334155;z-index:100}.dark-mode-toggle-new:hover{@apply transform -translate-y-0.5;background:rgba(241,245,249,0.9);box-shadow:0 8px 16px rgba(0,0,0,0.08),0 2px 4px rgba(0,0,0,0.06)}.dark-mode-toggle-new:active{@apply transform scale-95;transition:transform .1s}.dark .dark-mode-toggle-new{background:rgba(30,41,59,0.8);border:1px solid rgba(255,255,255,0.1);box-shadow:0 2px 8px rgba(0,0,0,0.2),0 1px 2px rgba(0,0,0,0.1);color:#e2e8f0}.dark .dark-mode-toggle-new:hover{background:rgba(30,41,59,0.9);box-shadow:0 8px 16px rgba(0,0,0,0.2),0 2px 4px rgba(0,0,0,0.15)}.dark-mode-toggle-new .sun-icon,.dark-mode-toggle-new .moon-icon{@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transition-all duration-300}.dark-mode-toggle-new .sun-icon:not(.hidden){animation:spin-in .5s cubic-bezier(0.25,1,0.5,1) forwards}.dark-mode-toggle-new .moon-icon:not(.hidden){animation:spin-in .5s cubic-bezier(0.25,1,0.5,1) forwards}@keyframes spin-in{0%{opacity:0;transform:translateY(10px) scale(0.7) rotate(20deg)}100%{opacity:1;transform:translateY(0) scale(1) rotate(0)}}.dark .sun-icon{display:none}.dark .moon-icon{display:block}.sun-icon{display:block}.moon-icon{display:none}.user-menu-button-new{@apply flex items-center space-x-1.5 rounded-lg p-1 transition-all duration-300;background:rgba(241,245,249,0.6);border:1px solid rgba(255,255,255,0.6);box-shadow:0 2px 8px rgba(0,0,0,0.04),0 1px 2px rgba(0,0,0,0.02)}.user-menu-button-new:hover{@apply transform -translate-y-0.5;background:rgba(241,245,249,0.8);box-shadow:0 8px 16px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04)}.dark .user-menu-button-new{background:rgba(30,41,59,0.6);border:1px solid rgba(255,255,255,0.08);box-shadow:0 2px 8px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.1)}.dark .user-menu-button-new:hover{background:rgba(30,41,59,0.8);box-shadow:0 8px 16px rgba(0,0,0,0.15),0 2px 4px rgba(0,0,0,0.1)}.user-avatar-new{@apply h-7 w-7 rounded-full flex items-center justify-center text-white font-semibold text-xs shadow-md transition-all duration-300;background:linear-gradient(135deg,#000,#333);box-shadow:0 2px 4px rgba(0,0,0,0.2),0 1px 2px rgba(0,0,0,0.1)}.dark .user-avatar-new{background:linear-gradient(135deg,#f8fafc,#e2e8f0);color:#0f172a;box-shadow:0 2px 4px rgba(0,0,0,0.3),0 1px 2px rgba(0,0,0,0.2)}.login-button-new{@apply flex items-center px-3 py-1.5 rounded-lg text-xs font-medium shadow-sm transition-all duration-300;background:#000;color:#fff;border:1px solid rgba(255,255,255,0.1);box-shadow:0 2px 8px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.08)}.login-button-new:hover{@apply transform -translate-y-0.5;background:#333;box-shadow:0 8px 16px rgba(0,0,0,0.15),0 3px 4px rgba(0,0,0,0.1)}.dark .login-button-new{background:#fff;color:#000;border:1px solid rgba(0,0,0,0.1);box-shadow:0 2px 8px rgba(0,0,0,0.2),0 1px 2px rgba(0,0,0,0.15)}.dark .login-button-new:hover{background:#f1f5f9;box-shadow:0 8px 16px rgba(0,0,0,0.25),0 3px 4px rgba(0,0,0,0.2)}.mobile-menu-new{@apply w-full overflow-hidden transition-all duration-300 z-40;background:rgba(255,255,255,0.8);backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);box-shadow:0 4px 20px rgba(0,0,0,0.06);border-bottom:1px solid rgba(241,245,249,0.8);max-height:0;opacity:0}.mobile-menu-new.open{max-height:400px;opacity:1;border-bottom:1px solid rgba(241,245,249,0.8)}.dark .mobile-menu-new{background:rgba(15,23,42,0.8);box-shadow:0 4px 20px rgba(0,0,0,0.2);border-bottom:1px solid rgba(30,41,59,0.8)}.mobile-nav-item{@apply flex items-center space-x-2.5 px-3 py-2.5 rounded-lg text-sm text-slate-800 dark:text-slate-200 transition-all duration-300}.mobile-nav-item:hover{background:rgba(241,245,249,0.8)}.dark .mobile-nav-item:hover{background:rgba(30,41,59,0.6)}.mobile-nav-item.active{background:rgba(241,245,249,0.9);color:#000;font-weight:500}.dark .mobile-nav-item.active{background:rgba(30,41,59,0.8);color:#fff}.mb-stat-card{background:linear-gradient(135deg,rgba(240,249,255,0.6) 0,rgba(230,242,255,0.6) 100%);color:#0f172a;position:relative;overflow:hidden;border:0;border-radius:var(--card-radius);backdrop-filter:blur(20px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(180%) brightness(110%);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 0 0 1px rgba(255,255,255,0.1);padding:1.5rem;margin:1rem;transition:transform .3s ease,box-shadow .3s ease}.dark .mb-stat-card{background:linear-gradient(135deg,rgba(0,0,0,0.7) 0,rgba(10,10,10,0.7) 100%);color:var(--text-primary,#f8fafc);box-shadow:0 25px 50px rgba(0,0,0,0.3),0 0 0 1px rgba(255,255,255,0.05)}.stats-card,.job-card{@apply bg-white/60 dark:bg-black/80 backdrop-blur-2xl border border-gray-200/70 dark:border-slate-700/20 rounded-xl shadow-2xl transition-all duration-300;backdrop-filter:blur(24px) saturate(200%) brightness(120%);-webkit-backdrop-filter:blur(24px) saturate(200%) brightness(120%);box-shadow:0 25px 50px rgba(0,0,0,0.2),0 0 0 1px rgba(255,255,255,0.1);border-radius:var(--card-radius)}footer{@apply transition-all duration-300;background:rgba(255,255,255,0.1);backdrop-filter:blur(30px) saturate(180%) brightness(120%);-webkit-backdrop-filter:blur(30px) saturate(180%) brightness(120%);border-top:1px solid rgba(255,255,255,0.2);box-shadow:0 -8px 32px rgba(0,0,0,0.1),0 -2px 8px rgba(0,0,0,0.05),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}.dark footer{background:rgba(0,0,0,0.3);backdrop-filter:blur(30px) saturate(160%) brightness(110%);-webkit-backdrop-filter:blur(30px) saturate(160%) brightness(110%);border-top:1px solid rgba(255,255,255,0.1);box-shadow:0 -8px 32px rgba(0,0,0,0.3),0 -2px 8px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.1),0 0 0 1px rgba(255,255,255,0.03)}.dropdown-arrow{@apply transition-transform duration-300}.mercedes-star-bg{position:relative}.mercedes-star-bg::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background-image:url("data:image/svg+xml,%3Csvgxmlns='http://www.w3.org/2000/svg'viewBox='008080'width='80'height='80'opacity='0.05'fill='currentColor'%3E%3Cpathd='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.5C27,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.5zM4,40c0-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,40zM58.6,70.8C53.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.9C67.6,63.9,63.4,67.9,58.6,70.8zM58.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,40c0,6.2-1.5,12-4.3,17.1L58.6,46.8z'/%3E%3C/svg%3E");background-position:center;background-repeat:repeat;background-size:40px 40px;z-index:-1;opacity:.05}.dark .mercedes-star-bg::after{opacity:.02;filter:invert(1) brightness(0.4)}.glass-effect{backdrop-filter:blur(20px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(180%) brightness(110%);background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);box-shadow:0 8px 32px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.3)}.dark .glass-effect{background:rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.1);box-shadow:0 8px 32px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.15)}.glass-hover{transition:all .3s cubic-bezier(0.4,0,0.2,1)}.glass-hover:hover{transform:translateY(-2px);backdrop-filter:blur(25px) saturate(200%) brightness(120%);-webkit-backdrop-filter:blur(25px) saturate(200%) brightness(120%);box-shadow:0 20px 40px rgba(0,0,0,0.15),0 8px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.4)}.dark .glass-hover:hover{box-shadow:0 20px 40px rgba(0,0,0,0.4),0 8px 16px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.2)}.printer-card-new{@apply bg-gradient-to-br from-white/90 to-white/70 dark:from-slate-800/90 dark:to-slate-900/70 backdrop-blur-2xl rounded-xl border border-gray-200/70 dark:border-slate-700/30 p-5 shadow-2xl transition-all duration-300 hover:-translate-y-1 relative overflow-hidden;box-shadow:0 20px 40px rgba(0,0,0,0.08),0 10px 20px rgba(0,0,0,0.06),0 0 0 1px rgba(255,255,255,0.1);border-radius:var(--card-radius,1rem)}.dark .printer-card-new{box-shadow:0 20px 40px rgba(0,0,0,0.4),0 10px 20px rgba(0,0,0,0.3),0 0 0 1px rgba(255,255,255,0.05)}.printer-card-new.online{@apply bg-gradient-to-br from-green-50/90 to-emerald-50/80 dark:from-green-900/30 dark:to-emerald-900/20 border-green-200 dark:border-green-700/50;box-shadow:0 20px 40px rgba(0,122,85,0.08),0 10px 20px rgba(0,122,85,0.06),0 0 0 1px rgba(209,250,229,0.4)}.dark .printer-card-new.online{box-shadow:0 20px 40px rgba(0,0,0,0.3),0 10px 20px rgba(0,0,0,0.2),0 0 0 1px rgba(16,185,129,0.2)}.status-badge-new{@apply px-2.5 py-1 rounded-full text-xs font-medium inline-flex items-center space-x-1 shadow-sm;background:rgba(255,255,255,0.9);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:0 2px 5px rgba(0,0,0,0.05)}.dark .status-badge-new{background:rgba(30,41,59,0.7);box-shadow:0 2px 5px rgba(0,0,0,0.2)}.status-badge-new.online{@apply bg-green-100/90 text-green-800 dark:bg-green-900/60 dark:text-green-300}.status-badge-new.offline{@apply bg-red-100/90 text-red-800 dark:bg-red-900/60 dark:text-red-300}.filter-bar-new{@apply bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-lg p-1.5 border border-gray-200/60 dark:border-slate-700/30 shadow-xl;box-shadow:0 10px 25px rgba(0,0,0,0.05),0 5px 10px rgba(0,0,0,0.03),0 0 0 1px rgba(255,255,255,0.2)}.dark .filter-bar-new{box-shadow:0 10px 25px rgba(0,0,0,0.2),0 5px 10px rgba(0,0,0,0.15),0 0 0 1px rgba(255,255,255,0.05)}.filter-btn-new{@apply px-3.5 py-2 text-sm rounded-md transition-all duration-300 font-medium}.filter-btn-new.active{@apply bg-black text-white dark:bg-white dark:text-slate-900 shadow-md;box-shadow:0 4px 10px rgba(0,0,0,0.1)}.dark .filter-btn-new.active{box-shadow:0 4px 10px rgba(0,0,0,0.3)}.action-btn-new{@apply flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-300 shadow-md hover:-translate-y-0.5;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.action-btn-new.primary{@apply bg-indigo-600 hover:bg-indigo-700 text-white dark:bg-indigo-600 dark:hover:bg-indigo-500;box-shadow:0 5px 15px rgba(79,70,229,0.2)}.dark .action-btn-new.primary{box-shadow:0 5px 15px rgba(79,70,229,0.3)}.action-btn-new.success{@apply bg-green-600 hover:bg-green-700 text-white dark:bg-green-600 dark:hover:bg-green-500;box-shadow:0 5px 15px rgba(16,185,129,0.2)}.dark .action-btn-new.success{box-shadow:0 5px 15px rgba(16,185,129,0.3)}.action-btn-new.danger{@apply bg-red-600 hover:bg-red-700 text-white dark:bg-red-600 dark:hover:bg-red-500;box-shadow:0 5px 15px rgba(239,68,68,0.2)}.dark .action-btn-new.danger{box-shadow:0 5px 15px rgba(239,68,68,0.3)}.printer-info-row{@apply flex items-center gap-2 text-xs sm:text-sm text-slate-700 dark:text-slate-300 mb-1.5}.printer-info-icon{@apply w-3.5 h-3.5 sm:w-4 sm:h-4 text-slate-500 dark:text-slate-400 flex-shrink-0}.online-indicator{@apply absolute top-2.5 right-2.5 w-3 h-3 bg-green-500 rounded-full shadow-lg;box-shadow:0 0 0 rgba(16,185,129,0.6);animation:pulse-ring 2s cubic-bezier(0.455,0.03,0.515,0.955) infinite}@keyframes pulse-ring{0%{box-shadow:0 0 0 0 rgba(16,185,129,0.6)}70%{box-shadow:0 0 0 6px rgba(16,185,129,0)}100%{box-shadow:0 0 0 0 rgba(16,185,129,0)}}.status-overview-new{@apply flex flex-wrap gap-3 text-xs sm:text-sm p-3 bg-white/60 dark:bg-slate-800/60 backdrop-blur-xl rounded-lg border border-gray-200/60 dark:border-slate-700/30 shadow-lg;box-shadow:0 10px 25px rgba(0,0,0,0.04),0 5px 10px rgba(0,0,0,0.02),0 0 0 1px rgba(255,255,255,0.1)}.dark .status-overview-new{box-shadow:0 10px 25px rgba(0,0,0,0.15),0 5px 10px rgba(0,0,0,0.1),0 0 0 1px rgba(255,255,255,0.03)}.status-dot{@apply w-2.5 h-2.5 rounded-full}.status-dot.online{@apply bg-green-500;animation:pulse-dot 2s cubic-bezier(0.455,0.03,0.515,0.955) infinite}.status-dot.offline{@apply bg-red-500}@keyframes pulse-dot{0%{transform:scale(0.95);opacity:1}50%{transform:scale(1.1);opacity:.8}100%{transform:scale(0.95);opacity:1}}.modal-new{@apply fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm}.modal-content-new{@apply bg-white/90 dark:bg-slate-800/90 backdrop-blur-2xl rounded-2xl p-6 w-full max-w-md shadow-2xl border border-gray-200/60 dark:border-slate-700/30 transform transition-all duration-300;box-shadow:0 25px 50px rgba(0,0,0,0.15),0 15px 30px rgba(0,0,0,0.1),0 20px 25px -5px rgba(0,0,0,0.5),0 10px 10px -5px rgba(0,0,0,0.3)}.user-dropdown-item{@apply flex items-center px-4 py-3 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all duration-200 cursor-pointer}.user-dropdown-item:first-child{@apply rounded-t-xl}.user-dropdown-item:last-child{@apply rounded-b-xl}.user-dropdown-item:hover{background:rgba(248,250,252,0.8);transform:translateX(2px)}.dark .user-dropdown-item:hover{background:rgba(30,41,59,0.8)}.user-dropdown-icon{@apply w-4 h-4 mr-3 text-slate-500 dark:text-slate-400 transition-colors duration-200}.user-dropdown-item:hover .user-dropdown-icon{@apply text-slate-700 dark:text-slate-200}.user-dropdown-divider{@apply border-t border-slate-200 dark:border-slate-700 my-1}.user-info-section{@apply px-4 py-3 border-b border-slate-200 dark:border-slate-700;background:rgba(248,250,252,0.5)}.dark .user-info-section{background:rgba(30,41,59,0.5)}.user-info-name{@apply text-sm font-semibold text-slate-900 dark:text-white}.user-info-role{@apply text-xs text-slate-500 dark:text-slate-400 mt-1} \ No newline at end of file diff --git a/backend/static/css/input.min.css.gz b/backend/static/css/input.min.css.gz new file mode 100644 index 00000000..050b879f Binary files /dev/null and b/backend/static/css/input.min.css.gz differ diff --git a/backend/static/css/kiosk-optimized.css b/backend/static/css/kiosk-optimized.css new file mode 100644 index 00000000..fcb7586d --- /dev/null +++ b/backend/static/css/kiosk-optimized.css @@ -0,0 +1,346 @@ +/** + * MYP Platform - Kiosk-Optimierte CSS + * Maximale Performance für Offline-Kiosk-Umgebung + * Keine Touch-Events, minimale Animationen, optimierte Rendering-Performance + */ + +/* ===== CRITICAL INLINE STYLES (sollten im HTML-Head stehen) ===== */ +:root { + /* Reduzierte Farbvariablen für bessere Performance */ + --primary: #0073ce; + --primary-dark: #005a9f; + --bg: #fafbfc; + --surface: #ffffff; + --text: #111827; + --text-muted: #6b7280; + --border: #e5e7eb; + --shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +/* CSS Containment für bessere Performance */ +* { + contain: layout style; +} + +/* Basis-Reset ohne aufwendige Normalisierung */ +* { box-sizing: border-box; margin: 0; padding: 0; } +body { + font-family: system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; + contain: layout style paint; + /* Optimiert für Kiosk-Rendering */ + text-rendering: optimizeSpeed; + -webkit-font-smoothing: antialiased; +} + +/* ===== LAYOUT OPTIMIERUNGEN ===== */ +.header { + background: var(--surface); + border-bottom: 1px solid var(--border); + padding: 1rem; + contain: layout style; +} + +.nav { + display: flex; + gap: 1rem; + contain: layout; +} + +.nav-item { + padding: 0.5rem 1rem; + border-radius: 6px; + text-decoration: none; + color: var(--text-muted); + transition: background-color 0.1s ease; /* Minimale Transition */ +} + +.nav-item:hover { + background: var(--bg); + color: var(--text); +} + +.nav-item.active { + background: var(--primary); + color: white; +} + +/* ===== OPTIMIERTE KARTEN ===== */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + box-shadow: var(--shadow); + contain: layout style paint; +} + +.card:hover { + transform: translateY(-1px); /* Minimaler Hover-Effekt */ +} + +/* ===== BUTTONS OHNE KOMPLEXE ANIMATIONEN ===== */ +.btn { + background: var(--primary); + color: white; + border: none; + border-radius: 6px; + padding: 0.75rem 1.5rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.1s ease; + contain: layout style; +} + +.btn:hover { + background: var(--primary-dark); +} + +.btn-secondary { + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--bg); +} + +/* ===== INPUTS OPTIMIERT ===== */ +.input { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.75rem; + width: 100%; + transition: border-color 0.1s ease; + contain: layout style; +} + +.input:focus { + outline: none; + border-color: var(--primary); +} + +/* ===== TABELLEN OHNE KOMPLEXE EFFEKTE ===== */ +.table { + width: 100%; + border-collapse: collapse; + background: var(--surface); + border-radius: 8px; + overflow: hidden; + contain: layout; +} + +.table th { + background: var(--bg); + padding: 1rem; + text-align: left; + font-weight: 600; + border-bottom: 1px solid var(--border); +} + +.table td { + padding: 1rem; + border-bottom: 1px solid var(--border); +} + +.table tr:hover { + background: var(--bg); +} + +/* ===== STATUS BADGES VEREINFACHT ===== */ +.status { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.status-online { background: #d1fae5; color: #065f46; } +.status-offline { background: #fee2e2; color: #991b1b; } +.status-printing { background: #dbeafe; color: #1e40af; } + +/* ===== GRID LAYOUTS OPTIMIERT ===== */ +.grid { + display: grid; + gap: 1rem; + contain: layout; +} + +.grid-2 { grid-template-columns: repeat(2, 1fr); } +.grid-3 { grid-template-columns: repeat(3, 1fr); } +.grid-4 { grid-template-columns: repeat(4, 1fr); } + +/* ===== UTILITIES MINIMAL ===== */ +.flex { display: flex; contain: layout; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-1 { gap: 0.25rem; } +.gap-2 { gap: 0.5rem; } +.gap-4 { gap: 1rem; } + +.p-1 { padding: 0.25rem; } +.p-2 { padding: 0.5rem; } +.p-4 { padding: 1rem; } +.p-6 { padding: 1.5rem; } + +.m-1 { margin: 0.25rem; } +.m-2 { margin: 0.5rem; } +.m-4 { margin: 1rem; } + +.mb-2 { margin-bottom: 0.5rem; } +.mb-4 { margin-bottom: 1rem; } +.mb-6 { margin-bottom: 1.5rem; } + +.text-sm { font-size: 0.875rem; } +.text-lg { font-size: 1.125rem; } +.text-xl { font-size: 1.25rem; } +.text-2xl { font-size: 1.5rem; } + +.font-semibold { font-weight: 600; } +.font-bold { font-weight: 700; } + +.text-center { text-align: center; } +.text-right { text-align: right; } + +.w-full { width: 100%; } +.h-full { height: 100%; } + +.rounded { border-radius: 6px; } +.rounded-lg { border-radius: 8px; } + +.border { border: 1px solid var(--border); } +.border-t { border-top: 1px solid var(--border); } +.border-b { border-bottom: 1px solid var(--border); } + +.bg-white { background: var(--surface); } +.bg-gray-50 { background: var(--bg); } + +.text-gray-600 { color: var(--text-muted); } +.text-blue-600 { color: var(--primary); } + +/* ===== DARK MODE MINIMAL ===== */ +@media (prefers-color-scheme: dark) { + :root { + --bg: #1e293b; + --surface: #334155; + --text: #f8fafc; + --text-muted: #94a3b8; + --border: #475569; + --shadow: 0 2px 4px rgba(0,0,0,0.3); + } +} + +/* ===== RESPONSIVE FÜR KIOSK-DISPLAYS ===== */ +@media (max-width: 1024px) { + .grid-4 { grid-template-columns: repeat(2, 1fr); } + .grid-3 { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 768px) { + .grid-4, + .grid-3, + .grid-2 { grid-template-columns: 1fr; } + + .nav { + flex-direction: column; + gap: 0.5rem; + } +} + +/* ===== PERFORMANCE OPTIMIERUNGEN ===== */ +/* Deaktivierung nicht benötigter Features für Kiosk */ +* { + /* Keine Touch-Events */ + touch-action: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; +} + +/* Nur notwendige Elemente selektierbar */ +input, +textarea { + -webkit-user-select: text; + user-select: text; + touch-action: manipulation; +} + +/* Optimierte Scrolling-Performance */ +* { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; +} + +/* GPU-Acceleration nur wo nötig */ +.card:hover, +.btn:hover { + will-change: transform; +} + +/* Minimale Animationen für bessere Performance */ +@media (prefers-reduced-motion: reduce) { + * { + transition: none !important; + animation: none !important; + } +} + +/* Print-Optimierung (falls Kiosk drucken kann) */ +@media print { + .nav, + .btn { + display: none; + } + + .card { + box-shadow: none; + border: 1px solid #000; + } +} + +/* ===== LAYOUT SHIFT PREVENTION ===== */ +img { + height: auto; + max-width: 100%; + vertical-align: middle; +} + +/* Container für stabile Layouts */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + contain: layout; +} + +/* ===== KIOSK-SPEZIFISCHE OPTIMIERUNGEN ===== */ +/* Vollbildmodus-Unterstützung */ +html, +body { + height: 100%; + overflow-x: hidden; +} + +/* Fokus-Management für Keyboard-Navigation */ +:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Kein Selection-Highlighting */ +::selection { + background: transparent; +} + +/* Optimierte Font-Loading */ +@font-face { + font-family: 'system-ui'; + font-display: swap; +} \ No newline at end of file diff --git a/backend/static/css/optimization-animations.css b/backend/static/css/optimization-animations.css index 917091ca..66ecc014 100644 --- a/backend/static/css/optimization-animations.css +++ b/backend/static/css/optimization-animations.css @@ -1,100 +1,27 @@ /** - * MYP Platform - Optimierungs-Animationen - * Belohnende und motivierende Animationen für Auto-Optimierung + * MYP Platform - Optimierte einfache Animationen + * Reduzierte, performante Animationen für bessere User Experience */ -/* ===== MODAL FADE-IN ANIMATION ===== */ +/* ===== EINFACHE FADE-IN ANIMATION ===== */ @keyframes fade-in { from { opacity: 0; - backdrop-filter: blur(0px); } to { opacity: 1; - backdrop-filter: blur(8px); } } .animate-fade-in { - animation: fade-in 0.3s ease-out; + animation: fade-in 0.2s ease-out; } -/* ===== BOUNCE-IN ANIMATION ===== */ -@keyframes bounce-in { - 0% { - opacity: 0; - transform: scale(0.3) translateY(-50px); - } - 50% { - opacity: 1; - transform: scale(1.05) translateY(0); - } - 70% { - transform: scale(0.95); - } - 100% { - opacity: 1; - transform: scale(1); - } -} - -.animate-bounce-in { - animation: bounce-in 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -/* ===== PULSE-SCALE ANIMATION ===== */ -@keyframes pulse-scale { - 0%, 100% { - transform: scale(1); - } - 50% { - transform: scale(1.1); - } -} - -.animate-pulse-scale { - animation: pulse-scale 3s infinite ease-in-out; -} - -/* ===== FLOATING ANIMATIONS ===== */ -@keyframes float { - 0%, 100% { - transform: translateY(0px) rotate(0deg); - } - 33% { - transform: translateY(-15px) rotate(5deg); - } - 66% { - transform: translateY(-5px) rotate(-3deg); - } -} - -@keyframes float-delay { - 0%, 100% { - transform: translateY(0px) rotate(0deg); - } - 33% { - transform: translateY(-10px) rotate(-5deg); - } - 66% { - transform: translateY(-8px) rotate(3deg); - } -} - -.animate-float { - animation: float 4s infinite ease-in-out; -} - -.animate-float-delay { - animation: float-delay 4s infinite ease-in-out; - animation-delay: 1.5s; -} - -/* ===== SLIDE-UP ANIMATIONS ===== */ +/* ===== EINFACHE SLIDE-UP ANIMATION ===== */ @keyframes slide-up { from { opacity: 0; - transform: translateY(30px); + transform: translateY(15px); } to { opacity: 1; @@ -103,239 +30,105 @@ } .animate-slide-up { - animation: slide-up 0.6s ease-out; + animation: slide-up 0.3s ease-out; } .animate-slide-up-delay { - animation: slide-up 0.6s ease-out; - animation-delay: 0.2s; + animation: slide-up 0.3s ease-out; + animation-delay: 0.1s; animation-fill-mode: both; } .animate-slide-up-delay-2 { - animation: slide-up 0.6s ease-out; - animation-delay: 0.4s; + animation: slide-up 0.3s ease-out; + animation-delay: 0.2s; animation-fill-mode: both; } -.animate-slide-up-delay-3 { - animation: slide-up 0.6s ease-out; - animation-delay: 0.6s; - animation-fill-mode: both; +/* ===== EINFACHER HOVER-EFFEKT ===== */ +.animate-hover-lift { + transition: transform 0.2s ease; } -/* ===== COUNT-UP ANIMATION ===== */ -@keyframes count-up { - from { - opacity: 0; - transform: scale(0.5) rotate(-10deg); - } - 50% { - opacity: 1; - transform: scale(1.2) rotate(5deg); - } - to { - opacity: 1; - transform: scale(1) rotate(0deg); - } +.animate-hover-lift:hover { + transform: translateY(-2px); } -.animate-count-up { - animation: count-up 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275); - animation-delay: 0.3s; - animation-fill-mode: both; -} - -/* ===== GLOW ANIMATION ===== */ -@keyframes glow { - 0%, 100% { - box-shadow: 0 0 5px rgba(59, 130, 246, 0.5), - 0 0 10px rgba(59, 130, 246, 0.3), - 0 0 15px rgba(59, 130, 246, 0.1); - } - 50% { - box-shadow: 0 0 20px rgba(59, 130, 246, 0.8), - 0 0 30px rgba(59, 130, 246, 0.6), - 0 0 40px rgba(59, 130, 246, 0.4); - } -} - -.animate-glow { - animation: glow 3s infinite ease-in-out; -} - -/* ===== KONFETTI ANIMATION ===== */ -.confetti-container { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: hidden; - pointer-events: none; -} - -.confetti-piece { - position: absolute; - width: 8px; - height: 8px; - top: -10px; - border-radius: 2px; - animation: confetti-fall linear infinite; -} - -@keyframes confetti-fall { +/* ===== EINFACHE SUCCESS ANIMATION ===== */ +@keyframes simple-success { 0% { - transform: translateY(-100vh) rotate(0deg); - opacity: 1; + opacity: 0; + transform: scale(0.9); } 100% { - transform: translateY(120vh) rotate(720deg); - opacity: 0; - } -} - -/* ===== ERFOLGS-BADGE SPEZIAL-ANIMATIONEN ===== */ -@keyframes star-twinkle { - 0%, 100% { - opacity: 0.6; + opacity: 1; transform: scale(1); } - 50% { +} + +.animate-success { + animation: simple-success 0.3s ease-out; +} + +/* ===== EINFACHER PULSE FÜR LOADING ===== */ +@keyframes simple-pulse { + 0%, 100% { opacity: 1; - transform: scale(1.2); - } -} - -.badge-star { - animation: star-twinkle 1.5s infinite ease-in-out; -} - -/* ===== LOADING SPINNER IMPROVEMENTS ===== */ -@keyframes spinner-glow { - 0% { - filter: drop-shadow(0 0 5px rgba(59, 130, 246, 0.5)); } 50% { - filter: drop-shadow(0 0 15px rgba(59, 130, 246, 0.8)); - } - 100% { - filter: drop-shadow(0 0 5px rgba(59, 130, 246, 0.5)); + opacity: 0.6; } } -#optimization-loader .animate-spin { - animation: spin 1s linear infinite, spinner-glow 2s ease-in-out infinite; +.animate-pulse-simple { + animation: simple-pulse 2s infinite ease-in-out; } -/* ===== DARK MODE OPTIMIERUNGEN ===== */ -@media (prefers-color-scheme: dark) { - .animate-glow { - animation: glow-dark 2s infinite ease-in-out; - } - - @keyframes glow-dark { - 0%, 100% { - box-shadow: 0 0 5px rgba(96, 165, 250, 0.5), - 0 0 10px rgba(96, 165, 250, 0.3), - 0 0 15px rgba(96, 165, 250, 0.1); - } - 50% { - box-shadow: 0 0 20px rgba(96, 165, 250, 0.8), - 0 0 30px rgba(96, 165, 250, 0.6), - 0 0 40px rgba(96, 165, 250, 0.4); - } - } +/* ===== EINFACHER PROGRESS BAR ===== */ +.progress-fill { + transition: width 1s ease-out; } /* ===== PERFORMANCE OPTIMIERUNGEN ===== */ -.animate-bounce-in, .animate-fade-in, .animate-slide-up, .animate-slide-up-delay, -.animate-slide-up-delay-2, -.animate-slide-up-delay-3 { +.animate-slide-up-delay-2 { will-change: transform, opacity; } -.animate-pulse-scale, -.animate-float, -.animate-float-delay { +.animate-hover-lift { will-change: transform; } -.animate-glow { - will-change: box-shadow; +/* ===== UTILITY CLASSES ===== */ +.animate-smooth { + transition: all 0.2s ease; +} + +.animate-smooth-fast { + transition: all 0.1s ease; } /* ===== RESPONSIVE ANPASSUNGEN ===== */ @media (max-width: 768px) { - .confetti-piece { - width: 6px; - height: 6px; + .animate-fade-in, + .animate-slide-up, + .animate-slide-up-delay, + .animate-slide-up-delay-2 { + animation-duration: 0.2s; + } +} + +/* ===== REDUZIERTE BEWEGUNG UNTERSTÜTZUNG ===== */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; } - #optimization-reward-modal .text-8xl { - font-size: 4rem; - } - - #optimization-reward-modal .max-w-md { - max-width: 90vw; - } -} - -/* ===== BUTTON HOVER VERBESSERUNGEN ===== */ -button:hover .badge-star { - animation-duration: 0.8s; -} - -/* ===== ZUSÄTZLICHE UTILITY CLASSES ===== */ -.animate-shake { - animation: shake 0.5s ease-in-out; -} - -@keyframes shake { - 0%, 100% { transform: translateX(0); } - 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } - 20%, 40%, 60%, 80% { transform: translateX(5px); } -} - -.animate-heartbeat { - animation: heartbeat 1.5s infinite; -} - -@keyframes heartbeat { - 0%, 100% { transform: scale(1); } - 14% { transform: scale(1.1); } - 28% { transform: scale(1); } - 42% { transform: scale(1.1); } - 70% { transform: scale(1); } -} - -/* ===== PROGRESS BAR ANIMATION ===== */ -.progress-fill { - animation: progress-grow 2s ease-out; -} - -@keyframes progress-grow { - from { - width: 0%; - } - to { - width: var(--progress-width, 100%); - } -} - -/* ===== SUCCESS CHECKMARK ANIMATION ===== */ -.success-checkmark { - animation: checkmark-draw 0.8s ease-out; -} - -@keyframes checkmark-draw { - 0% { - stroke-dasharray: 0 100; - } - 100% { - stroke-dasharray: 100 0; + .animate-hover-lift:hover { + transform: none; } } \ No newline at end of file diff --git a/backend/static/css/optimization-animations.css.gz b/backend/static/css/optimization-animations.css.gz new file mode 100644 index 00000000..ae260918 Binary files /dev/null and b/backend/static/css/optimization-animations.css.gz differ diff --git a/backend/static/css/optimization-animations.min.css b/backend/static/css/optimization-animations.min.css new file mode 100644 index 00000000..4796e342 --- /dev/null +++ b/backend/static/css/optimization-animations.min.css @@ -0,0 +1 @@ +@keyframes fade-in{from{opacity:0}to{opacity:1}}.animate-fade-in{animation:fade-in .2s ease-out}@keyframes slide-up{from{opacity:0;transform:translateY(15px)}to{opacity:1;transform:translateY(0)}}.animate-slide-up{animation:slide-up .3s ease-out}.animate-slide-up-delay{animation:slide-up .3s ease-out;animation-delay:.1s;animation-fill-mode:both}.animate-slide-up-delay-2{animation:slide-up .3s ease-out;animation-delay:.2s;animation-fill-mode:both}.animate-hover-lift{transition:transform .2s ease}.animate-hover-lift:hover{transform:translateY(-2px)}@keyframes simple-success{0%{opacity:0;transform:scale(0.9)}100%{opacity:1;transform:scale(1)}}.animate-success{animation:simple-success .3s ease-out}@keyframes simple-pulse{0%,100%{opacity:1}50%{opacity:.6}}.animate-pulse-simple{animation:simple-pulse 2s infinite ease-in-out}.progress-fill{transition:width 1s ease-out}.animate-fade-in,.animate-slide-up,.animate-slide-up-delay,.animate-slide-up-delay-2{will-change:transform,opacity}.animate-hover-lift{will-change:transform}.animate-smooth{transition:all .2s ease}.animate-smooth-fast{transition:all .1s ease}@media(max-width:768px){.animate-fade-in,.animate-slide-up,.animate-slide-up-delay,.animate-slide-up-delay-2{animation-duration:.2s}}@media(prefers-reduced-motion:reduce){*{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important}.animate-hover-lift:hover{transform:none}} \ No newline at end of file diff --git a/backend/static/css/optimization-animations.min.css.gz b/backend/static/css/optimization-animations.min.css.gz new file mode 100644 index 00000000..02d58e68 Binary files /dev/null and b/backend/static/css/optimization-animations.min.css.gz differ diff --git a/backend/static/css/output.css.gz b/backend/static/css/output.css.gz new file mode 100644 index 00000000..3d635a6c Binary files /dev/null and b/backend/static/css/output.css.gz differ diff --git a/backend/static/css/output.min.css b/backend/static/css/output.min.css new file mode 100644 index 00000000..90aaa02c --- /dev/null +++ b/backend/static/css/output.min.css @@ -0,0 +1 @@ +*,::before,::after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x:;--tw-pan-y:;--tw-pinch-zoom:;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position:;--tw-gradient-via-position:;--tw-gradient-to-position:;--tw-ordinal:;--tw-slashed-zero:;--tw-numeric-figure:;--tw-numeric-spacing:;--tw-numeric-fraction:;--tw-ring-inset:;--tw-ring-offset-width:0;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / .5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur:;--tw-brightness:;--tw-contrast:;--tw-grayscale:;--tw-hue-rotate:;--tw-invert:;--tw-saturate:;--tw-sepia:;--tw-drop-shadow:;--tw-backdrop-blur:;--tw-backdrop-brightness:;--tw-backdrop-contrast:;--tw-backdrop-grayscale:;--tw-backdrop-hue-rotate:;--tw-backdrop-invert:;--tw-backdrop-opacity:;--tw-backdrop-saturate:;--tw-backdrop-sepia:;--tw-contain-size:;--tw-contain-layout:;--tw-contain-paint:;--tw-contain-style:}::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x:;--tw-pan-y:;--tw-pinch-zoom:;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position:;--tw-gradient-via-position:;--tw-gradient-to-position:;--tw-ordinal:;--tw-slashed-zero:;--tw-numeric-figure:;--tw-numeric-spacing:;--tw-numeric-fraction:;--tw-ring-inset:;--tw-ring-offset-width:0;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / .5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur:;--tw-brightness:;--tw-contrast:;--tw-grayscale:;--tw-hue-rotate:;--tw-invert:;--tw-saturate:;--tw-sepia:;--tw-drop-shadow:;--tw-backdrop-blur:;--tw-backdrop-brightness:;--tw-backdrop-contrast:;--tw-backdrop-grayscale:;--tw-backdrop-hue-rotate:;--tw-backdrop-invert:;--tw-backdrop-opacity:;--tw-backdrop-saturate:;--tw-backdrop-sepia:;--tw-contain-size:;--tw-contain-layout:;--tw-contain-paint:;--tw-contain-style:}*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}::before,::after{--tw-content:''}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type='button']),input:where([type='reset']),input:where([type='submit']){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type='search']{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role="button"]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden="until-found"])){display:none}:root{--color-bg-primary:#fff;--color-bg-secondary:#f8fafc;--color-text-primary:#0f172a;--color-text-secondary:#334155;--color-text-muted:#64748b;--color-border-primary:#e2e8f0;--color-accent:#000;--color-accent-hover:#333;--color-accent-text:#fff;--color-shadow:rgba(0,0,0,0.1);--card-radius:1rem}.dark{--color-bg-primary:#000;--color-bg-secondary:#0a0a0a;--color-text-primary:#fff;--color-text-secondary:#e2e8f0;--color-text-muted:#94a3b8;--color-border-primary:#1a1a1a;--color-accent:#fff;--color-accent-hover:#f0f0f0;--color-accent-text:#000;--color-shadow:rgba(0,0,0,0.8);--mb-black:#000}body{--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}body:is(.dark *){--tw-bg-opacity:1;background-color:rgb(0 0 0 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}body{position:relative;min-height:100vh}.dark body{background:linear-gradient(135deg,#000 0,#0a0a0a 50%,#000 100%)}nav{border-bottom-width:1px;border-color:rgb(229 231 235 / .7);background-color:rgb(255 255 255 / .6);--tw-shadow:0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}nav:is(.dark *){border-color:rgb(51 65 85 / .2);background-color:rgb(0 0 0 / .6)}nav{backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);box-shadow:0 8px 32px rgba(0,0,0,0.15),0 0 0 1px rgba(255,255,255,0.05)}#user-dropdown{position:absolute;right:0;z-index:50;margin-top:.5rem;width:16rem;border-radius:.75rem;border-width:1px;border-color:rgb(229 231 235 / .7);background-color:rgb(255 255 255 / .6);--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:200ms}#user-dropdown:is(.dark *){border-color:rgb(51 65 85 / .2);background-color:rgb(0 0 0 / .6)}#user-dropdown{backdrop-filter:blur(20px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(180%) brightness(110%);box-shadow:0 20px 40px rgba(0,0,0,0.2),0 0 0 1px rgba(255,255,255,0.1);animation:fadeIn .2s ease-out forwards}.\!container{width:100% !important;margin-right:auto !important;margin-left:auto !important;padding-right:1rem !important;padding-left:1rem !important}.container{width:100%;margin-right:auto;margin-left:auto;padding-right:1rem;padding-left:1rem}@media(min-width:640px){.\!container{max-width:640px !important;padding-right:1.5rem !important;padding-left:1.5rem !important}.container{max-width:640px;padding-right:1.5rem;padding-left:1.5rem}}@media(min-width:768px){.\!container{max-width:768px !important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px !important;padding-right:2rem !important;padding-left:2rem !important}.container{max-width:1024px;padding-right:2rem;padding-left:2rem}}@media(min-width:1280px){.\!container{max-width:1280px !important;padding-right:3rem !important;padding-left:3rem !important}.container{max-width:1280px;padding-right:3rem;padding-left:3rem}}@media(min-width:1400px){.\!container{max-width:1400px !important;padding-right:4rem !important;padding-left:4rem !important}.container{max-width:1400px;padding-right:4rem;padding-left:4rem}}.dark .bg-dark-card{background-color:#1e293b;--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms}.bg-dark-surface{background-color:#1e293b}.transition-all-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.admin-container{margin-left:auto;margin-right:auto;max-width:80rem;padding:1rem}@media(min-width:768px){.admin-container{padding:2rem}}.admin-stats{margin-bottom:2rem;display:grid;grid-template-columns:repeat(1,minmax(0,1fr));gap:1rem}@media(min-width:640px){.admin-stats{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.admin-stats{grid-template-columns:repeat(4,minmax(0,1fr))}}.stat-card{position:relative;overflow:hidden;border-radius:.75rem;border-width:1px;border-color:rgb(229 231 235 / .6);background-color:rgb(255 255 255 / .6);padding:1.25rem;--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.stat-card:hover{--tw-translate-y:-.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.stat-card:is(.dark *){border-color:rgb(51 65 85 / .3);background-color:rgb(0 0 0 / .7)}.stat-card{backdrop-filter:blur(20px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(180%) brightness(110%);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 0 0 1px rgba(255,255,255,0.1)}.stat-icon{position:absolute;top:1rem;right:1rem;font-size:2.25rem;line-height:2.5rem;opacity:.15}.stat-title{margin-bottom:.5rem;font-size:.875rem;line-height:1.25rem;font-weight:500;text-transform:uppercase;--tw-text-opacity:1;color:rgb(100 116 139 / var(--tw-text-opacity,1))}.stat-title:is(.dark *){--tw-text-opacity:1;color:rgb(148 163 184 / var(--tw-text-opacity,1))}.stat-value{margin-bottom:.25rem;font-size:1.5rem;line-height:2rem;font-weight:700;--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.stat-value:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.stat-desc{font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(100 116 139 / var(--tw-text-opacity,1))}.stat-desc:is(.dark *){--tw-text-opacity:1;color:rgb(148 163 184 / var(--tw-text-opacity,1))}.nav-tabs{margin-bottom:1rem;display:flex;overflow-x:auto;border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(229 231 235 / var(--tw-border-opacity,1))}.nav-tabs:is(.dark *){border-color:rgb(51 65 85 / .3)}.nav-tab{cursor:pointer;white-space:nowrap;border-bottom-width:2px;border-color:transparent;padding-top:1rem;padding-bottom:1rem;padding-left:1.5rem;padding-right:1.5rem;--tw-text-opacity:1;color:rgb(71 85 105 / var(--tw-text-opacity,1));transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:200ms}.nav-tab:hover{--tw-bg-opacity:1;background-color:rgb(248 250 252 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.nav-tab:is(.dark *){--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}.nav-tab:hover:is(.dark *){background-color:rgb(30 41 59 / .5);--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.nav-tab.active{border-bottom-width:2px;--tw-border-opacity:1;border-color:rgb(0 0 0 / var(--tw-border-opacity,1));font-weight:500;--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.nav-tab.active:is(.dark *){--tw-border-opacity:1;border-color:rgb(255 255 255 / var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.tab-content{margin-top:2rem}.tab-pane{display:none}.dark-mode-toggle-new .sun-icon:not(.tab-pane){animation:spin-in .5s cubic-bezier(0.25,1,0.5,1) forwards}.dark-mode-toggle-new .moon-icon:not(.tab-pane){animation:spin-in .5s cubic-bezier(0.25,1,0.5,1) forwards}.tab-pane.active{display:block}.form-group{margin-bottom:1rem}.form-label{margin-bottom:.5rem;display:block;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity:1;color:rgb(51 65 85 / var(--tw-text-opacity,1))}.form-label:is(.dark *){--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}.form-input,.form-select,.form-textarea{width:100%;border-radius:.5rem;border-width:1px;border-color:rgb(209 213 219 / .6);background-color:rgb(255 255 255 / .6);padding-left:.75rem;padding-right:.75rem;padding-top:.5rem;padding-bottom:.5rem;--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.form-input::-moz-placeholder,.form-select::-moz-placeholder,.form-textarea::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175 / var(--tw-placeholder-opacity,1))}.form-input::placeholder,.form-select::placeholder,.form-textarea::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175 / var(--tw-placeholder-opacity,1))}.form-input,.form-select,.form-textarea{--tw-backdrop-blur:blur(16px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:200ms}.form-input:focus,.form-select:focus,.form-textarea:focus{border-color:transparent;outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(100 116 139 / var(--tw-ring-opacity,1))}.form-input:is(.dark *),.form-select:is(.dark *),.form-textarea:is(.dark *){border-color:rgb(71 85 105 / .6);background-color:rgb(30 41 59 / .6);--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.form-input,.form-select,.form-textarea{backdrop-filter:blur(16px) saturate(150%);-webkit-backdrop-filter:blur(16px) saturate(150%);box-shadow:0 10px 20px rgba(0,0,0,0.1),0 0 0 1px rgba(255,255,255,0.05)}.admin-table{min-width:100%}.admin-table>:not([hidden]) ~ :not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse));--tw-divide-opacity:1;border-color:rgb(229 231 235 / var(--tw-divide-opacity,1))}.admin-table:is(.dark *)>:not([hidden]) ~ :not([hidden]){--tw-divide-opacity:1;border-color:rgb(51 65 85 / var(--tw-divide-opacity,1))}.admin-table thead{--tw-bg-opacity:1;background-color:rgb(248 250 252 / var(--tw-bg-opacity,1))}.admin-table thead:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.admin-table th{padding-left:1.5rem;padding-right:1.5rem;padding-top:.75rem;padding-bottom:.75rem;text-align:left;font-size:.75rem;line-height:1rem;font-weight:500;text-transform:uppercase;letter-spacing:.05em;--tw-text-opacity:1;color:rgb(100 116 139 / var(--tw-text-opacity,1))}.admin-table th:is(.dark *){--tw-text-opacity:1;color:rgb(148 163 184 / var(--tw-text-opacity,1))}.admin-table tbody>:not([hidden]) ~ :not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse));--tw-divide-opacity:1;border-color:rgb(229 231 235 / var(--tw-divide-opacity,1))}.admin-table tbody{--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1))}.admin-table tbody:is(.dark *){background-color:#1e293b}.admin-table tbody:is(.dark *)>:not([hidden]) ~ :not([hidden]){--tw-divide-opacity:1;border-color:rgb(51 65 85 / var(--tw-divide-opacity,1))}.admin-table tbody:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.admin-table tr{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms}.admin-table tr:hover{--tw-bg-opacity:1;background-color:rgb(248 250 252 / var(--tw-bg-opacity,1))}.admin-table tr:hover:is(.dark *){background-color:rgb(51 65 85 / .5)}.admin-table td{white-space:nowrap;padding-left:1.5rem;padding-right:1.5rem;padding-top:1rem;padding-bottom:1rem;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.admin-table td:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.badge{display:inline-flex;border-radius:9999px;padding-left:.5rem;padding-right:.5rem;font-size:.75rem;font-weight:600;line-height:1.25rem}.badge-success{--tw-bg-opacity:1;background-color:rgb(220 252 231 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(22 101 52 / var(--tw-text-opacity,1))}.badge-success:is(.dark *){--tw-bg-opacity:1;background-color:rgb(20 83 45 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(187 247 208 / var(--tw-text-opacity,1))}.badge-error{--tw-bg-opacity:1;background-color:rgb(254 226 226 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(153 27 27 / var(--tw-text-opacity,1))}.badge-error:is(.dark *){--tw-bg-opacity:1;background-color:rgb(127 29 29 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(254 202 202 / var(--tw-text-opacity,1))}.badge-warning{--tw-bg-opacity:1;background-color:rgb(254 249 195 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(133 77 14 / var(--tw-text-opacity,1))}.badge-warning:is(.dark *){--tw-bg-opacity:1;background-color:rgb(113 63 18 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(254 240 138 / var(--tw-text-opacity,1))}.badge-info{--tw-bg-opacity:1;background-color:rgb(219 234 254 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(30 64 175 / var(--tw-text-opacity,1))}.badge-info:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 58 138 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(191 219 254 / var(--tw-text-opacity,1))}.printer-card{border-radius:.75rem;border-width:1px;border-color:rgb(229 231 235 / .6);background-color:rgb(255 255 255 / .6);padding:1.5rem;--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.printer-card:hover{--tw-translate-y:-.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.printer-card:is(.dark *){border-color:rgb(51 65 85 / .3);background-color:rgb(0 0 0 / .7)}.printer-card{backdrop-filter:blur(20px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(180%) brightness(110%);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 0 0 1px rgba(255,255,255,0.1)}.printer-header{margin-bottom:1rem;display:flex;align-items:center;justify-content:space-between}.printer-name{font-size:1.25rem;line-height:1.75rem;font-weight:700;--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.printer-name:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.printer-actions{display:flex}.printer-actions>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.5rem * var(--tw-space-x-reverse));margin-left:calc(0.5rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.printer-info{margin-bottom:1rem;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.printer-status{margin-top:1rem;display:flex;align-items:center}.status-indicator{margin-right:.5rem;height:.75rem;width:.75rem;border-radius:9999px}.status-running{--tw-bg-opacity:1;background-color:rgb(34 197 94 / var(--tw-bg-opacity,1));animation:pulse 2s infinite}.status-stopped{--tw-bg-opacity:1;background-color:rgb(239 68 68 / var(--tw-bg-opacity,1))}@keyframes pulse{0%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.1)}100%{opacity:1;transform:scale(1)}}.log-entry{margin-bottom:.5rem;border-top-right-radius:.5rem;border-bottom-right-radius:.5rem;border-left-width:4px;--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1));padding:.75rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms}.log-entry:hover{--tw-bg-opacity:1;background-color:rgb(248 250 252 / var(--tw-bg-opacity,1))}.log-entry:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.log-entry:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(51 65 85 / var(--tw-bg-opacity,1))}.log-debug{--tw-border-opacity:1;border-color:rgb(156 163 175 / var(--tw-border-opacity,1))}.log-debug:is(.dark *){--tw-border-opacity:1;border-color:rgb(107 114 128 / var(--tw-border-opacity,1))}.log-info{--tw-border-opacity:1;border-color:rgb(96 165 250 / var(--tw-border-opacity,1))}.log-info:is(.dark *){--tw-border-opacity:1;border-color:rgb(59 130 246 / var(--tw-border-opacity,1))}.log-warning{--tw-border-opacity:1;border-color:rgb(250 204 21 / var(--tw-border-opacity,1))}.log-warning:is(.dark *){--tw-border-opacity:1;border-color:rgb(234 179 8 / var(--tw-border-opacity,1))}.log-error{--tw-border-opacity:1;border-color:rgb(248 113 113 / var(--tw-border-opacity,1))}.log-error:is(.dark *){--tw-border-opacity:1;border-color:rgb(239 68 68 / var(--tw-border-opacity,1))}.log-critical{--tw-border-opacity:1;border-color:rgb(192 132 252 / var(--tw-border-opacity,1))}.log-critical:is(.dark *){--tw-border-opacity:1;border-color:rgb(168 85 247 / var(--tw-border-opacity,1))}.scheduler-status{display:flex;align-items:center;border-radius:.5rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(229 231 235 / var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1));padding:1rem;--tw-shadow:0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.scheduler-status:is(.dark *){--tw-border-opacity:1;border-color:rgb(51 65 85 / var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.progress-bar{height:.5rem;width:100%;overflow:hidden;border-radius:9999px;--tw-bg-opacity:1;background-color:rgb(229 231 235 / var(--tw-bg-opacity,1))}.progress-bar:is(.dark *){--tw-bg-opacity:1;background-color:rgb(51 65 85 / var(--tw-bg-opacity,1))}.progress-bar-fill{height:100%;transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.progress-bar-fill-blue{--tw-bg-opacity:1;background-color:rgb(59 130 246 / var(--tw-bg-opacity,1))}.progress-bar-fill-blue:is(.dark *){--tw-bg-opacity:1;background-color:rgb(37 99 235 / var(--tw-bg-opacity,1))}.progress-bar-fill-green{--tw-bg-opacity:1;background-color:rgb(34 197 94 / var(--tw-bg-opacity,1))}.progress-bar-fill-green:is(.dark *){--tw-bg-opacity:1;background-color:rgb(22 163 74 / var(--tw-bg-opacity,1))}.progress-bar-fill-purple{--tw-bg-opacity:1;background-color:rgb(168 85 247 / var(--tw-bg-opacity,1))}.progress-bar-fill-purple:is(.dark *){--tw-bg-opacity:1;background-color:rgb(147 51 234 / var(--tw-bg-opacity,1))}.\!notification{position:fixed;top:1rem;right:1rem;z-index:50;max-width:28rem;--tw-translate-x:100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:.5rem;border-left-width:4px;--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1));padding:1rem;opacity:0;--tw-shadow:0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.\!notification:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.notification{position:fixed;top:1rem;right:1rem;z-index:50;max-width:28rem;--tw-translate-x:100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:.5rem;border-left-width:4px;--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1));padding:1rem;opacity:0;--tw-shadow:0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.notification:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.\!notification.show{--tw-translate-x:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));opacity:1}.notification.show{--tw-translate-x:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));opacity:1}.notification.\!show{--tw-translate-x:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));opacity:1}.notification-success{--tw-border-opacity:1;border-color:rgb(34 197 94 / var(--tw-border-opacity,1))}.notification-error{--tw-border-opacity:1;border-color:rgb(239 68 68 / var(--tw-border-opacity,1))}.notification-warning{--tw-border-opacity:1;border-color:rgb(234 179 8 / var(--tw-border-opacity,1))}.notification-info{--tw-border-opacity:1;border-color:rgb(59 130 246 / var(--tw-border-opacity,1))}.alert{margin-bottom:1rem;border-radius:.5rem;border-width:1px;padding:1rem}.alert-success{--tw-border-opacity:1;border-color:rgb(34 197 94 / var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(240 253 244 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(22 101 52 / var(--tw-text-opacity,1))}.alert-success:is(.dark *){background-color:rgb(20 83 45 / .3);--tw-text-opacity:1;color:rgb(187 247 208 / var(--tw-text-opacity,1))}.alert-error{--tw-border-opacity:1;border-color:rgb(239 68 68 / var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(254 242 242 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(153 27 27 / var(--tw-text-opacity,1))}.alert-error:is(.dark *){background-color:rgb(127 29 29 / .3);--tw-text-opacity:1;color:rgb(254 202 202 / var(--tw-text-opacity,1))}.alert-warning{--tw-border-opacity:1;border-color:rgb(234 179 8 / var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(254 252 232 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(133 77 14 / var(--tw-text-opacity,1))}.alert-warning:is(.dark *){background-color:rgb(113 63 18 / .3);--tw-text-opacity:1;color:rgb(254 240 138 / var(--tw-text-opacity,1))}.alert-info{--tw-border-opacity:1;border-color:rgb(59 130 246 / var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(239 246 255 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(30 64 175 / var(--tw-text-opacity,1))}.alert-info:is(.dark *){background-color:rgb(30 58 138 / .3);--tw-text-opacity:1;color:rgb(191 219 254 / var(--tw-text-opacity,1))}.btn-primary{border-radius:.5rem;padding-left:1rem;padding-right:1rem;padding-top:.5rem;padding-bottom:.5rem;--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1));--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.btn-primary:hover{--tw-translate-y:-.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.btn-primary:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(107 114 128 / var(--tw-ring-opacity,1));--tw-ring-offset-width:2px}.btn-primary:is(.dark *){--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.btn-primary{background:rgba(0,0,0,0.7);backdrop-filter:blur(20px) saturate(150%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(150%) brightness(110%);border:1px solid rgba(255,255,255,0.2);box-shadow:0 20px 40px rgba(0,0,0,0.3),0 8px 16px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.1)}.btn-primary:hover{background:rgba(0,0,0,0.9);backdrop-filter:blur(25px) saturate(180%) brightness(120%);-webkit-backdrop-filter:blur(25px) saturate(180%) brightness(120%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 25px 50px rgba(0,0,0,0.4),0 10px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.3)}.dark .btn-primary{background:rgba(255,255,255,0.7);border:1px solid rgba(0,0,0,0.1);box-shadow:0 20px 40px rgba(0,0,0,0.2),0 8px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.8),0 0 0 1px rgba(0,0,0,0.05)}.dark .btn-primary:hover{background:rgba(255,255,255,0.9);border:1px solid rgba(0,0,0,0.15);box-shadow:0 25px 50px rgba(0,0,0,0.3),0 10px 20px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.9)}.btn-secondary{border-radius:.5rem;padding-left:1rem;padding-right:1rem;padding-top:.5rem;padding-bottom:.5rem;--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1));--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.btn-secondary:hover{--tw-translate-y:-.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.btn-secondary:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(100 116 139 / var(--tw-ring-opacity,1));--tw-ring-offset-width:2px}.btn-secondary:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.btn-secondary{background:rgba(255,255,255,0.3);backdrop-filter:blur(20px) saturate(150%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(150%) brightness(110%);border:1px solid rgba(255,255,255,0.4);box-shadow:0 20px 40px rgba(0,0,0,0.15),0 8px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.5),0 0 0 1px rgba(255,255,255,0.2)}.btn-secondary:hover{background:rgba(255,255,255,0.5);backdrop-filter:blur(25px) saturate(180%) brightness(120%);-webkit-backdrop-filter:blur(25px) saturate(180%) brightness(120%);border:1px solid rgba(255,255,255,0.6);box-shadow:0 25px 50px rgba(0,0,0,0.2),0 10px 20px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.7)}.dark .btn-secondary{background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.2);box-shadow:0 20px 40px rgba(0,0,0,0.3),0 8px 16px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.1)}.dark .btn-secondary:hover{background:rgba(0,0,0,0.6);border:1px solid rgba(255,255,255,0.3);box-shadow:0 25px 50px rgba(0,0,0,0.4),0 10px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.3)}.btn-outline{border-radius:.5rem;border-width:2px;border-color:rgb(0 0 0 / .7);padding-left:1rem;padding-right:1rem;padding-top:.5rem;padding-bottom:.5rem;--tw-text-opacity:1;color:rgb(0 0 0 / var(--tw-text-opacity,1));--tw-backdrop-blur:blur(16px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.btn-outline:hover{background-color:rgb(0 0 0 / .7);--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.btn-outline:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(107 114 128 / var(--tw-ring-opacity,1));--tw-ring-offset-width:2px}.btn-outline:is(.dark *){border-color:rgb(255 255 255 / .7);--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.btn-outline:hover:is(.dark *){background-color:rgb(255 255 255 / .7);--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.btn-outline{backdrop-filter:blur(16px) saturate(150%);-webkit-backdrop-filter:blur(16px) saturate(150%);box-shadow:0 15px 30px rgba(0,0,0,0.1),0 0 0 1px rgba(255,255,255,0.05)}.glass-card{border-radius:.75rem;padding:1.5rem;--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms;background:rgba(255,255,255,0.15);backdrop-filter:blur(30px) saturate(200%) brightness(120%) contrast(110%);-webkit-backdrop-filter:blur(30px) saturate(200%) brightness(120%) contrast(110%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 8px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.1);border-radius:var(--card-radius)}.dark .glass-card{background:rgba(0,0,0,0.3);backdrop-filter:blur(30px) saturate(180%) brightness(110%) contrast(120%);-webkit-backdrop-filter:blur(30px) saturate(180%) brightness(110%) contrast(120%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 25px 50px rgba(0,0,0,0.4),0 8px 16px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.15),0 0 0 1px rgba(255,255,255,0.05)}.dashboard-card{border-radius:.75rem;padding:1.5rem;--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.dashboard-card:hover{--tw-translate-y:-.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dashboard-card{background:rgba(255,255,255,0.12);backdrop-filter:blur(35px) saturate(200%) brightness(125%) contrast(115%);-webkit-backdrop-filter:blur(35px) saturate(200%) brightness(125%) contrast(115%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 8px 16px rgba(0,0,0,0.08),inset 0 1px 0 rgba(255,255,255,0.25),0 0 0 1px rgba(255,255,255,0.1);border-radius:var(--card-radius)}.dark .dashboard-card{background:rgba(0,0,0,0.35);backdrop-filter:blur(35px) saturate(180%) brightness(115%) contrast(125%);-webkit-backdrop-filter:blur(35px) saturate(180%) brightness(115%) contrast(125%);border:1px solid rgba(255,255,255,0.12);box-shadow:0 25px 50px rgba(0,0,0,0.5),0 8px 16px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.12),0 0 0 1px rgba(255,255,255,0.05)}.nav-link{display:flex;align-items:center;border-radius:.5rem;padding-left:1rem;padding-right:1rem;padding-top:.5rem;padding-bottom:.5rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity:1;color:rgb(51 65 85 / var(--tw-text-opacity,1));transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:200ms}.nav-link:hover{--tw-bg-opacity:1;background-color:rgb(241 245 249 / var(--tw-bg-opacity,1));--tw-shadow:0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.nav-link:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(100 116 139 / var(--tw-ring-opacity,1));--tw-ring-offset-width:2px}.nav-link:is(.dark *){--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}.nav-link:hover:is(.dark *){background-color:rgb(51 65 85 / .5)}.nav-link.active{--tw-bg-opacity:1;background-color:rgb(241 245 249 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1));--tw-shadow:0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.nav-link.active:is(.dark *){--tw-bg-opacity:1;background-color:rgb(0 0 0 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.navbar{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;background:rgba(255,255,255,0.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:10px;box-shadow:0 4px 6px rgba(0,0,0,0.1);transition:all .3s ease}.navbar-button{padding:.25rem .5rem;font-size:.875rem;border-radius:5px;transition:background-color .3s ease}.navbar-button:hover{background-color:rgba(255,255,255,0.2)}@media(max-width:768px){.navbar{flex-direction:column;padding:.25rem}.navbar-button{margin:.25rem 0}}.dark .navbar{background:rgba(0,0,0,0.25);backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(120%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(120%);box-shadow:0 8px 32px rgba(0,0,0,0.6),0 2px 8px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.1),0 0 0 1px rgba(255,255,255,0.05);border-bottom:1px solid rgba(255,255,255,0.1)}.navbar-brand{display:flex;align-items:center}.navbar-brand>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.5rem * var(--tw-space-x-reverse));margin-left:calc(0.5rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.navbar-brand{transition-property:transform;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.navbar-brand:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.navbar-menu{margin-left:1rem;margin-right:1rem;display:flex;align-items:center;justify-content:center}.navbar-menu>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.25rem * var(--tw-space-x-reverse));margin-left:calc(0.25rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.navbar-menu{border-radius:1rem;border-width:1px;padding:.75rem}@media(min-width:768px){.navbar-menu>:not([hidden]) ~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.75rem * var(--tw-space-x-reverse));margin-left:calc(0.75rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}}@media(min-width:1024px){.navbar-menu>:not([hidden]) ~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.5rem * var(--tw-space-x-reverse));margin-left:calc(1.5rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}}.navbar-menu{background:rgba(255,255,255,0.25);backdrop-filter:blur(20px) saturate(150%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(150%) brightness(110%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 4px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.1)}.dark .navbar-menu{background:rgba(0,0,0,0.4);backdrop-filter:blur(20px) saturate(150%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(150%) brightness(110%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 4px 16px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}.navbar-button{border-radius:9999px;padding:.5rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.navbar-button:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-offset-width:2px}.user-menu-button{display:flex;align-items:center}.user-menu-button>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.5rem * var(--tw-space-x-reverse));margin-left:calc(0.5rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.user-menu-button{border-radius:.5rem;padding:.25rem;transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.user-menu-button:hover{background-color:rgb(243 244 246 / .8)}.user-menu-button:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(100 116 139 / var(--tw-ring-opacity,1));--tw-ring-offset-width:2px}.user-menu-button:hover:is(.dark *){background-color:rgb(51 65 85 / .6)}.user-avatar{display:flex;height:2.5rem;width:2.5rem;align-items:center;justify-content:center;border-radius:9999px;--tw-bg-opacity:1;background-color:rgb(0 0 0 / var(--tw-bg-opacity,1));font-size:.875rem;line-height:1.25rem;font-weight:700;--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1));--tw-shadow:0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.user-avatar:hover{--tw-shadow:0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.user-avatar:is(.dark *){--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.avatar-large{display:flex;height:3.5rem;width:3.5rem;align-items:center;justify-content:center;border-radius:9999px;--tw-bg-opacity:1;background-color:rgb(0 0 0 / var(--tw-bg-opacity,1));font-size:1.125rem;line-height:1.75rem;font-weight:700;--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1));--tw-shadow:0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.avatar-large:is(.dark *){--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.user-dropdown-item{display:flex;align-items:center;padding-left:1rem;padding-right:1rem;padding-top:.75rem;padding-bottom:.75rem;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(51 65 85 / var(--tw-text-opacity,1));transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.user-dropdown-item:hover{background-color:rgb(243 244 246 / .8);--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.user-dropdown-item:focus{background-color:rgb(243 244 246 / .8);outline:2px solid transparent;outline-offset:2px}.user-dropdown-item:is(.dark *){--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}.user-dropdown-item:hover:is(.dark *){background-color:rgb(51 65 85 / .6);--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.user-dropdown-item:focus:is(.dark *){background-color:rgb(51 65 85 / .6)}.user-dropdown-separator{margin-top:.25rem;margin-bottom:.25rem;border-top-width:1px;border-color:rgb(229 231 235 / .8)}.user-dropdown-separator:is(.dark *){border-color:rgb(51 65 85 / .3)}.menu-item{display:flex;align-items:center}.menu-item>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.5rem * var(--tw-space-x-reverse));margin-left:calc(0.5rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.menu-item{border-radius:.75rem;padding-left:1rem;padding-right:1rem;padding-top:.625rem;padding-bottom:.625rem;--tw-text-opacity:1;color:rgb(51 65 85 / var(--tw-text-opacity,1));transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.menu-item:is(.dark *){--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}.menu-item{background:rgba(255,255,255,0.1);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.2);box-shadow:0 2px 8px rgba(0,0,0,0.05)}.menu-item:hover{--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.menu-item:hover:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.menu-item:hover{background:rgba(255,255,255,0.3);backdrop-filter:blur(15px) saturate(150%);-webkit-backdrop-filter:blur(15px) saturate(150%);border:1px solid rgba(255,255,255,0.4);box-shadow:0 4px 16px rgba(0,0,0,0.1);transform:translateY(-1px)}.dark .menu-item{background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.1);box-shadow:0 2px 8px rgba(0,0,0,0.2)}.dark .menu-item:hover{background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.2);box-shadow:0 4px 16px rgba(0,0,0,0.3)}.menu-item.active{font-weight:500;--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.menu-item.active:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.menu-item.active{background:rgba(255,255,255,0.5);backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border:1px solid rgba(255,255,255,0.6);box-shadow:0 4px 16px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.5)}.dark .menu-item.active{background:rgba(0,0,0,0.6);border:1px solid rgba(255,255,255,0.3);box-shadow:0 4px 16px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2)}.user-dropdown{position:absolute;right:0;z-index:50;margin-top:.5rem;width:16rem;overflow:hidden;border-radius:.75rem;--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);background:rgba(255,255,255,0.1);backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);-webkit-backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 25px 50px rgba(0,0,0,0.25),0 8px 16px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.1);animation:fadeIn .2s ease-out forwards}.dark .user-dropdown{background:rgba(0,0,0,0.4);backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(120%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(120%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 25px 50px rgba(0,0,0,0.6),0 8px 16px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}.dropdown-header{display:flex;align-items:center;border-bottom-width:1px;border-color:rgb(229 231 235 / .8);padding:1rem}.dropdown-header:is(.dark *){border-color:rgb(51 65 85 / .3)}.dropdown-item{display:flex;align-items:center;gap:.75rem;padding-left:1rem;padding-right:1rem;padding-top:.75rem;padding-bottom:.75rem;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(51 65 85 / var(--tw-text-opacity,1));transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.dropdown-item:hover{background-color:rgb(243 244 246 / .8);--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.dropdown-item:is(.dark *){--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}.dropdown-item:hover:is(.dark *){background-color:rgb(51 65 85 / .6);--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.dropdown-divider{border-top-width:1px;border-color:rgb(229 231 235 / .8)}.dropdown-divider:is(.dark *){border-color:rgb(51 65 85 / .3)}@keyframes mercedes-rotate{0%{transform:rotate(0)}25%{transform:rotate(90deg)}50%{transform:rotate(180deg)}75%{transform:rotate(270deg)}100%{transform:rotate(360deg)}}.navbar-brand:hover svg{animation:mercedes-rotate 5s infinite linear;transform-origin:center}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.-inset-1{inset:-.25rem}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-bottom-2{bottom:-.5rem}.-bottom-8{bottom:-2rem}.-left-2{left:-.5rem}.-right-1{right:-.25rem}.-right-2{right:-.5rem}.-top-1{top:-.25rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.bottom-8{bottom:2rem}.bottom-full{bottom:100%}.end-1{inset-inline-end:.25rem}.left-0{left:0}.left-1\/2{left:50%}.left-3{left:.75rem}.left-4{left:1rem}.right-0{right:0}.right-2\.5{right:.625rem}.right-4{right:1rem}.right-5{right:1.25rem}.right-6{right:1.5rem}.right-8{right:2rem}.top-0{top:0}.top-1\/2{top:50%}.top-2\.5{top:.625rem}.top-3{top:.75rem}.top-4{top:1rem}.top-5{top:1.25rem}.top-8{top:2rem}.top-full{top:100%}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.col-span-full{grid-column:1 / -1}.m-1{margin:.25rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.-ml-1{margin-left:-.25rem}.-mt-8{margin-top:-2rem}.mb-0{margin-bottom:0}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-16{margin-bottom:4rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-8{margin-left:2rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.list-item{display:list-item}.hidden{display:none}.h-0{height:0}.h-1{height:.25rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-24{height:6rem}.h-28{height:7rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-32{height:8rem}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-full{height:100%}.max-h-32{max-height:8rem}.max-h-80{max-height:20rem}.max-h-96{max-height:24rem}.max-h-\[90vh\]{max-height:90vh}.min-h-\[80vh\]{min-height:80vh}.min-h-screen{min-height:100vh}.w-0{width:0}.w-1{width:.25rem}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-2\/3{width:66.666667%}.w-20{width:5rem}.w-24{width:6rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-3\/4{width:75%}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-80{width:20rem}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[150px\]{min-width:150px}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-screen-xl{max-width:1280px}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.flex-grow{flex-grow:1}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.origin-top-right{transform-origin:top right}.-translate-x-1{--tw-translate-x:-.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-1\/2{--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-0{--tw-translate-y:-0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1{--tw-translate-y:-.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-1{--tw-translate-x:.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x:100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-1{--tw-translate-y:.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-0{--tw-rotate:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.skew-x-12{--tw-skew-x:12deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x:1;--tw-scale-y:1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-75{--tw-scale-x:.75;--tw-scale-y:.75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,100%{transform:translateY(-25%);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,0.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.animate-fade-in{animation:fadeIn .5s ease-in-out}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(0.4,0,0.6,1) infinite}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}100%{transform:translateY(0);opacity:1}}.animate-slide-up{animation:slideUp .5s ease-out}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.select-all{-webkit-user-select:all;-moz-user-select:all;user-select:all}.resize-none{resize:none}.resize{resize:both}.scroll-mt-8{scroll-margin-top:2rem}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-0\.5>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.125rem * var(--tw-space-x-reverse));margin-left:calc(0.125rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.25rem * var(--tw-space-x-reverse));margin-left:calc(0.25rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.space-x-1\.5>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.375rem * var(--tw-space-x-reverse));margin-left:calc(0.375rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.5rem * var(--tw-space-x-reverse));margin-left:calc(0.5rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.space-x-2\.5>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.625rem * var(--tw-space-x-reverse));margin-left:calc(0.625rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.75rem * var(--tw-space-x-reverse));margin-left:calc(0.75rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.space-x-6>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.5rem * var(--tw-space-x-reverse));margin-left:calc(1.5rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0.25rem * calc(1 - var(--tw-space-y-reverse)))calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0.25rem * var(--tw-space-y-reverse))}.space-y-16>:not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(4rem * calc(1 - var(--tw-space-y-reverse)))calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(4rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0.5rem * calc(1 - var(--tw-space-y-reverse)))calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0.75rem * calc(1 - var(--tw-space-y-reverse)))calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)))calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)))calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)))calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden]) ~ :not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden]) ~ :not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235 / var(--tw-divide-opacity,1))}.divide-slate-200>:not([hidden]) ~ :not([hidden]){--tw-divide-opacity:1;border-color:rgb(226 232 240 / var(--tw-divide-opacity,1))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-l-md{border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l-2{border-left-width:2px}.border-l-4{border-left-width:4px}.border-r-4{border-right-width:4px}.border-t{border-top-width:1px}.border-t-4{border-top-width:4px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-amber-200{--tw-border-opacity:1;border-color:rgb(253 230 138 / var(--tw-border-opacity,1))}.border-black{--tw-border-opacity:1;border-color:rgb(0 0 0 / var(--tw-border-opacity,1))}.border-black\/70{border-color:rgb(0 0 0 / .7)}.border-blue-200{--tw-border-opacity:1;border-color:rgb(191 219 254 / var(--tw-border-opacity,1))}.border-blue-200\/50{border-color:rgb(191 219 254 / .5)}.border-blue-300{--tw-border-opacity:1;border-color:rgb(147 197 253 / var(--tw-border-opacity,1))}.border-blue-400{--tw-border-opacity:1;border-color:rgb(96 165 250 / var(--tw-border-opacity,1))}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246 / var(--tw-border-opacity,1))}.border-blue-600{--tw-border-opacity:1;border-color:rgb(37 99 235 / var(--tw-border-opacity,1))}.border-emerald-200\/50{border-color:rgb(167 243 208 / .5)}.border-emerald-500{--tw-border-opacity:1;border-color:rgb(16 185 129 / var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235 / var(--tw-border-opacity,1))}.border-gray-200\/30{border-color:rgb(229 231 235 / .3)}.border-gray-200\/50{border-color:rgb(229 231 235 / .5)}.border-gray-200\/60{border-color:rgb(229 231 235 / .6)}.border-gray-200\/70{border-color:rgb(229 231 235 / .7)}.border-gray-200\/80{border-color:rgb(229 231 235 / .8)}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219 / var(--tw-border-opacity,1))}.border-gray-300\/60{border-color:rgb(209 213 219 / .6)}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175 / var(--tw-border-opacity,1))}.border-green-200{--tw-border-opacity:1;border-color:rgb(187 247 208 / var(--tw-border-opacity,1))}.border-green-200\/50{border-color:rgb(187 247 208 / .5)}.border-green-300{--tw-border-opacity:1;border-color:rgb(134 239 172 / var(--tw-border-opacity,1))}.border-green-400{--tw-border-opacity:1;border-color:rgb(74 222 128 / var(--tw-border-opacity,1))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94 / var(--tw-border-opacity,1))}.border-indigo-200{--tw-border-opacity:1;border-color:rgb(199 210 254 / var(--tw-border-opacity,1))}.border-indigo-200\/50{border-color:rgb(199 210 254 / .5)}.border-indigo-600{--tw-border-opacity:1;border-color:rgb(79 70 229 / var(--tw-border-opacity,1))}.border-light-border{--tw-border-opacity:1;border-color:rgb(226 232 240 / var(--tw-border-opacity,1))}.border-mercedes-silver{--tw-border-opacity:1;border-color:rgb(192 192 192 / var(--tw-border-opacity,1))}.border-orange-200{--tw-border-opacity:1;border-color:rgb(254 215 170 / var(--tw-border-opacity,1))}.border-orange-200\/50{border-color:rgb(254 215 170 / .5)}.border-orange-500{--tw-border-opacity:1;border-color:rgb(249 115 22 / var(--tw-border-opacity,1))}.border-purple-200\/50{border-color:rgb(233 213 255 / .5)}.border-purple-400{--tw-border-opacity:1;border-color:rgb(192 132 252 / var(--tw-border-opacity,1))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202 / var(--tw-border-opacity,1))}.border-red-200\/50{border-color:rgb(254 202 202 / .5)}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165 / var(--tw-border-opacity,1))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113 / var(--tw-border-opacity,1))}.border-red-500{--tw-border-opacity:1;border-color:rgb(239 68 68 / var(--tw-border-opacity,1))}.border-slate-200{--tw-border-opacity:1;border-color:rgb(226 232 240 / var(--tw-border-opacity,1))}.border-slate-200\/50{border-color:rgb(226 232 240 / .5)}.border-slate-300{--tw-border-opacity:1;border-color:rgb(203 213 225 / var(--tw-border-opacity,1))}.border-transparent{border-color:transparent}.border-white\/20{border-color:rgb(255 255 255 / .2)}.border-white\/30{border-color:rgb(255 255 255 / .3)}.border-yellow-200{--tw-border-opacity:1;border-color:rgb(254 240 138 / var(--tw-border-opacity,1))}.border-yellow-400{--tw-border-opacity:1;border-color:rgb(250 204 21 / var(--tw-border-opacity,1))}.border-yellow-500{--tw-border-opacity:1;border-color:rgb(234 179 8 / var(--tw-border-opacity,1))}.border-t-slate-800{--tw-border-opacity:1;border-top-color:rgb(30 41 59 / var(--tw-border-opacity,1))}.border-t-slate-900{--tw-border-opacity:1;border-top-color:rgb(15 23 42 / var(--tw-border-opacity,1))}.bg-accent-primary{--tw-bg-opacity:1;background-color:rgb(59 130 246 / var(--tw-bg-opacity,1))}.bg-amber-400{--tw-bg-opacity:1;background-color:rgb(251 191 36 / var(--tw-bg-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235 / var(--tw-bg-opacity,1))}.bg-amber-500{--tw-bg-opacity:1;background-color:rgb(245 158 11 / var(--tw-bg-opacity,1))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0 / var(--tw-bg-opacity,1))}.bg-black\/20{background-color:rgb(0 0 0 / .2)}.bg-black\/30{background-color:rgb(0 0 0 / .3)}.bg-black\/40{background-color:rgb(0 0 0 / .4)}.bg-black\/50{background-color:rgb(0 0 0 / .5)}.bg-black\/60{background-color:rgb(0 0 0 / .6)}.bg-black\/70{background-color:rgb(0 0 0 / .7)}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254 / var(--tw-bg-opacity,1))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250 / var(--tw-bg-opacity,1))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255 / var(--tw-bg-opacity,1))}.bg-blue-50\/50{background-color:rgb(239 246 255 / .5)}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246 / var(--tw-bg-opacity,1))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235 / var(--tw-bg-opacity,1))}.bg-cyan-100{--tw-bg-opacity:1;background-color:rgb(207 250 254 / var(--tw-bg-opacity,1))}.bg-dark-card{--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.bg-dark-surface{--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.bg-emerald-100{--tw-bg-opacity:1;background-color:rgb(209 250 229 / var(--tw-bg-opacity,1))}.bg-emerald-600{--tw-bg-opacity:1;background-color:rgb(5 150 105 / var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246 / var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235 / var(--tw-bg-opacity,1))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219 / var(--tw-bg-opacity,1))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175 / var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251 / var(--tw-bg-opacity,1))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128 / var(--tw-bg-opacity,1))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99 / var(--tw-bg-opacity,1))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231 / var(--tw-bg-opacity,1))}.bg-green-100\/90{background-color:rgb(220 252 231 / .9)}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128 / var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244 / var(--tw-bg-opacity,1))}.bg-green-50\/50{background-color:rgb(240 253 244 / .5)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94 / var(--tw-bg-opacity,1))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74 / var(--tw-bg-opacity,1))}.bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255 / var(--tw-bg-opacity,1))}.bg-indigo-50{--tw-bg-opacity:1;background-color:rgb(238 242 255 / var(--tw-bg-opacity,1))}.bg-indigo-50\/50{background-color:rgb(238 242 255 / .5)}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241 / var(--tw-bg-opacity,1))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229 / var(--tw-bg-opacity,1))}.bg-light-surface{--tw-bg-opacity:1;background-color:rgb(247 250 252 / var(--tw-bg-opacity,1))}.bg-mercedes-black{--tw-bg-opacity:1;background-color:rgb(0 0 0 / var(--tw-bg-opacity,1))}.bg-mercedes-silver{--tw-bg-opacity:1;background-color:rgb(192 192 192 / var(--tw-bg-opacity,1))}.bg-orange-100{--tw-bg-opacity:1;background-color:rgb(255 237 213 / var(--tw-bg-opacity,1))}.bg-orange-400{--tw-bg-opacity:1;background-color:rgb(251 146 60 / var(--tw-bg-opacity,1))}.bg-orange-50{--tw-bg-opacity:1;background-color:rgb(255 247 237 / var(--tw-bg-opacity,1))}.bg-orange-50\/50{background-color:rgb(255 247 237 / .5)}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22 / var(--tw-bg-opacity,1))}.bg-orange-600{--tw-bg-opacity:1;background-color:rgb(234 88 12 / var(--tw-bg-opacity,1))}.bg-purple-100{--tw-bg-opacity:1;background-color:rgb(243 232 255 / var(--tw-bg-opacity,1))}.bg-purple-400{--tw-bg-opacity:1;background-color:rgb(192 132 252 / var(--tw-bg-opacity,1))}.bg-purple-50{--tw-bg-opacity:1;background-color:rgb(250 245 255 / var(--tw-bg-opacity,1))}.bg-purple-50\/50{background-color:rgb(250 245 255 / .5)}.bg-purple-500{--tw-bg-opacity:1;background-color:rgb(168 85 247 / var(--tw-bg-opacity,1))}.bg-purple-600{--tw-bg-opacity:1;background-color:rgb(147 51 234 / var(--tw-bg-opacity,1))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226 / var(--tw-bg-opacity,1))}.bg-red-100\/90{background-color:rgb(254 226 226 / .9)}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113 / var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242 / var(--tw-bg-opacity,1))}.bg-red-50\/50{background-color:rgb(254 242 242 / .5)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68 / var(--tw-bg-opacity,1))}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38 / var(--tw-bg-opacity,1))}.bg-slate-100{--tw-bg-opacity:1;background-color:rgb(241 245 249 / var(--tw-bg-opacity,1))}.bg-slate-100\/50{background-color:rgb(241 245 249 / .5)}.bg-slate-200{--tw-bg-opacity:1;background-color:rgb(226 232 240 / var(--tw-bg-opacity,1))}.bg-slate-50{--tw-bg-opacity:1;background-color:rgb(248 250 252 / var(--tw-bg-opacity,1))}.bg-slate-50\/50{background-color:rgb(248 250 252 / .5)}.bg-slate-500{--tw-bg-opacity:1;background-color:rgb(100 116 139 / var(--tw-bg-opacity,1))}.bg-slate-600{--tw-bg-opacity:1;background-color:rgb(71 85 105 / var(--tw-bg-opacity,1))}.bg-slate-800{--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.bg-slate-900{--tw-bg-opacity:1;background-color:rgb(15 23 42 / var(--tw-bg-opacity,1))}.bg-teal-100{--tw-bg-opacity:1;background-color:rgb(204 251 241 / var(--tw-bg-opacity,1))}.bg-teal-500{--tw-bg-opacity:1;background-color:rgb(20 184 166 / var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1))}.bg-white\/10{background-color:rgb(255 255 255 / .1)}.bg-white\/15{background-color:rgb(255 255 255 / .15)}.bg-white\/20{background-color:rgb(255 255 255 / .2)}.bg-white\/40{background-color:rgb(255 255 255 / .4)}.bg-white\/60{background-color:rgb(255 255 255 / .6)}.bg-white\/80{background-color:rgb(255 255 255 / .8)}.bg-white\/90{background-color:rgb(255 255 255 / .9)}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195 / var(--tw-bg-opacity,1))}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21 / var(--tw-bg-opacity,1))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232 / var(--tw-bg-opacity,1))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8 / var(--tw-bg-opacity,1))}.bg-yellow-600{--tw-bg-opacity:1;background-color:rgb(202 138 4 / var(--tw-bg-opacity,1))}.bg-opacity-50{--tw-bg-opacity:.5}.bg-opacity-75{--tw-bg-opacity:.75}.bg-opacity-95{--tw-bg-opacity:.95}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-amber-300{--tw-gradient-from:#fcd34d var(--tw-gradient-from-position);--tw-gradient-to:rgb(252 211 77 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-100{--tw-gradient-from:#dbeafe var(--tw-gradient-from-position);--tw-gradient-to:rgb(219 234 254 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-400{--tw-gradient-from:#60a5fa var(--tw-gradient-from-position);--tw-gradient-to:rgb(96 165 250 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-50{--tw-gradient-from:#eff6ff var(--tw-gradient-from-position);--tw-gradient-to:rgb(239 246 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-500\/10{--tw-gradient-from:rgb(59 130 246 / .1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-600{--tw-gradient-from:#2563eb var(--tw-gradient-from-position);--tw-gradient-to:rgb(37 99 235 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-600\/10{--tw-gradient-from:rgb(37 99 235 / .1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(37 99 235 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-400{--tw-gradient-from:#34d399 var(--tw-gradient-from-position);--tw-gradient-to:rgb(52 211 153 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-50{--tw-gradient-from:#ecfdf5 var(--tw-gradient-from-position);--tw-gradient-to:rgb(236 253 245 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-100{--tw-gradient-from:#dcfce7 var(--tw-gradient-from-position);--tw-gradient-to:rgb(220 252 231 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-400{--tw-gradient-from:#4ade80 var(--tw-gradient-from-position);--tw-gradient-to:rgb(74 222 128 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-50{--tw-gradient-from:#f0fdf4 var(--tw-gradient-from-position);--tw-gradient-to:rgb(240 253 244 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-50\/90{--tw-gradient-from:rgb(240 253 244 / .9) var(--tw-gradient-from-position);--tw-gradient-to:rgb(240 253 244 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-500{--tw-gradient-from:#22c55e var(--tw-gradient-from-position);--tw-gradient-to:rgb(34 197 94 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-500\/10{--tw-gradient-from:rgb(34 197 94 / .1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(34 197 94 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-50{--tw-gradient-from:#fff7ed var(--tw-gradient-from-position);--tw-gradient-to:rgb(255 247 237 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-500{--tw-gradient-from:#f97316 var(--tw-gradient-from-position);--tw-gradient-to:rgb(249 115 22 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-500\/10{--tw-gradient-from:rgb(249 115 22 / .1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(249 115 22 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-purple-100{--tw-gradient-from:#f3e8ff var(--tw-gradient-from-position);--tw-gradient-to:rgb(243 232 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-purple-50{--tw-gradient-from:#faf5ff var(--tw-gradient-from-position);--tw-gradient-to:rgb(250 245 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-purple-500{--tw-gradient-from:#a855f7 var(--tw-gradient-from-position);--tw-gradient-to:rgb(168 85 247 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-purple-500\/10{--tw-gradient-from:rgb(168 85 247 / .1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(168 85 247 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-purple-600{--tw-gradient-from:#9333ea var(--tw-gradient-from-position);--tw-gradient-to:rgb(147 51 234 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-red-400{--tw-gradient-from:#f87171 var(--tw-gradient-from-position);--tw-gradient-to:rgb(248 113 113 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-red-500{--tw-gradient-from:#ef4444 var(--tw-gradient-from-position);--tw-gradient-to:rgb(239 68 68 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-red-500\/10{--tw-gradient-from:rgb(239 68 68 / .1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(239 68 68 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-slate-50{--tw-gradient-from:#f8fafc var(--tw-gradient-from-position);--tw-gradient-to:rgb(248 250 252 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-slate-500{--tw-gradient-from:#64748b var(--tw-gradient-from-position);--tw-gradient-to:rgb(100 116 139 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-slate-900{--tw-gradient-from:#0f172a var(--tw-gradient-from-position);--tw-gradient-to:rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:transparent var(--tw-gradient-from-position);--tw-gradient-to:rgb(0 0 0 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-white{--tw-gradient-from:#fff var(--tw-gradient-from-position);--tw-gradient-to:rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-white\/90{--tw-gradient-from:rgb(255 255 255 / .9) var(--tw-gradient-from-position);--tw-gradient-to:rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-blue-100{--tw-gradient-to:rgb(219 234 254 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#dbeafe var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-blue-200{--tw-gradient-to:rgb(191 219 254 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#bfdbfe var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-blue-50{--tw-gradient-to:rgb(239 246 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#eff6ff var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-blue-900{--tw-gradient-to:rgb(30 58 138 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1e3a8a var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-green-50{--tw-gradient-to:rgb(240 253 244 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#f0fdf4 var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-green-500{--tw-gradient-to:rgb(34 197 94 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#22c55e var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-indigo-50{--tw-gradient-to:rgb(238 242 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#eef2ff var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-purple-500{--tw-gradient-to:rgb(168 85 247 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#a855f7 var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-red-50{--tw-gradient-to:rgb(254 242 242 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#fef2f2 var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-white\/20{--tw-gradient-to:rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgb(255 255 255 / .2) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-white\/5{--tw-gradient-to:rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgb(255 255 255 / .05) var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-blue-200{--tw-gradient-to:#bfdbfe var(--tw-gradient-to-position)}.to-blue-600{--tw-gradient-to:#2563eb var(--tw-gradient-to-position)}.to-blue-700{--tw-gradient-to:#1d4ed8 var(--tw-gradient-to-position)}.to-cyan-50{--tw-gradient-to:#ecfeff var(--tw-gradient-to-position)}.to-emerald-50{--tw-gradient-to:#ecfdf5 var(--tw-gradient-to-position)}.to-emerald-50\/80{--tw-gradient-to:rgb(236 253 245 / .8) var(--tw-gradient-to-position)}.to-emerald-500{--tw-gradient-to:#10b981 var(--tw-gradient-to-position)}.to-emerald-500\/10{--tw-gradient-to:rgb(16 185 129 / .1) var(--tw-gradient-to-position)}.to-emerald-600{--tw-gradient-to:#059669 var(--tw-gradient-to-position)}.to-green-200{--tw-gradient-to:#bbf7d0 var(--tw-gradient-to-position)}.to-green-50{--tw-gradient-to:#f0fdf4 var(--tw-gradient-to-position)}.to-green-600{--tw-gradient-to:#16a34a var(--tw-gradient-to-position)}.to-indigo-50{--tw-gradient-to:#eef2ff var(--tw-gradient-to-position)}.to-indigo-500{--tw-gradient-to:#6366f1 var(--tw-gradient-to-position)}.to-indigo-500\/10{--tw-gradient-to:rgb(99 102 241 / .1) var(--tw-gradient-to-position)}.to-indigo-900{--tw-gradient-to:#312e81 var(--tw-gradient-to-position)}.to-orange-400{--tw-gradient-to:#fb923c var(--tw-gradient-to-position)}.to-orange-50{--tw-gradient-to:#fff7ed var(--tw-gradient-to-position)}.to-orange-500{--tw-gradient-to:#f97316 var(--tw-gradient-to-position)}.to-orange-600{--tw-gradient-to:#ea580c var(--tw-gradient-to-position)}.to-pink-50{--tw-gradient-to:#fdf2f8 var(--tw-gradient-to-position)}.to-pink-500\/10{--tw-gradient-to:rgb(236 72 153 / .1) var(--tw-gradient-to-position)}.to-purple-200{--tw-gradient-to:#e9d5ff var(--tw-gradient-to-position)}.to-purple-50{--tw-gradient-to:#faf5ff var(--tw-gradient-to-position)}.to-purple-500{--tw-gradient-to:#a855f7 var(--tw-gradient-to-position)}.to-purple-600{--tw-gradient-to:#9333ea var(--tw-gradient-to-position)}.to-purple-600\/10{--tw-gradient-to:rgb(147 51 234 / .1) var(--tw-gradient-to-position)}.to-purple-700{--tw-gradient-to:#7e22ce var(--tw-gradient-to-position)}.to-red-50{--tw-gradient-to:#fef2f2 var(--tw-gradient-to-position)}.to-red-500\/10{--tw-gradient-to:rgb(239 68 68 / .1) var(--tw-gradient-to-position)}.to-red-600{--tw-gradient-to:#dc2626 var(--tw-gradient-to-position)}.to-rose-500{--tw-gradient-to:#f43f5e var(--tw-gradient-to-position)}.to-slate-100{--tw-gradient-to:#f1f5f9 var(--tw-gradient-to-position)}.to-slate-600{--tw-gradient-to:#475569 var(--tw-gradient-to-position)}.to-teal-50{--tw-gradient-to:#f0fdfa var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.to-violet-500\/10{--tw-gradient-to:rgb(139 92 246 / .1) var(--tw-gradient-to-position)}.to-white{--tw-gradient-to:#fff var(--tw-gradient-to-position)}.to-white\/70{--tw-gradient-to:rgb(255 255 255 / .7) var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-20{padding-top:5rem;padding-bottom:5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pb-20{padding-bottom:5rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-10{padding-left:2.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pr-20{padding-right:5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-6xl{font-size:3.75rem;line-height:1}.text-8xl{font-size:6rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-relaxed{line-height:1.625}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-accent-primary{--tw-text-opacity:1;color:rgb(59 130 246 / var(--tw-text-opacity,1))}.text-amber-500{--tw-text-opacity:1;color:rgb(245 158 11 / var(--tw-text-opacity,1))}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6 / var(--tw-text-opacity,1))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14 / var(--tw-text-opacity,1))}.text-amber-900{--tw-text-opacity:1;color:rgb(120 53 15 / var(--tw-text-opacity,1))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0 / var(--tw-text-opacity,1))}.text-blue-100{--tw-text-opacity:1;color:rgb(219 234 254 / var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254 / var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250 / var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246 / var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235 / var(--tw-text-opacity,1))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216 / var(--tw-text-opacity,1))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175 / var(--tw-text-opacity,1))}.text-blue-900{--tw-text-opacity:1;color:rgb(30 58 138 / var(--tw-text-opacity,1))}.text-current{color:currentColor}.text-cyan-600{--tw-text-opacity:1;color:rgb(8 145 178 / var(--tw-text-opacity,1))}.text-emerald-600{--tw-text-opacity:1;color:rgb(5 150 105 / var(--tw-text-opacity,1))}.text-emerald-800{--tw-text-opacity:1;color:rgb(6 95 70 / var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175 / var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128 / var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99 / var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81 / var(--tw-text-opacity,1))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55 / var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39 / var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128 / var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94 / var(--tw-text-opacity,1))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74 / var(--tw-text-opacity,1))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61 / var(--tw-text-opacity,1))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52 / var(--tw-text-opacity,1))}.text-green-900{--tw-text-opacity:1;color:rgb(20 83 45 / var(--tw-text-opacity,1))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229 / var(--tw-text-opacity,1))}.text-indigo-800{--tw-text-opacity:1;color:rgb(55 48 163 / var(--tw-text-opacity,1))}.text-indigo-900{--tw-text-opacity:1;color:rgb(49 46 129 / var(--tw-text-opacity,1))}.text-light-text{--tw-text-opacity:1;color:rgb(26 32 44 / var(--tw-text-opacity,1))}.text-light-text-muted{--tw-text-opacity:1;color:rgb(74 85 104 / var(--tw-text-opacity,1))}.text-mercedes-black{--tw-text-opacity:1;color:rgb(0 0 0 / var(--tw-text-opacity,1))}.text-mercedes-silver{--tw-text-opacity:1;color:rgb(192 192 192 / var(--tw-text-opacity,1))}.text-orange-600{--tw-text-opacity:1;color:rgb(234 88 12 / var(--tw-text-opacity,1))}.text-orange-700{--tw-text-opacity:1;color:rgb(194 65 12 / var(--tw-text-opacity,1))}.text-orange-800{--tw-text-opacity:1;color:rgb(154 52 18 / var(--tw-text-opacity,1))}.text-purple-600{--tw-text-opacity:1;color:rgb(147 51 234 / var(--tw-text-opacity,1))}.text-purple-800{--tw-text-opacity:1;color:rgb(107 33 168 / var(--tw-text-opacity,1))}.text-purple-900{--tw-text-opacity:1;color:rgb(88 28 135 / var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113 / var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68 / var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38 / var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28 / var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27 / var(--tw-text-opacity,1))}.text-red-900{--tw-text-opacity:1;color:rgb(127 29 29 / var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184 / var(--tw-text-opacity,1))}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139 / var(--tw-text-opacity,1))}.text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105 / var(--tw-text-opacity,1))}.text-slate-700{--tw-text-opacity:1;color:rgb(51 65 85 / var(--tw-text-opacity,1))}.text-slate-800{--tw-text-opacity:1;color:rgb(30 41 59 / var(--tw-text-opacity,1))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.text-teal-600{--tw-text-opacity:1;color:rgb(13 148 136 / var(--tw-text-opacity,1))}.text-transparent{color:transparent}.text-white{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21 / var(--tw-text-opacity,1))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8 / var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4 / var(--tw-text-opacity,1))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7 / var(--tw-text-opacity,1))}.text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14 / var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.overline{text-decoration-line:overline}.placeholder-gray-400::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175 / var(--tw-placeholder-opacity,1))}.placeholder-gray-400::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175 / var(--tw-placeholder-opacity,1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128 / var(--tw-placeholder-opacity,1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128 / var(--tw-placeholder-opacity,1))}.placeholder-slate-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(100 116 139 / var(--tw-placeholder-opacity,1))}.placeholder-slate-500::placeholder{--tw-placeholder-opacity:1;color:rgb(100 116 139 / var(--tw-placeholder-opacity,1))}.opacity-0{opacity:0}.opacity-10{opacity:.1}.opacity-100{opacity:1}.opacity-15{opacity:.15}.opacity-25{opacity:.25}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 rgb(0 0 0 / .1),0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgb(0 0 0 / .1),0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0 / var(--tw-ring-opacity,1))}.ring-opacity-5{--tw-ring-opacity:.05}.blur{--tw-blur:blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-sm{--tw-blur:blur(4px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-sm{--tw-drop-shadow:drop-shadow(0 1px 1px rgb(0 0 0 / .05));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.\!filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) !important}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(40px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms}.duration-1000{transition-duration:1000ms}.duration-200{transition-duration:200ms}.duration-300{transition-duration:300ms}.duration-500{transition-duration:500ms}.ease-in{transition-timing-function:cubic-bezier(0.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(0.4,0,0.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,0.2,1)}.flash-message{position:fixed;top:1rem;right:1rem;z-index:50;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:1rem;border-width:1px;padding-left:1.5rem;padding-right:1.5rem;padding-top:1rem;padding-bottom:1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:500ms;background:rgba(255,255,255,0.08);backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);-webkit-backdrop-filter:blur(40px) saturate(200%) brightness(130%) contrast(110%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 32px 64px rgba(0,0,0,0.25),0 12px 24px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.1);animation:flash-slide-in .5s cubic-bezier(0.4,0,0.2,1);transition:all .5s cubic-bezier(0.4,0,0.2,1)}.dark .flash-message{background:rgba(0,0,0,0.2);backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(115%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(120%) contrast(115%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 32px 64px rgba(0,0,0,0.6),0 12px 24px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}.flash-message:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 40px 80px rgba(0,0,0,0.3),0 16px 32px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.5),0 0 0 1px rgba(255,255,255,0.15)}.dark .flash-message:hover{box-shadow:0 40px 80px rgba(0,0,0,0.7),0 16px 32px rgba(0,0,0,0.5),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.1)}.flash-message.info{--tw-text-opacity:1;color:rgb(219 234 254 / var(--tw-text-opacity,1));background:linear-gradient(135deg,rgba(59,130,246,0.2) 0,rgba(147,197,253,0.15) 50%,rgba(59,130,246,0.1) 100%);border:1px solid rgba(59,130,246,0.3)}.flash-message.success{--tw-text-opacity:1;color:rgb(220 252 231 / var(--tw-text-opacity,1));background:linear-gradient(135deg,rgba(34,197,94,0.2) 0,rgba(134,239,172,0.15) 50%,rgba(34,197,94,0.1) 100%);border:1px solid rgba(34,197,94,0.3)}.flash-message.warning{--tw-text-opacity:1;color:rgb(254 249 195 / var(--tw-text-opacity,1));background:linear-gradient(135deg,rgba(245,158,11,0.2) 0,rgba(252,211,77,0.15) 50%,rgba(245,158,11,0.1) 100%);border:1px solid rgba(245,158,11,0.3)}.flash-message.error{--tw-text-opacity:1;color:rgb(254 226 226 / var(--tw-text-opacity,1));background:linear-gradient(135deg,rgba(239,68,68,0.2) 0,rgba(252,165,165,0.15) 50%,rgba(239,68,68,0.1) 100%);border:1px solid rgba(239,68,68,0.3)}@keyframes flash-slide-in{0%{opacity:0;transform:translateX(100%) translateY(-20px) scale(0.9);-webkit-backdrop-filter:blur(0);backdrop-filter:blur(0)}50%{opacity:.8;transform:translateX(20px) translateY(-10px) scale(1.05);-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px)}100%{opacity:1;transform:translateX(0) translateY(0) scale(1);-webkit-backdrop-filter:blur(40px);backdrop-filter:blur(40px)}}@keyframes flash-slide-out{0%{opacity:1;transform:translateX(0) translateY(0) scale(1)}100%{opacity:0;transform:translateX(100%) translateY(-20px) scale(0.9)}}.flash-message.hiding{animation:flash-slide-out .4s cubic-bezier(0.4,0,0.2,1) forwards}.dnd-toggle{position:relative;display:inline-flex;height:1.5rem;width:2.75rem;align-items:center;border-radius:9999px;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.dnd-toggle:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246 / var(--tw-ring-opacity,1));--tw-ring-offset-width:2px}.dnd-toggle{background:rgba(156,163,175,0.3);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border:1px solid rgba(156,163,175,0.2)}.dnd-toggle.active{background:rgba(239,68,68,0.3);border:1px solid rgba(239,68,68,0.4)}.dnd-toggle-slider{display:inline-block;height:1rem;width:1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:9999px;--tw-shadow:0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:transform;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms;background:rgba(255,255,255,0.9);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.3);box-shadow:0 4px 8px rgba(0,0,0,0.2),0 2px 4px rgba(0,0,0,0.1);margin:.125rem}.dnd-toggle.active .dnd-toggle-slider{transform:translateX(1.25rem);background:rgba(255,255,255,1);box-shadow:0 6px 12px rgba(239,68,68,0.3),0 3px 6px rgba(239,68,68,0.2)}.dnd-indicator{position:fixed;top:1rem;left:1rem;z-index:50;display:flex;align-items:center;border-radius:.5rem;padding-left:.75rem;padding-right:.75rem;padding-top:.5rem;padding-bottom:.5rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms;background:rgba(239,68,68,0.1);backdrop-filter:blur(20px) saturate(150%);-webkit-backdrop-filter:blur(20px) saturate(150%);border:1px solid rgba(239,68,68,0.3);color:#ef4444;transform:translateY(-100%);opacity:0}.dnd-indicator.active{transform:translateY(0);opacity:1}.dnd-modal{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;padding:1rem;background:rgba(0,0,0,0.3);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}.dnd-modal-content{width:100%;max-width:28rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:1rem;padding:1.5rem;--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms;background:rgba(255,255,255,0.1);backdrop-filter:blur(40px) saturate(200%) brightness(120%);-webkit-backdrop-filter:blur(40px) saturate(200%) brightness(120%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 25px 50px rgba(0,0,0,0.25),0 8px 16px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.4)}.dark .dnd-modal-content{background:rgba(0,0,0,0.3);backdrop-filter:blur(40px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(40px) saturate(180%) brightness(110%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 25px 50px rgba(0,0,0,0.6),0 8px 16px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2)}.flash-message.dnd-suppressed{animation:flash-fade-in .3s ease-out;opacity:.3;transform:scale(0.95);pointer-events:none}@keyframes flash-fade-in{0%{opacity:0;transform:scale(0.9)}100%{opacity:.3;transform:scale(0.95)}}.dnd-counter{position:absolute;top:-.5rem;right:-.5rem;display:flex;height:1.25rem;width:1.25rem;align-items:center;justify-content:center;border-radius:9999px;--tw-bg-opacity:1;background-color:rgb(239 68 68 / var(--tw-bg-opacity,1));font-size:.75rem;line-height:1rem;font-weight:700;--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1));background:rgba(239,68,68,0.9);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.2);box-shadow:0 2px 4px rgba(0,0,0,0.2);animation:dnd-counter-bounce .5s ease-out}@keyframes dnd-counter-bounce{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}}@keyframes slide-down{0%{opacity:0;transform:translateY(-20px)}100%{opacity:1;transform:translateY(0)}}.mercedes-background::before{content:'';position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;background-image:url("data:image/svg+xml,%3Csvgxmlns='http://www.w3.org/2000/svg'viewBox='008080'width='80'height='80'opacity='0.03'fill='currentColor'%3E%3Cpathd='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.5C27,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.5zM4,40c0-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,40zM58.6,70.8C53.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.9C67.6,63.9,63.4,67.9,58.6,70.8zM58.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,40c0,6.2-1.5,12-4.3,17.1L58.6,46.8z'/%3E%3C/svg%3E");background-position:center;background-repeat:repeat;background-size:120px 120px;pointer-events:none;opacity:.03;transition:opacity .3s ease}.dark .mercedes-background::before{opacity:.015;filter:invert(1) brightness(0.3);background-size:150px 150px}.navbar{position:sticky !important;top:0 !important;z-index:50 !important;width:100% !important;left:0 !important;right:0 !important;--navbar-blur:40px;--navbar-opacity:.15;background:rgba(255,255,255,var(--navbar-opacity,0.15)) !important;backdrop-filter:blur(var(--navbar-blur,40px)) saturate(200%) brightness(110%) contrast(105%) !important;-webkit-backdrop-filter:blur(var(--navbar-blur,40px)) saturate(200%) brightness(110%) contrast(105%) !important;box-shadow:0 8px 32px rgba(0,0,0,0.12),0 2px 8px rgba(0,0,0,0.08),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.15) !important;border-bottom:1px solid rgba(255,255,255,0.2) !important;transition:all .3s cubic-bezier(0.4,0,0.2,1) !important}.dark .navbar{--navbar-dark-opacity:.25;background:rgba(0,0,0,var(--navbar-dark-opacity,0.25)) !important;backdrop-filter:blur(calc(var(--navbar-blur, 40px) + 5px)) saturate(180%) brightness(120%) contrast(115%) !important;-webkit-backdrop-filter:blur(calc(var(--navbar-blur, 40px) + 5px)) saturate(180%) brightness(120%) contrast(115%) !important;box-shadow:0 8px 32px rgba(0,0,0,0.4),0 2px 8px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.15),0 0 0 1px rgba(255,255,255,0.08) !important;border-bottom:1px solid rgba(255,255,255,0.1) !important}.navbar.scrolled{--navbar-blur:50px;--navbar-opacity:.25;background:rgba(255,255,255,var(--navbar-opacity,0.25)) !important;backdrop-filter:blur(var(--navbar-blur,50px)) saturate(220%) brightness(115%) contrast(110%) !important;-webkit-backdrop-filter:blur(var(--navbar-blur,50px)) saturate(220%) brightness(115%) contrast(110%) !important;box-shadow:0 12px 40px rgba(0,0,0,0.15),0 4px 12px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.4),0 0 0 1px rgba(255,255,255,0.2) !important}.dark .navbar.scrolled{--navbar-dark-opacity:.35;background:rgba(0,0,0,var(--navbar-dark-opacity,0.35)) !important;backdrop-filter:blur(calc(var(--navbar-blur, 50px) + 5px)) saturate(200%) brightness(125%) contrast(120%) !important;-webkit-backdrop-filter:blur(calc(var(--navbar-blur, 50px) + 5px)) saturate(200%) brightness(125%) contrast(120%) !important;box-shadow:0 12px 40px rgba(0,0,0,0.5),0 4px 12px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.1) !important}.navbar-menu-new{display:flex;align-items:center;justify-content:center}.navbar-menu-new>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.125rem * var(--tw-space-x-reverse));margin-left:calc(0.125rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}@media(min-width:768px){.navbar-menu-new>:not([hidden]) ~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.25rem * var(--tw-space-x-reverse));margin-left:calc(0.25rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}}.navbar-menu-new{max-width:100%;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;background:rgba(255,255,255,0.1);backdrop-filter:blur(25px) saturate(170%) brightness(108%);-webkit-backdrop-filter:blur(25px) saturate(170%) brightness(108%);border-radius:16px;padding:8px;margin:0 16px;border:1px solid rgba(255,255,255,0.15);box-shadow:0 6px 20px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05);transition:all .3s cubic-bezier(0.4,0,0.2,1)}.dark .navbar-menu-new{background:rgba(0,0,0,0.2);backdrop-filter:blur(30px) saturate(150%) brightness(115%);-webkit-backdrop-filter:blur(30px) saturate(150%) brightness(115%);border:1px solid rgba(255,255,255,0.1);box-shadow:0 6px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.1),0 0 0 1px rgba(255,255,255,0.03)}.navbar-menu-new::-webkit-scrollbar{display:none}.navbar-menu-new:hover{backdrop-filter:blur(35px) saturate(190%) brightness(112%);-webkit-backdrop-filter:blur(35px) saturate(190%) brightness(112%);box-shadow:0 8px 25px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.1);transform:translateY(-1px)}.dark .navbar-menu-new:hover{backdrop-filter:blur(40px) saturate(170%) brightness(120%);-webkit-backdrop-filter:blur(40px) saturate(170%) brightness(120%);box-shadow:0 8px 25px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.15),0 0 0 1px rgba(255,255,255,0.05)}.nav-item{display:flex;align-items:center}.nav-item>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.375rem * var(--tw-space-x-reverse));margin-left:calc(0.375rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.nav-item{border-radius:.75rem;padding-left:.75rem;padding-right:.75rem;padding-top:.625rem;padding-bottom:.625rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms;color:rgba(15,23,42,0.85);background:rgba(255,255,255,0.08);backdrop-filter:blur(15px) saturate(140%);-webkit-backdrop-filter:blur(15px) saturate(140%);border:1px solid rgba(255,255,255,0.1);box-shadow:0 4px 12px rgba(0,0,0,0.05),inset 0 1px 0 rgba(255,255,255,0.15);position:relative;overflow:hidden;animation:nav-item-entrance .6s ease-out}.nav-item::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 .5s}.nav-item:hover::before{left:100%}.nav-item::after{content:'';position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:conic-gradient(from 0 at 50% 50%,transparent 0,rgba(255,255,255,0.1) 30deg,transparent 60deg);opacity:0;transition:opacity .3s ease;pointer-events:none;animation:rotate 3s linear infinite}.nav-item:hover::after{opacity:1}.dark .nav-item{color:rgba(255,255,255,0.85);background:rgba(0,0,0,0.15);backdrop-filter:blur(20px) saturate(130%);-webkit-backdrop-filter:blur(20px) saturate(130%);border:1px solid rgba(255,255,255,0.08);box-shadow:0 4px 12px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.08)}.nav-item:hover{color:rgba(15,23,42,1);background:rgba(255,255,255,0.2);backdrop-filter:blur(25px) saturate(160%) brightness(110%);-webkit-backdrop-filter:blur(25px) saturate(160%) brightness(110%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 8px 20px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.3),0 0 0 1px rgba(255,255,255,0.1);transform:translateY(-2px) scale(1.02)}.dark .nav-item:hover{color:rgba(255,255,255,1);background:rgba(0,0,0,0.25);backdrop-filter:blur(30px) saturate(150%) brightness(120%);-webkit-backdrop-filter:blur(30px) saturate(150%) brightness(120%);border:1px solid rgba(255,255,255,0.15);box-shadow:0 8px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.15),0 0 0 1px rgba(255,255,255,0.05)}.nav-item.active{color:rgba(15,23,42,1);background:rgba(255,255,255,0.35);backdrop-filter:blur(35px) saturate(180%) brightness(115%);-webkit-backdrop-filter:blur(35px) saturate(180%) brightness(115%);border:1px solid rgba(255,255,255,0.4);box-shadow:0 12px 24px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.5),0 0 0 1px rgba(59,130,246,0.3);transform:translateY(-1px);animation:nav-item-active-glow 2s ease-in-out infinite alternate}.dark .nav-item.active{color:rgba(255,255,255,1);background:rgba(0,0,0,0.4);backdrop-filter:blur(40px) saturate(160%) brightness(125%);-webkit-backdrop-filter:blur(40px) saturate(160%) brightness(125%);border:1px solid rgba(255,255,255,0.2);box-shadow:0 12px 24px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(59,130,246,0.2)}@keyframes nav-item-entrance{0%{opacity:0;transform:translateY(10px) scale(0.95);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}100%{opacity:1;transform:translateY(0) scale(1);-webkit-backdrop-filter:blur(15px) saturate(140%);backdrop-filter:blur(15px) saturate(140%)}}@keyframes nav-item-active-glow{0%{box-shadow:0 12px 24px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.5),0 0 0 1px rgba(59,130,246,0.3)}100%{box-shadow:0 16px 32px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.6),0 0 0 2px rgba(59,130,246,0.5)}}@keyframes rotate{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.navbar::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at 20% 50%,rgba(255,255,255,0.1) 1px,transparent 1px),radial-gradient(circle at 80% 50%,rgba(255,255,255,0.1) 1px,transparent 1px),radial-gradient(circle at 40% 20%,rgba(255,255,255,0.05) 1px,transparent 1px),radial-gradient(circle at 60% 80%,rgba(255,255,255,0.05) 1px,transparent 1px);opacity:0;animation:glassmorphism-particles 8s ease-in-out infinite;pointer-events:none}.dark .navbar::before{background:radial-gradient(circle at 20% 50%,rgba(255,255,255,0.05) 1px,transparent 1px),radial-gradient(circle at 80% 50%,rgba(255,255,255,0.05) 1px,transparent 1px),radial-gradient(circle at 40% 20%,rgba(255,255,255,0.03) 1px,transparent 1px),radial-gradient(circle at 60% 80%,rgba(255,255,255,0.03) 1px,transparent 1px)}@keyframes glassmorphism-particles{0%,100%{opacity:0;transform:scale(1)}50%{opacity:1;transform:scale(1.1)}}.dark-mode-toggle-new{position:relative;display:flex;cursor:pointer;align-items:center;justify-content:center;border-radius:9999px;padding:.5rem;transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms;background:rgba(241,245,249,0.8);border:1px solid rgba(255,255,255,0.7);box-shadow:0 2px 8px rgba(0,0,0,0.05),0 1px 2px rgba(0,0,0,0.04);color:#334155;z-index:100}.dark-mode-toggle-new:hover{--tw-translate-y:-.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));background:rgba(241,245,249,0.9);box-shadow:0 8px 16px rgba(0,0,0,0.08),0 2px 4px rgba(0,0,0,0.06)}.dark-mode-toggle-new:active{--tw-scale-x:.95;--tw-scale-y:.95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transition:transform .1s}.dark .dark-mode-toggle-new{background:rgba(30,41,59,0.8);border:1px solid rgba(255,255,255,0.1);box-shadow:0 2px 8px rgba(0,0,0,0.2),0 1px 2px rgba(0,0,0,0.1);color:#e2e8f0}.dark .dark-mode-toggle-new:hover{background:rgba(30,41,59,0.9);box-shadow:0 8px 16px rgba(0,0,0,0.2),0 2px 4px rgba(0,0,0,0.15)}.dark-mode-toggle-new .sun-icon,.dark-mode-toggle-new .moon-icon{position:absolute;top:50%;left:50%;--tw-translate-x:-50%;--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.dark-mode-toggle-new .sun-icon:not(.hidden){animation:spin-in .5s cubic-bezier(0.25,1,0.5,1) forwards}.dark-mode-toggle-new .moon-icon:not(.hidden){animation:spin-in .5s cubic-bezier(0.25,1,0.5,1) forwards}@keyframes spin-in{0%{opacity:0;transform:translateY(10px) scale(0.7) rotate(20deg)}100%{opacity:1;transform:translateY(0) scale(1) rotate(0)}}.dark .sun-icon{display:none}.dark .moon-icon{display:block}.sun-icon{display:block}.moon-icon{display:none}.user-menu-button-new{display:flex;align-items:center}.user-menu-button-new>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.375rem * var(--tw-space-x-reverse));margin-left:calc(0.375rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.user-menu-button-new{border-radius:.5rem;padding:.25rem;transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms;background:rgba(241,245,249,0.6);border:1px solid rgba(255,255,255,0.6);box-shadow:0 2px 8px rgba(0,0,0,0.04),0 1px 2px rgba(0,0,0,0.02)}.user-menu-button-new:hover{--tw-translate-y:-.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));background:rgba(241,245,249,0.8);box-shadow:0 8px 16px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04)}.dark .user-menu-button-new{background:rgba(30,41,59,0.6);border:1px solid rgba(255,255,255,0.08);box-shadow:0 2px 8px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.1)}.dark .user-menu-button-new:hover{background:rgba(30,41,59,0.8);box-shadow:0 8px 16px rgba(0,0,0,0.15),0 2px 4px rgba(0,0,0,0.1)}.user-avatar-new{display:flex;height:1.75rem;width:1.75rem;align-items:center;justify-content:center;border-radius:9999px;font-size:.75rem;line-height:1rem;font-weight:600;--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1));--tw-shadow:0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms;background:linear-gradient(135deg,#000,#333);box-shadow:0 2px 4px rgba(0,0,0,0.2),0 1px 2px rgba(0,0,0,0.1)}.dark .user-avatar-new{background:linear-gradient(135deg,#f8fafc,#e2e8f0);color:#0f172a;box-shadow:0 2px 4px rgba(0,0,0,0.3),0 1px 2px rgba(0,0,0,0.2)}.login-button-new{display:flex;align-items:center;border-radius:.5rem;padding-left:.75rem;padding-right:.75rem;padding-top:.375rem;padding-bottom:.375rem;font-size:.75rem;line-height:1rem;font-weight:500;--tw-shadow:0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms;background:#000;color:#fff;border:1px solid rgba(255,255,255,0.1);box-shadow:0 2px 8px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.08)}.login-button-new:hover{--tw-translate-y:-.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));background:#333;box-shadow:0 8px 16px rgba(0,0,0,0.15),0 3px 4px rgba(0,0,0,0.1)}.dark .login-button-new{background:#fff;color:#000;border:1px solid rgba(0,0,0,0.1);box-shadow:0 2px 8px rgba(0,0,0,0.2),0 1px 2px rgba(0,0,0,0.15)}.dark .login-button-new:hover{background:#f1f5f9;box-shadow:0 8px 16px rgba(0,0,0,0.25),0 3px 4px rgba(0,0,0,0.2)}.mobile-menu-new{z-index:40;width:100%;overflow:hidden;transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms;background:rgba(255,255,255,0.8);backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);box-shadow:0 4px 20px rgba(0,0,0,0.06);border-bottom:1px solid rgba(241,245,249,0.8);max-height:0;opacity:0}.mobile-menu-new.open{max-height:400px;opacity:1;border-bottom:1px solid rgba(241,245,249,0.8)}.dark .mobile-menu-new{background:rgba(15,23,42,0.8);box-shadow:0 4px 20px rgba(0,0,0,0.2);border-bottom:1px solid rgba(30,41,59,0.8)}.mobile-nav-item{display:flex;align-items:center}.mobile-nav-item>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.625rem * var(--tw-space-x-reverse));margin-left:calc(0.625rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.mobile-nav-item{border-radius:.5rem;padding-left:.75rem;padding-right:.75rem;padding-top:.625rem;padding-bottom:.625rem;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(30 41 59 / var(--tw-text-opacity,1));transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.mobile-nav-item:is(.dark *){--tw-text-opacity:1;color:rgb(226 232 240 / var(--tw-text-opacity,1))}.mobile-nav-item:hover{background:rgba(241,245,249,0.8)}.dark .mobile-nav-item:hover{background:rgba(30,41,59,0.6)}.mobile-nav-item.active{background:rgba(241,245,249,0.9);color:#000;font-weight:500}.dark .mobile-nav-item.active{background:rgba(30,41,59,0.8);color:#fff}.mb-stat-card{background:linear-gradient(135deg,rgba(240,249,255,0.6) 0,rgba(230,242,255,0.6) 100%);color:#0f172a;position:relative;overflow:hidden;border:0;border-radius:var(--card-radius);backdrop-filter:blur(20px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(180%) brightness(110%);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 0 0 1px rgba(255,255,255,0.1);padding:1.5rem;margin:1rem;transition:transform .3s ease,box-shadow .3s ease}.dark .mb-stat-card{background:linear-gradient(135deg,rgba(0,0,0,0.7) 0,rgba(10,10,10,0.7) 100%);color:var(--text-primary,#f8fafc);box-shadow:0 25px 50px rgba(0,0,0,0.3),0 0 0 1px rgba(255,255,255,0.05)}.stats-card,.job-card{border-radius:.75rem;border-width:1px;border-color:rgb(229 231 235 / .7);background-color:rgb(255 255 255 / .6);--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(40px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.stats-card:is(.dark *),.job-card:is(.dark *){border-color:rgb(51 65 85 / .2);background-color:rgb(0 0 0 / .8)}.stats-card,.job-card{backdrop-filter:blur(24px) saturate(200%) brightness(120%);-webkit-backdrop-filter:blur(24px) saturate(200%) brightness(120%);box-shadow:0 25px 50px rgba(0,0,0,0.2),0 0 0 1px rgba(255,255,255,0.1);border-radius:var(--card-radius)}footer{transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms;background:rgba(255,255,255,0.1);backdrop-filter:blur(30px) saturate(180%) brightness(120%);-webkit-backdrop-filter:blur(30px) saturate(180%) brightness(120%);border-top:1px solid rgba(255,255,255,0.2);box-shadow:0 -8px 32px rgba(0,0,0,0.1),0 -2px 8px rgba(0,0,0,0.05),inset 0 1px 0 rgba(255,255,255,0.2),0 0 0 1px rgba(255,255,255,0.05)}.dark footer{background:rgba(0,0,0,0.3);backdrop-filter:blur(30px) saturate(160%) brightness(110%);-webkit-backdrop-filter:blur(30px) saturate(160%) brightness(110%);border-top:1px solid rgba(255,255,255,0.1);box-shadow:0 -8px 32px rgba(0,0,0,0.3),0 -2px 8px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.1),0 0 0 1px rgba(255,255,255,0.03)}.dropdown-arrow{transition-property:transform;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.mercedes-star-bg{position:relative}.mercedes-star-bg::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background-image:url("data:image/svg+xml,%3Csvgxmlns='http://www.w3.org/2000/svg'viewBox='008080'width='80'height='80'opacity='0.05'fill='currentColor'%3E%3Cpathd='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.5C27,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.5zM4,40c0-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,40zM58.6,70.8C53.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.9C67.6,63.9,63.4,67.9,58.6,70.8zM58.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,40c0,6.2-1.5,12-4.3,17.1L58.6,46.8z'/%3E%3C/svg%3E");background-position:center;background-repeat:repeat;background-size:40px 40px;z-index:-1;opacity:.05}.dark .mercedes-star-bg::after{opacity:.02;filter:invert(1) brightness(0.4)}.glass-effect{backdrop-filter:blur(20px) saturate(180%) brightness(110%);-webkit-backdrop-filter:blur(20px) saturate(180%) brightness(110%);background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);box-shadow:0 8px 32px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.3)}.dark .glass-effect{background:rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.1);box-shadow:0 8px 32px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.15)}.glass-hover{transition:all .3s cubic-bezier(0.4,0,0.2,1)}.glass-hover:hover{transform:translateY(-2px);backdrop-filter:blur(25px) saturate(200%) brightness(120%);-webkit-backdrop-filter:blur(25px) saturate(200%) brightness(120%);box-shadow:0 20px 40px rgba(0,0,0,0.15),0 8px 16px rgba(0,0,0,0.1),inset 0 1px 0 rgba(255,255,255,0.4)}.dark .glass-hover:hover{box-shadow:0 20px 40px rgba(0,0,0,0.4),0 8px 16px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.2)}.printer-card-new{position:relative;overflow:hidden;border-radius:.75rem;border-width:1px;border-color:rgb(229 231 235 / .7);background-image:linear-gradient(to bottom right,var(--tw-gradient-stops));--tw-gradient-from:rgb(255 255 255 / .9) var(--tw-gradient-from-position);--tw-gradient-to:rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to);--tw-gradient-to:rgb(255 255 255 / .7) var(--tw-gradient-to-position);padding:1.25rem;--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(40px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.printer-card-new:hover{--tw-translate-y:-.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.printer-card-new:is(.dark *){border-color:rgb(51 65 85 / .3);--tw-gradient-from:rgb(30 41 59 / .9) var(--tw-gradient-from-position);--tw-gradient-to:rgb(30 41 59 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to);--tw-gradient-to:rgb(15 23 42 / .7) var(--tw-gradient-to-position)}.printer-card-new{box-shadow:0 20px 40px rgba(0,0,0,0.08),0 10px 20px rgba(0,0,0,0.06),0 0 0 1px rgba(255,255,255,0.1);border-radius:var(--card-radius,1rem)}.dark .printer-card-new{box-shadow:0 20px 40px rgba(0,0,0,0.4),0 10px 20px rgba(0,0,0,0.3),0 0 0 1px rgba(255,255,255,0.05)}.printer-card-new.online{--tw-border-opacity:1;border-color:rgb(187 247 208 / var(--tw-border-opacity,1));background-image:linear-gradient(to bottom right,var(--tw-gradient-stops));--tw-gradient-from:rgb(240 253 244 / .9) var(--tw-gradient-from-position);--tw-gradient-to:rgb(240 253 244 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to);--tw-gradient-to:rgb(236 253 245 / .8) var(--tw-gradient-to-position)}.printer-card-new.online:is(.dark *){border-color:rgb(21 128 61 / .5);--tw-gradient-from:rgb(20 83 45 / .3) var(--tw-gradient-from-position);--tw-gradient-to:rgb(20 83 45 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to);--tw-gradient-to:rgb(6 78 59 / .2) var(--tw-gradient-to-position)}.printer-card-new.online{box-shadow:0 20px 40px rgba(0,122,85,0.08),0 10px 20px rgba(0,122,85,0.06),0 0 0 1px rgba(209,250,229,0.4)}.dark .printer-card-new.online{box-shadow:0 20px 40px rgba(0,0,0,0.3),0 10px 20px rgba(0,0,0,0.2),0 0 0 1px rgba(16,185,129,0.2)}.status-badge-new{display:inline-flex;align-items:center}.status-badge-new>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.25rem * var(--tw-space-x-reverse));margin-left:calc(0.25rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.status-badge-new{border-radius:9999px;padding-left:.625rem;padding-right:.625rem;padding-top:.25rem;padding-bottom:.25rem;font-size:.75rem;line-height:1rem;font-weight:500;--tw-shadow:0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);background:rgba(255,255,255,0.9);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:0 2px 5px rgba(0,0,0,0.05)}.dark .status-badge-new{background:rgba(30,41,59,0.7);box-shadow:0 2px 5px rgba(0,0,0,0.2)}.status-badge-new.online{background-color:rgb(220 252 231 / .9);--tw-text-opacity:1;color:rgb(22 101 52 / var(--tw-text-opacity,1))}.status-badge-new.online:is(.dark *){background-color:rgb(20 83 45 / .6);--tw-text-opacity:1;color:rgb(134 239 172 / var(--tw-text-opacity,1))}.status-badge-new.offline{background-color:rgb(254 226 226 / .9);--tw-text-opacity:1;color:rgb(153 27 27 / var(--tw-text-opacity,1))}.status-badge-new.offline:is(.dark *){background-color:rgb(127 29 29 / .6);--tw-text-opacity:1;color:rgb(252 165 165 / var(--tw-text-opacity,1))}.filter-bar-new{border-radius:.5rem;border-width:1px;border-color:rgb(229 231 235 / .6);background-color:rgb(255 255 255 / .8);padding:.375rem;--tw-shadow:0 20px 25px -5px rgb(0 0 0 / .1),0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.filter-bar-new:is(.dark *){border-color:rgb(51 65 85 / .3);background-color:rgb(30 41 59 / .8)}.filter-bar-new{box-shadow:0 10px 25px rgba(0,0,0,0.05),0 5px 10px rgba(0,0,0,0.03),0 0 0 1px rgba(255,255,255,0.2)}.dark .filter-bar-new{box-shadow:0 10px 25px rgba(0,0,0,0.2),0 5px 10px rgba(0,0,0,0.15),0 0 0 1px rgba(255,255,255,0.05)}.filter-btn-new{border-radius:.375rem;padding-left:.875rem;padding-right:.875rem;padding-top:.5rem;padding-bottom:.5rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.filter-btn-new.active{--tw-bg-opacity:1;background-color:rgb(0 0 0 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1));--tw-shadow:0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.filter-btn-new.active:is(.dark *){--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.filter-btn-new.active{box-shadow:0 4px 10px rgba(0,0,0,0.1)}.dark .filter-btn-new.active{box-shadow:0 4px 10px rgba(0,0,0,0.3)}.action-btn-new{display:flex;align-items:center;justify-content:center;gap:.5rem;border-radius:.5rem;padding-left:1rem;padding-right:1rem;padding-top:.625rem;padding-bottom:.625rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-shadow:0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.action-btn-new:hover{--tw-translate-y:-.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.action-btn-new{backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.action-btn-new.primary{--tw-bg-opacity:1;background-color:rgb(79 70 229 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.action-btn-new.primary:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202 / var(--tw-bg-opacity,1))}.action-btn-new.primary:is(.dark *){--tw-bg-opacity:1;background-color:rgb(79 70 229 / var(--tw-bg-opacity,1))}.action-btn-new.primary:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(99 102 241 / var(--tw-bg-opacity,1))}.action-btn-new.primary{box-shadow:0 5px 15px rgba(79,70,229,0.2)}.dark .action-btn-new.primary{box-shadow:0 5px 15px rgba(79,70,229,0.3)}.action-btn-new.success{--tw-bg-opacity:1;background-color:rgb(22 163 74 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.action-btn-new.success:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61 / var(--tw-bg-opacity,1))}.action-btn-new.success:is(.dark *){--tw-bg-opacity:1;background-color:rgb(22 163 74 / var(--tw-bg-opacity,1))}.action-btn-new.success:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(34 197 94 / var(--tw-bg-opacity,1))}.action-btn-new.success{box-shadow:0 5px 15px rgba(16,185,129,0.2)}.dark .action-btn-new.success{box-shadow:0 5px 15px rgba(16,185,129,0.3)}.action-btn-new.danger{--tw-bg-opacity:1;background-color:rgb(220 38 38 / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.action-btn-new.danger:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28 / var(--tw-bg-opacity,1))}.action-btn-new.danger:is(.dark *){--tw-bg-opacity:1;background-color:rgb(220 38 38 / var(--tw-bg-opacity,1))}.action-btn-new.danger:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(239 68 68 / var(--tw-bg-opacity,1))}.action-btn-new.danger{box-shadow:0 5px 15px rgba(239,68,68,0.2)}.dark .action-btn-new.danger{box-shadow:0 5px 15px rgba(239,68,68,0.3)}.printer-info-row{margin-bottom:.375rem;display:flex;align-items:center;gap:.5rem;font-size:.75rem;line-height:1rem;--tw-text-opacity:1;color:rgb(51 65 85 / var(--tw-text-opacity,1))}.printer-info-row:is(.dark *){--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}@media(min-width:640px){.printer-info-row{font-size:.875rem;line-height:1.25rem}}.printer-info-icon{height:.875rem;width:.875rem;flex-shrink:0;--tw-text-opacity:1;color:rgb(100 116 139 / var(--tw-text-opacity,1))}.printer-info-icon:is(.dark *){--tw-text-opacity:1;color:rgb(148 163 184 / var(--tw-text-opacity,1))}@media(min-width:640px){.printer-info-icon{height:1rem;width:1rem}}.online-indicator{position:absolute;top:.625rem;right:.625rem;height:.75rem;width:.75rem;border-radius:9999px;--tw-bg-opacity:1;background-color:rgb(34 197 94 / var(--tw-bg-opacity,1));--tw-shadow:0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);box-shadow:0 0 0 rgba(16,185,129,0.6);animation:pulse-ring 2s cubic-bezier(0.455,0.03,0.515,0.955) infinite}@keyframes pulse-ring{0%{box-shadow:0 0 0 0 rgba(16,185,129,0.6)}70%{box-shadow:0 0 0 6px rgba(16,185,129,0)}100%{box-shadow:0 0 0 0 rgba(16,185,129,0)}}.status-overview-new{display:flex;flex-wrap:wrap;gap:.75rem;border-radius:.5rem;border-width:1px;border-color:rgb(229 231 235 / .6);background-color:rgb(255 255 255 / .6);padding:.75rem;font-size:.75rem;line-height:1rem;--tw-shadow:0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.status-overview-new:is(.dark *){border-color:rgb(51 65 85 / .3);background-color:rgb(30 41 59 / .6)}@media(min-width:640px){.status-overview-new{font-size:.875rem;line-height:1.25rem}}.status-overview-new{box-shadow:0 10px 25px rgba(0,0,0,0.04),0 5px 10px rgba(0,0,0,0.02),0 0 0 1px rgba(255,255,255,0.1)}.dark .status-overview-new{box-shadow:0 10px 25px rgba(0,0,0,0.15),0 5px 10px rgba(0,0,0,0.1),0 0 0 1px rgba(255,255,255,0.03)}.status-dot{height:.625rem;width:.625rem;border-radius:9999px}.status-dot.online{--tw-bg-opacity:1;background-color:rgb(34 197 94 / var(--tw-bg-opacity,1));animation:pulse-dot 2s cubic-bezier(0.455,0.03,0.515,0.955) infinite}.status-dot.offline{--tw-bg-opacity:1;background-color:rgb(239 68 68 / var(--tw-bg-opacity,1))}@keyframes pulse-dot{0%{transform:scale(0.95);opacity:1}50%{transform:scale(1.1);opacity:.8}100%{transform:scale(0.95);opacity:1}}.modal-new{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background-color:rgb(0 0 0 / .4);padding:1rem;--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.modal-content-new{width:100%;max-width:28rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));border-radius:1rem;border-width:1px;border-color:rgb(229 231 235 / .6);background-color:rgb(255 255 255 / .9);padding:1.5rem;--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(40px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:300ms}.modal-content-new:is(.dark *){border-color:rgb(51 65 85 / .3);background-color:rgb(30 41 59 / .9)}.modal-content-new{box-shadow:0 25px 50px rgba(0,0,0,0.15),0 15px 30px rgba(0,0,0,0.1),0 20px 25px -5px rgba(0,0,0,0.5),0 10px 10px -5px rgba(0,0,0,0.3)}.user-dropdown-item{display:flex;cursor:pointer;align-items:center;padding-left:1rem;padding-right:1rem;padding-top:.75rem;padding-bottom:.75rem;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(51 65 85 / var(--tw-text-opacity,1));transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:200ms}.user-dropdown-item:hover{--tw-bg-opacity:1;background-color:rgb(248 250 252 / var(--tw-bg-opacity,1))}.user-dropdown-item:is(.dark *){--tw-text-opacity:1;color:rgb(226 232 240 / var(--tw-text-opacity,1))}.user-dropdown-item:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.user-dropdown-item:first-child{border-top-left-radius:.75rem;border-top-right-radius:.75rem}.user-dropdown-item:last-child{border-bottom-right-radius:.75rem;border-bottom-left-radius:.75rem}.user-dropdown-item:hover{background:rgba(248,250,252,0.8);transform:translateX(2px)}.dark .user-dropdown-item:hover{background:rgba(30,41,59,0.8)}.user-dropdown-icon{margin-right:.75rem;height:1rem;width:1rem;--tw-text-opacity:1;color:rgb(100 116 139 / var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:200ms}.user-dropdown-icon:is(.dark *){--tw-text-opacity:1;color:rgb(148 163 184 / var(--tw-text-opacity,1))}.user-dropdown-item:hover .user-dropdown-icon{--tw-text-opacity:1;color:rgb(51 65 85 / var(--tw-text-opacity,1))}.user-dropdown-item:hover .user-dropdown-icon:is(.dark *){--tw-text-opacity:1;color:rgb(226 232 240 / var(--tw-text-opacity,1))}.user-dropdown-divider{margin-top:.25rem;margin-bottom:.25rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 232 240 / var(--tw-border-opacity,1))}.user-dropdown-divider:is(.dark *){--tw-border-opacity:1;border-color:rgb(51 65 85 / var(--tw-border-opacity,1))}.user-info-section{border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(226 232 240 / var(--tw-border-opacity,1));padding-left:1rem;padding-right:1rem;padding-top:.75rem;padding-bottom:.75rem}.user-info-section:is(.dark *){--tw-border-opacity:1;border-color:rgb(51 65 85 / var(--tw-border-opacity,1))}.user-info-section{background:rgba(248,250,252,0.5)}.dark .user-info-section{background:rgba(30,41,59,0.5)}.user-info-name{font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.user-info-name:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.user-info-role{margin-top:.25rem;font-size:.75rem;line-height:1rem;--tw-text-opacity:1;color:rgb(100 116 139 / var(--tw-text-opacity,1))}.user-info-role:is(.dark *){--tw-text-opacity:1;color:rgb(148 163 184 / var(--tw-text-opacity,1))}.dark\:bg-dark-surface:is(.dark *){background-color:#1e293b}.hover\:-translate-y-0:hover{--tw-translate-y:-0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-0\.5:hover{--tw-translate-y:-.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-1:hover{--tw-translate-y:-.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-2:hover{--tw-translate-y:-.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-blue-300:hover{--tw-border-opacity:1;border-color:rgb(147 197 253 / var(--tw-border-opacity,1))}.hover\:border-blue-600:hover{--tw-border-opacity:1;border-color:rgb(37 99 235 / var(--tw-border-opacity,1))}.hover\:border-emerald-600:hover{--tw-border-opacity:1;border-color:rgb(5 150 105 / var(--tw-border-opacity,1))}.hover\:bg-amber-100:hover{--tw-bg-opacity:1;background-color:rgb(254 243 199 / var(--tw-bg-opacity,1))}.hover\:bg-black\/70:hover{background-color:rgb(0 0 0 / .7)}.hover\:bg-blue-100:hover{--tw-bg-opacity:1;background-color:rgb(219 234 254 / var(--tw-bg-opacity,1))}.hover\:bg-blue-200:hover{--tw-bg-opacity:1;background-color:rgb(191 219 254 / var(--tw-bg-opacity,1))}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235 / var(--tw-bg-opacity,1))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216 / var(--tw-bg-opacity,1))}.hover\:bg-emerald-700:hover{--tw-bg-opacity:1;background-color:rgb(4 120 87 / var(--tw-bg-opacity,1))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246 / var(--tw-bg-opacity,1))}.hover\:bg-gray-100\/80:hover{background-color:rgb(243 244 246 / .8)}.hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219 / var(--tw-bg-opacity,1))}.hover\:bg-gray-400:hover{--tw-bg-opacity:1;background-color:rgb(156 163 175 / var(--tw-bg-opacity,1))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251 / var(--tw-bg-opacity,1))}.hover\:bg-gray-600:hover{--tw-bg-opacity:1;background-color:rgb(75 85 99 / var(--tw-bg-opacity,1))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81 / var(--tw-bg-opacity,1))}.hover\:bg-gray-800:hover{--tw-bg-opacity:1;background-color:rgb(31 41 55 / var(--tw-bg-opacity,1))}.hover\:bg-green-100:hover{--tw-bg-opacity:1;background-color:rgb(220 252 231 / var(--tw-bg-opacity,1))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(22 163 74 / var(--tw-bg-opacity,1))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61 / var(--tw-bg-opacity,1))}.hover\:bg-indigo-600:hover{--tw-bg-opacity:1;background-color:rgb(79 70 229 / var(--tw-bg-opacity,1))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202 / var(--tw-bg-opacity,1))}.hover\:bg-mercedes-silver:hover{--tw-bg-opacity:1;background-color:rgb(192 192 192 / var(--tw-bg-opacity,1))}.hover\:bg-orange-600:hover{--tw-bg-opacity:1;background-color:rgb(234 88 12 / var(--tw-bg-opacity,1))}.hover\:bg-orange-700:hover{--tw-bg-opacity:1;background-color:rgb(194 65 12 / var(--tw-bg-opacity,1))}.hover\:bg-purple-600:hover{--tw-bg-opacity:1;background-color:rgb(147 51 234 / var(--tw-bg-opacity,1))}.hover\:bg-purple-700:hover{--tw-bg-opacity:1;background-color:rgb(126 34 206 / var(--tw-bg-opacity,1))}.hover\:bg-red-100:hover{--tw-bg-opacity:1;background-color:rgb(254 226 226 / var(--tw-bg-opacity,1))}.hover\:bg-red-50:hover{--tw-bg-opacity:1;background-color:rgb(254 242 242 / var(--tw-bg-opacity,1))}.hover\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38 / var(--tw-bg-opacity,1))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28 / var(--tw-bg-opacity,1))}.hover\:bg-slate-100:hover{--tw-bg-opacity:1;background-color:rgb(241 245 249 / var(--tw-bg-opacity,1))}.hover\:bg-slate-100\/50:hover{background-color:rgb(241 245 249 / .5)}.hover\:bg-slate-100\/80:hover{background-color:rgb(241 245 249 / .8)}.hover\:bg-slate-200:hover{--tw-bg-opacity:1;background-color:rgb(226 232 240 / var(--tw-bg-opacity,1))}.hover\:bg-slate-300:hover{--tw-bg-opacity:1;background-color:rgb(203 213 225 / var(--tw-bg-opacity,1))}.hover\:bg-slate-50:hover{--tw-bg-opacity:1;background-color:rgb(248 250 252 / var(--tw-bg-opacity,1))}.hover\:bg-slate-600:hover{--tw-bg-opacity:1;background-color:rgb(71 85 105 / var(--tw-bg-opacity,1))}.hover\:bg-slate-700:hover{--tw-bg-opacity:1;background-color:rgb(51 65 85 / var(--tw-bg-opacity,1))}.hover\:bg-teal-600:hover{--tw-bg-opacity:1;background-color:rgb(13 148 136 / var(--tw-bg-opacity,1))}.hover\:bg-white\/20:hover{background-color:rgb(255 255 255 / .2)}.hover\:bg-white\/25:hover{background-color:rgb(255 255 255 / .25)}.hover\:bg-yellow-600:hover{--tw-bg-opacity:1;background-color:rgb(202 138 4 / var(--tw-bg-opacity,1))}.hover\:bg-yellow-700:hover{--tw-bg-opacity:1;background-color:rgb(161 98 7 / var(--tw-bg-opacity,1))}.hover\:from-blue-600:hover{--tw-gradient-from:#2563eb var(--tw-gradient-from-position);--tw-gradient-to:rgb(37 99 235 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:from-blue-700:hover{--tw-gradient-from:#1d4ed8 var(--tw-gradient-from-position);--tw-gradient-to:rgb(29 78 216 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:from-green-600:hover{--tw-gradient-from:#16a34a var(--tw-gradient-from-position);--tw-gradient-to:rgb(22 163 74 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:from-slate-600:hover{--tw-gradient-from:#475569 var(--tw-gradient-from-position);--tw-gradient-to:rgb(71 85 105 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:to-blue-700:hover{--tw-gradient-to:#1d4ed8 var(--tw-gradient-to-position)}.hover\:to-blue-800:hover{--tw-gradient-to:#1e40af var(--tw-gradient-to-position)}.hover\:to-green-700:hover{--tw-gradient-to:#15803d var(--tw-gradient-to-position)}.hover\:to-slate-700:hover{--tw-gradient-to:#334155 var(--tw-gradient-to-position)}.hover\:text-blue-500:hover{--tw-text-opacity:1;color:rgb(59 130 246 / var(--tw-text-opacity,1))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(37 99 235 / var(--tw-text-opacity,1))}.hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216 / var(--tw-text-opacity,1))}.hover\:text-blue-800:hover{--tw-text-opacity:1;color:rgb(30 64 175 / var(--tw-text-opacity,1))}.hover\:text-blue-900:hover{--tw-text-opacity:1;color:rgb(30 58 138 / var(--tw-text-opacity,1))}.hover\:text-emerald-600:hover{--tw-text-opacity:1;color:rgb(5 150 105 / var(--tw-text-opacity,1))}.hover\:text-gray-200:hover{--tw-text-opacity:1;color:rgb(229 231 235 / var(--tw-text-opacity,1))}.hover\:text-gray-500:hover{--tw-text-opacity:1;color:rgb(107 114 128 / var(--tw-text-opacity,1))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99 / var(--tw-text-opacity,1))}.hover\:text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55 / var(--tw-text-opacity,1))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39 / var(--tw-text-opacity,1))}.hover\:text-green-900:hover{--tw-text-opacity:1;color:rgb(20 83 45 / var(--tw-text-opacity,1))}.hover\:text-red-500:hover{--tw-text-opacity:1;color:rgb(239 68 68 / var(--tw-text-opacity,1))}.hover\:text-red-700:hover{--tw-text-opacity:1;color:rgb(185 28 28 / var(--tw-text-opacity,1))}.hover\:text-red-900:hover{--tw-text-opacity:1;color:rgb(127 29 29 / var(--tw-text-opacity,1))}.hover\:text-slate-700:hover{--tw-text-opacity:1;color:rgb(51 65 85 / var(--tw-text-opacity,1))}.hover\:text-slate-800:hover{--tw-text-opacity:1;color:rgb(30 41 59 / var(--tw-text-opacity,1))}.hover\:text-slate-900:hover{--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-2xl:hover{--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px rgb(0 0 0 / .1),0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:z-10:focus{z-index:10}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246 / var(--tw-border-opacity,1))}.focus\:border-blue-600:focus{--tw-border-opacity:1;border-color:rgb(37 99 235 / var(--tw-border-opacity,1))}.focus\:border-red-500:focus{--tw-border-opacity:1;border-color:rgb(239 68 68 / var(--tw-border-opacity,1))}.focus\:border-transparent:focus{border-color:transparent}.focus\:bg-gray-100\/80:focus{background-color:rgb(243 244 246 / .8)}.focus\:opacity-100:focus{opacity:1}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246 / var(--tw-ring-opacity,1))}.focus\:ring-blue-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(37 99 235 / var(--tw-ring-opacity,1))}.focus\:ring-gray-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(107 114 128 / var(--tw-ring-opacity,1))}.focus\:ring-green-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(74 222 128 / var(--tw-ring-opacity,1))}.focus\:ring-green-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(34 197 94 / var(--tw-ring-opacity,1))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241 / var(--tw-ring-opacity,1))}.focus\:ring-red-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(248 113 113 / var(--tw-ring-opacity,1))}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(239 68 68 / var(--tw-ring-opacity,1))}.focus\:ring-slate-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(100 116 139 / var(--tw-ring-opacity,1))}.focus\:ring-yellow-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(234 179 8 / var(--tw-ring-opacity,1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.active\:scale-95:active{--tw-scale-x:.95;--tw-scale-y:.95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-gray-100:disabled{--tw-bg-opacity:1;background-color:rgb(243 244 246 / var(--tw-bg-opacity,1))}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:translate-x-full{--tw-translate-x:100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:rotate-180{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105 / var(--tw-text-opacity,1))}.group:hover .group-hover\:opacity-100{opacity:1}.group:active .group-active\:scale-95{--tw-scale-x:.95;--tw-scale-y:.95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:inline:is(.dark *){display:inline}.dark\:hidden:is(.dark *){display:none}.dark\:rotate-0:is(.dark *){--tw-rotate:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:rotate-90:is(.dark *){--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:scale-100:is(.dark *){--tw-scale-x:1;--tw-scale-y:1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:scale-75:is(.dark *){--tw-scale-x:.75;--tw-scale-y:.75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:divide-gray-700:is(.dark *)>:not([hidden]) ~ :not([hidden]){--tw-divide-opacity:1;border-color:rgb(55 65 81 / var(--tw-divide-opacity,1))}.dark\:divide-slate-700:is(.dark *)>:not([hidden]) ~ :not([hidden]){--tw-divide-opacity:1;border-color:rgb(51 65 85 / var(--tw-divide-opacity,1))}.dark\:border-blue-400:is(.dark *){--tw-border-opacity:1;border-color:rgb(96 165 250 / var(--tw-border-opacity,1))}.dark\:border-blue-700:is(.dark *){--tw-border-opacity:1;border-color:rgb(29 78 216 / var(--tw-border-opacity,1))}.dark\:border-blue-700\/30:is(.dark *){border-color:rgb(29 78 216 / .3)}.dark\:border-blue-800:is(.dark *){--tw-border-opacity:1;border-color:rgb(30 64 175 / var(--tw-border-opacity,1))}.dark\:border-blue-800\/50:is(.dark *){border-color:rgb(30 64 175 / .5)}.dark\:border-dark-border:is(.dark *){--tw-border-opacity:1;border-color:rgb(51 65 85 / var(--tw-border-opacity,1))}.dark\:border-emerald-700\/30:is(.dark *){border-color:rgb(4 120 87 / .3)}.dark\:border-gray-600:is(.dark *){--tw-border-opacity:1;border-color:rgb(75 85 99 / var(--tw-border-opacity,1))}.dark\:border-gray-700:is(.dark *){--tw-border-opacity:1;border-color:rgb(55 65 81 / var(--tw-border-opacity,1))}.dark\:border-green-700:is(.dark *){--tw-border-opacity:1;border-color:rgb(21 128 61 / var(--tw-border-opacity,1))}.dark\:border-green-800:is(.dark *){--tw-border-opacity:1;border-color:rgb(22 101 52 / var(--tw-border-opacity,1))}.dark\:border-green-800\/50:is(.dark *){border-color:rgb(22 101 52 / .5)}.dark\:border-indigo-400:is(.dark *){--tw-border-opacity:1;border-color:rgb(129 140 248 / var(--tw-border-opacity,1))}.dark\:border-indigo-800\/50:is(.dark *){border-color:rgb(55 48 163 / .5)}.dark\:border-orange-800:is(.dark *){--tw-border-opacity:1;border-color:rgb(154 52 18 / var(--tw-border-opacity,1))}.dark\:border-orange-800\/50:is(.dark *){border-color:rgb(154 52 18 / .5)}.dark\:border-purple-800\/50:is(.dark *){border-color:rgb(107 33 168 / .5)}.dark\:border-red-700:is(.dark *){--tw-border-opacity:1;border-color:rgb(185 28 28 / var(--tw-border-opacity,1))}.dark\:border-red-800:is(.dark *){--tw-border-opacity:1;border-color:rgb(153 27 27 / var(--tw-border-opacity,1))}.dark\:border-red-800\/50:is(.dark *){border-color:rgb(153 27 27 / .5)}.dark\:border-slate-600:is(.dark *){--tw-border-opacity:1;border-color:rgb(71 85 105 / var(--tw-border-opacity,1))}.dark\:border-slate-600\/50:is(.dark *){border-color:rgb(71 85 105 / .5)}.dark\:border-slate-600\/60:is(.dark *){border-color:rgb(71 85 105 / .6)}.dark\:border-slate-700:is(.dark *){--tw-border-opacity:1;border-color:rgb(51 65 85 / var(--tw-border-opacity,1))}.dark\:border-slate-700\/20:is(.dark *){border-color:rgb(51 65 85 / .2)}.dark\:border-slate-700\/30:is(.dark *){border-color:rgb(51 65 85 / .3)}.dark\:border-slate-700\/50:is(.dark *){border-color:rgb(51 65 85 / .5)}.dark\:border-white:is(.dark *){--tw-border-opacity:1;border-color:rgb(255 255 255 / var(--tw-border-opacity,1))}.dark\:border-white\/20:is(.dark *){border-color:rgb(255 255 255 / .2)}.dark\:border-white\/70:is(.dark *){border-color:rgb(255 255 255 / .7)}.dark\:border-yellow-700:is(.dark *){--tw-border-opacity:1;border-color:rgb(161 98 7 / var(--tw-border-opacity,1))}.dark\:border-yellow-800:is(.dark *){--tw-border-opacity:1;border-color:rgb(133 77 14 / var(--tw-border-opacity,1))}.dark\:border-t-slate-700:is(.dark *){--tw-border-opacity:1;border-top-color:rgb(51 65 85 / var(--tw-border-opacity,1))}.dark\:bg-amber-600:is(.dark *){--tw-bg-opacity:1;background-color:rgb(217 119 6 / var(--tw-bg-opacity,1))}.dark\:bg-black:is(.dark *){--tw-bg-opacity:1;background-color:rgb(0 0 0 / var(--tw-bg-opacity,1))}.dark\:bg-black\/50:is(.dark *){background-color:rgb(0 0 0 / .5)}.dark\:bg-black\/60:is(.dark *){background-color:rgb(0 0 0 / .6)}.dark\:bg-black\/70:is(.dark *){background-color:rgb(0 0 0 / .7)}.dark\:bg-black\/80:is(.dark *){background-color:rgb(0 0 0 / .8)}.dark\:bg-blue-300:is(.dark *){--tw-bg-opacity:1;background-color:rgb(147 197 253 / var(--tw-bg-opacity,1))}.dark\:bg-blue-400:is(.dark *){--tw-bg-opacity:1;background-color:rgb(96 165 250 / var(--tw-bg-opacity,1))}.dark\:bg-blue-500:is(.dark *){--tw-bg-opacity:1;background-color:rgb(59 130 246 / var(--tw-bg-opacity,1))}.dark\:bg-blue-600:is(.dark *){--tw-bg-opacity:1;background-color:rgb(37 99 235 / var(--tw-bg-opacity,1))}.dark\:bg-blue-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 58 138 / var(--tw-bg-opacity,1))}.dark\:bg-blue-900\/10:is(.dark *){background-color:rgb(30 58 138 / .1)}.dark\:bg-blue-900\/20:is(.dark *){background-color:rgb(30 58 138 / .2)}.dark\:bg-blue-900\/30:is(.dark *){background-color:rgb(30 58 138 / .3)}.dark\:bg-blue-900\/50:is(.dark *){background-color:rgb(30 58 138 / .5)}.dark\:bg-cyan-900\/50:is(.dark *){background-color:rgb(22 78 99 / .5)}.dark\:bg-dark-surface:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.dark\:bg-emerald-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(6 78 59 / var(--tw-bg-opacity,1))}.dark\:bg-emerald-900\/50:is(.dark *){background-color:rgb(6 78 59 / .5)}.dark\:bg-gray-300:is(.dark *){--tw-bg-opacity:1;background-color:rgb(209 213 219 / var(--tw-bg-opacity,1))}.dark\:bg-gray-600:is(.dark *){--tw-bg-opacity:1;background-color:rgb(75 85 99 / var(--tw-bg-opacity,1))}.dark\:bg-gray-700:is(.dark *){--tw-bg-opacity:1;background-color:rgb(55 65 81 / var(--tw-bg-opacity,1))}.dark\:bg-gray-800:is(.dark *){--tw-bg-opacity:1;background-color:rgb(31 41 55 / var(--tw-bg-opacity,1))}.dark\:bg-gray-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(17 24 39 / var(--tw-bg-opacity,1))}.dark\:bg-gray-900\/30:is(.dark *){background-color:rgb(17 24 39 / .3)}.dark\:bg-green-300:is(.dark *){--tw-bg-opacity:1;background-color:rgb(134 239 172 / var(--tw-bg-opacity,1))}.dark\:bg-green-600:is(.dark *){--tw-bg-opacity:1;background-color:rgb(22 163 74 / var(--tw-bg-opacity,1))}.dark\:bg-green-700:is(.dark *){--tw-bg-opacity:1;background-color:rgb(21 128 61 / var(--tw-bg-opacity,1))}.dark\:bg-green-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(20 83 45 / var(--tw-bg-opacity,1))}.dark\:bg-green-900\/10:is(.dark *){background-color:rgb(20 83 45 / .1)}.dark\:bg-green-900\/20:is(.dark *){background-color:rgb(20 83 45 / .2)}.dark\:bg-green-900\/30:is(.dark *){background-color:rgb(20 83 45 / .3)}.dark\:bg-green-900\/50:is(.dark *){background-color:rgb(20 83 45 / .5)}.dark\:bg-green-900\/60:is(.dark *){background-color:rgb(20 83 45 / .6)}.dark\:bg-indigo-600:is(.dark *){--tw-bg-opacity:1;background-color:rgb(79 70 229 / var(--tw-bg-opacity,1))}.dark\:bg-indigo-700:is(.dark *){--tw-bg-opacity:1;background-color:rgb(67 56 202 / var(--tw-bg-opacity,1))}.dark\:bg-indigo-900\/10:is(.dark *){background-color:rgb(49 46 129 / .1)}.dark\:bg-indigo-900\/30:is(.dark *){background-color:rgb(49 46 129 / .3)}.dark\:bg-indigo-900\/50:is(.dark *){background-color:rgb(49 46 129 / .5)}.dark\:bg-orange-300:is(.dark *){--tw-bg-opacity:1;background-color:rgb(253 186 116 / var(--tw-bg-opacity,1))}.dark\:bg-orange-800:is(.dark *){--tw-bg-opacity:1;background-color:rgb(154 52 18 / var(--tw-bg-opacity,1))}.dark\:bg-orange-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(124 45 18 / var(--tw-bg-opacity,1))}.dark\:bg-orange-900\/10:is(.dark *){background-color:rgb(124 45 18 / .1)}.dark\:bg-orange-900\/30:is(.dark *){background-color:rgb(124 45 18 / .3)}.dark\:bg-orange-900\/50:is(.dark *){background-color:rgb(124 45 18 / .5)}.dark\:bg-purple-600:is(.dark *){--tw-bg-opacity:1;background-color:rgb(147 51 234 / var(--tw-bg-opacity,1))}.dark\:bg-purple-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(88 28 135 / var(--tw-bg-opacity,1))}.dark\:bg-purple-900\/10:is(.dark *){background-color:rgb(88 28 135 / .1)}.dark\:bg-purple-900\/30:is(.dark *){background-color:rgb(88 28 135 / .3)}.dark\:bg-purple-900\/50:is(.dark *){background-color:rgb(88 28 135 / .5)}.dark\:bg-red-300:is(.dark *){--tw-bg-opacity:1;background-color:rgb(252 165 165 / var(--tw-bg-opacity,1))}.dark\:bg-red-600:is(.dark *){--tw-bg-opacity:1;background-color:rgb(220 38 38 / var(--tw-bg-opacity,1))}.dark\:bg-red-800:is(.dark *){--tw-bg-opacity:1;background-color:rgb(153 27 27 / var(--tw-bg-opacity,1))}.dark\:bg-red-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(127 29 29 / var(--tw-bg-opacity,1))}.dark\:bg-red-900\/10:is(.dark *){background-color:rgb(127 29 29 / .1)}.dark\:bg-red-900\/20:is(.dark *){background-color:rgb(127 29 29 / .2)}.dark\:bg-red-900\/30:is(.dark *){background-color:rgb(127 29 29 / .3)}.dark\:bg-red-900\/50:is(.dark *){background-color:rgb(127 29 29 / .5)}.dark\:bg-red-900\/60:is(.dark *){background-color:rgb(127 29 29 / .6)}.dark\:bg-slate-600:is(.dark *){--tw-bg-opacity:1;background-color:rgb(71 85 105 / var(--tw-bg-opacity,1))}.dark\:bg-slate-700:is(.dark *){--tw-bg-opacity:1;background-color:rgb(51 65 85 / var(--tw-bg-opacity,1))}.dark\:bg-slate-700\/30:is(.dark *){background-color:rgb(51 65 85 / .3)}.dark\:bg-slate-700\/40:is(.dark *){background-color:rgb(51 65 85 / .4)}.dark\:bg-slate-700\/60:is(.dark *){background-color:rgb(51 65 85 / .6)}.dark\:bg-slate-800:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.dark\:bg-slate-800\/50:is(.dark *){background-color:rgb(30 41 59 / .5)}.dark\:bg-slate-800\/60:is(.dark *){background-color:rgb(30 41 59 / .6)}.dark\:bg-slate-800\/80:is(.dark *){background-color:rgb(30 41 59 / .8)}.dark\:bg-slate-800\/90:is(.dark *){background-color:rgb(30 41 59 / .9)}.dark\:bg-slate-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(15 23 42 / var(--tw-bg-opacity,1))}.dark\:bg-slate-900\/50:is(.dark *){background-color:rgb(15 23 42 / .5)}.dark\:bg-slate-900\/60:is(.dark *){background-color:rgb(15 23 42 / .6)}.dark\:bg-slate-900\/80:is(.dark *){background-color:rgb(15 23 42 / .8)}.dark\:bg-slate-900\/90:is(.dark *){background-color:rgb(15 23 42 / .9)}.dark\:bg-teal-900\/50:is(.dark *){background-color:rgb(19 78 74 / .5)}.dark\:bg-white:is(.dark *){--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity,1))}.dark\:bg-white\/10:is(.dark *){background-color:rgb(255 255 255 / .1)}.dark\:bg-yellow-300:is(.dark *){--tw-bg-opacity:1;background-color:rgb(253 224 71 / var(--tw-bg-opacity,1))}.dark\:bg-yellow-800:is(.dark *){--tw-bg-opacity:1;background-color:rgb(133 77 14 / var(--tw-bg-opacity,1))}.dark\:bg-yellow-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(113 63 18 / var(--tw-bg-opacity,1))}.dark\:bg-yellow-900\/20:is(.dark *){background-color:rgb(113 63 18 / .2)}.dark\:bg-yellow-900\/30:is(.dark *){background-color:rgb(113 63 18 / .3)}.dark\:bg-yellow-900\/50:is(.dark *){background-color:rgb(113 63 18 / .5)}.dark\:bg-opacity-95:is(.dark *){--tw-bg-opacity:.95}.dark\:from-blue-400:is(.dark *){--tw-gradient-from:#60a5fa var(--tw-gradient-from-position);--tw-gradient-to:rgb(96 165 250 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-blue-400\/20:is(.dark *){--tw-gradient-from:rgb(96 165 250 / .2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(96 165 250 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-blue-900\/10:is(.dark *){--tw-gradient-from:rgb(30 58 138 / .1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(30 58 138 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-blue-900\/20:is(.dark *){--tw-gradient-from:rgb(30 58 138 / .2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(30 58 138 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-blue-900\/30:is(.dark *){--tw-gradient-from:rgb(30 58 138 / .3) var(--tw-gradient-from-position);--tw-gradient-to:rgb(30 58 138 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-emerald-900\/20:is(.dark *){--tw-gradient-from:rgb(6 78 59 / .2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(6 78 59 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-green-400:is(.dark *){--tw-gradient-from:#4ade80 var(--tw-gradient-from-position);--tw-gradient-to:rgb(74 222 128 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-green-400\/20:is(.dark *){--tw-gradient-from:rgb(74 222 128 / .2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(74 222 128 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-green-900\/10:is(.dark *){--tw-gradient-from:rgb(20 83 45 / .1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(20 83 45 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-green-900\/20:is(.dark *){--tw-gradient-from:rgb(20 83 45 / .2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(20 83 45 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-green-900\/30:is(.dark *){--tw-gradient-from:rgb(20 83 45 / .3) var(--tw-gradient-from-position);--tw-gradient-to:rgb(20 83 45 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-orange-400:is(.dark *){--tw-gradient-from:#fb923c var(--tw-gradient-from-position);--tw-gradient-to:rgb(251 146 60 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-orange-400\/20:is(.dark *){--tw-gradient-from:rgb(251 146 60 / .2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(251 146 60 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-orange-900\/10:is(.dark *){--tw-gradient-from:rgb(124 45 18 / .1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(124 45 18 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-orange-900\/20:is(.dark *){--tw-gradient-from:rgb(124 45 18 / .2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(124 45 18 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-purple-900\/20:is(.dark *){--tw-gradient-from:rgb(88 28 135 / .2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(88 28 135 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-purple-900\/30:is(.dark *){--tw-gradient-from:rgb(88 28 135 / .3) var(--tw-gradient-from-position);--tw-gradient-to:rgb(88 28 135 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-red-400:is(.dark *){--tw-gradient-from:#f87171 var(--tw-gradient-from-position);--tw-gradient-to:rgb(248 113 113 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-red-400\/20:is(.dark *){--tw-gradient-from:rgb(248 113 113 / .2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(248 113 113 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-slate-800\/90:is(.dark *){--tw-gradient-from:rgb(30 41 59 / .9) var(--tw-gradient-from-position);--tw-gradient-to:rgb(30 41 59 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-slate-900:is(.dark *){--tw-gradient-from:#0f172a var(--tw-gradient-from-position);--tw-gradient-to:rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-slate-950:is(.dark *){--tw-gradient-from:#020617 var(--tw-gradient-from-position);--tw-gradient-to:rgb(2 6 23 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:from-white:is(.dark *){--tw-gradient-from:#fff var(--tw-gradient-from-position);--tw-gradient-to:rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.dark\:via-blue-200:is(.dark *){--tw-gradient-to:rgb(191 219 254 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#bfdbfe var(--tw-gradient-via-position),var(--tw-gradient-to)}.dark\:via-blue-900\/20:is(.dark *){--tw-gradient-to:rgb(30 58 138 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgb(30 58 138 / .2) var(--tw-gradient-via-position),var(--tw-gradient-to)}.dark\:via-blue-950:is(.dark *){--tw-gradient-to:rgb(23 37 84 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#172554 var(--tw-gradient-via-position),var(--tw-gradient-to)}.dark\:via-emerald-900\/20:is(.dark *){--tw-gradient-to:rgb(6 78 59 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgb(6 78 59 / .2) var(--tw-gradient-via-position),var(--tw-gradient-to)}.dark\:via-red-900\/20:is(.dark *){--tw-gradient-to:rgb(127 29 29 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgb(127 29 29 / .2) var(--tw-gradient-via-position),var(--tw-gradient-to)}.dark\:via-slate-800:is(.dark *){--tw-gradient-to:rgb(30 41 59 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1e293b var(--tw-gradient-via-position),var(--tw-gradient-to)}.dark\:to-blue-500:is(.dark *){--tw-gradient-to:#3b82f6 var(--tw-gradient-to-position)}.dark\:to-blue-800\/30:is(.dark *){--tw-gradient-to:rgb(30 64 175 / .3) var(--tw-gradient-to-position)}.dark\:to-cyan-900\/20:is(.dark *){--tw-gradient-to:rgb(22 78 99 / .2) var(--tw-gradient-to-position)}.dark\:to-emerald-400\/20:is(.dark *){--tw-gradient-to:rgb(52 211 153 / .2) var(--tw-gradient-to-position)}.dark\:to-emerald-900\/10:is(.dark *){--tw-gradient-to:rgb(6 78 59 / .1) var(--tw-gradient-to-position)}.dark\:to-emerald-900\/20:is(.dark *){--tw-gradient-to:rgb(6 78 59 / .2) var(--tw-gradient-to-position)}.dark\:to-green-500:is(.dark *){--tw-gradient-to:#22c55e var(--tw-gradient-to-position)}.dark\:to-green-800\/30:is(.dark *){--tw-gradient-to:rgb(22 101 52 / .3) var(--tw-gradient-to-position)}.dark\:to-green-900\/20:is(.dark *){--tw-gradient-to:rgb(20 83 45 / .2) var(--tw-gradient-to-position)}.dark\:to-indigo-400\/20:is(.dark *){--tw-gradient-to:rgb(129 140 248 / .2) var(--tw-gradient-to-position)}.dark\:to-indigo-900\/10:is(.dark *){--tw-gradient-to:rgb(49 46 129 / .1) var(--tw-gradient-to-position)}.dark\:to-indigo-900\/20:is(.dark *){--tw-gradient-to:rgb(49 46 129 / .2) var(--tw-gradient-to-position)}.dark\:to-indigo-950:is(.dark *){--tw-gradient-to:#1e1b4b var(--tw-gradient-to-position)}.dark\:to-orange-500:is(.dark *){--tw-gradient-to:#f97316 var(--tw-gradient-to-position)}.dark\:to-orange-900\/20:is(.dark *){--tw-gradient-to:rgb(124 45 18 / .2) var(--tw-gradient-to-position)}.dark\:to-pink-400\/20:is(.dark *){--tw-gradient-to:rgb(244 114 182 / .2) var(--tw-gradient-to-position)}.dark\:to-pink-900\/20:is(.dark *){--tw-gradient-to:rgb(131 24 67 / .2) var(--tw-gradient-to-position)}.dark\:to-purple-500:is(.dark *){--tw-gradient-to:#a855f7 var(--tw-gradient-to-position)}.dark\:to-purple-800\/30:is(.dark *){--tw-gradient-to:rgb(107 33 168 / .3) var(--tw-gradient-to-position)}.dark\:to-red-400\/20:is(.dark *){--tw-gradient-to:rgb(248 113 113 / .2) var(--tw-gradient-to-position)}.dark\:to-red-500:is(.dark *){--tw-gradient-to:#ef4444 var(--tw-gradient-to-position)}.dark\:to-red-900\/10:is(.dark *){--tw-gradient-to:rgb(127 29 29 / .1) var(--tw-gradient-to-position)}.dark\:to-red-900\/20:is(.dark *){--tw-gradient-to:rgb(127 29 29 / .2) var(--tw-gradient-to-position)}.dark\:to-slate-200:is(.dark *){--tw-gradient-to:#e2e8f0 var(--tw-gradient-to-position)}.dark\:to-slate-900:is(.dark *){--tw-gradient-to:#0f172a var(--tw-gradient-to-position)}.dark\:to-slate-900\/70:is(.dark *){--tw-gradient-to:rgb(15 23 42 / .7) var(--tw-gradient-to-position)}.dark\:text-amber-400:is(.dark *){--tw-text-opacity:1;color:rgb(251 191 36 / var(--tw-text-opacity,1))}.dark\:text-blue-100:is(.dark *){--tw-text-opacity:1;color:rgb(219 234 254 / var(--tw-text-opacity,1))}.dark\:text-blue-200:is(.dark *){--tw-text-opacity:1;color:rgb(191 219 254 / var(--tw-text-opacity,1))}.dark\:text-blue-300:is(.dark *){--tw-text-opacity:1;color:rgb(147 197 253 / var(--tw-text-opacity,1))}.dark\:text-blue-400:is(.dark *){--tw-text-opacity:1;color:rgb(96 165 250 / var(--tw-text-opacity,1))}.dark\:text-blue-500:is(.dark *){--tw-text-opacity:1;color:rgb(59 130 246 / var(--tw-text-opacity,1))}.dark\:text-cyan-400:is(.dark *){--tw-text-opacity:1;color:rgb(34 211 238 / var(--tw-text-opacity,1))}.dark\:text-dark-text:is(.dark *){--tw-text-opacity:1;color:rgb(248 250 252 / var(--tw-text-opacity,1))}.dark\:text-dark-text-muted:is(.dark *){--tw-text-opacity:1;color:rgb(148 163 184 / var(--tw-text-opacity,1))}.dark\:text-emerald-300:is(.dark *){--tw-text-opacity:1;color:rgb(110 231 183 / var(--tw-text-opacity,1))}.dark\:text-emerald-400:is(.dark *){--tw-text-opacity:1;color:rgb(52 211 153 / var(--tw-text-opacity,1))}.dark\:text-gray-100:is(.dark *){--tw-text-opacity:1;color:rgb(243 244 246 / var(--tw-text-opacity,1))}.dark\:text-gray-200:is(.dark *){--tw-text-opacity:1;color:rgb(229 231 235 / var(--tw-text-opacity,1))}.dark\:text-gray-300:is(.dark *){--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity,1))}.dark\:text-gray-400:is(.dark *){--tw-text-opacity:1;color:rgb(156 163 175 / var(--tw-text-opacity,1))}.dark\:text-gray-500:is(.dark *){--tw-text-opacity:1;color:rgb(107 114 128 / var(--tw-text-opacity,1))}.dark\:text-gray-600:is(.dark *){--tw-text-opacity:1;color:rgb(75 85 99 / var(--tw-text-opacity,1))}.dark\:text-green-100:is(.dark *){--tw-text-opacity:1;color:rgb(220 252 231 / var(--tw-text-opacity,1))}.dark\:text-green-200:is(.dark *){--tw-text-opacity:1;color:rgb(187 247 208 / var(--tw-text-opacity,1))}.dark\:text-green-300:is(.dark *){--tw-text-opacity:1;color:rgb(134 239 172 / var(--tw-text-opacity,1))}.dark\:text-green-400:is(.dark *){--tw-text-opacity:1;color:rgb(74 222 128 / var(--tw-text-opacity,1))}.dark\:text-indigo-400:is(.dark *){--tw-text-opacity:1;color:rgb(129 140 248 / var(--tw-text-opacity,1))}.dark\:text-orange-100:is(.dark *){--tw-text-opacity:1;color:rgb(255 237 213 / var(--tw-text-opacity,1))}.dark\:text-orange-200:is(.dark *){--tw-text-opacity:1;color:rgb(254 215 170 / var(--tw-text-opacity,1))}.dark\:text-orange-300:is(.dark *){--tw-text-opacity:1;color:rgb(253 186 116 / var(--tw-text-opacity,1))}.dark\:text-orange-400:is(.dark *){--tw-text-opacity:1;color:rgb(251 146 60 / var(--tw-text-opacity,1))}.dark\:text-purple-200:is(.dark *){--tw-text-opacity:1;color:rgb(233 213 255 / var(--tw-text-opacity,1))}.dark\:text-purple-300:is(.dark *){--tw-text-opacity:1;color:rgb(216 180 254 / var(--tw-text-opacity,1))}.dark\:text-purple-400:is(.dark *){--tw-text-opacity:1;color:rgb(192 132 252 / var(--tw-text-opacity,1))}.dark\:text-red-100:is(.dark *){--tw-text-opacity:1;color:rgb(254 226 226 / var(--tw-text-opacity,1))}.dark\:text-red-200:is(.dark *){--tw-text-opacity:1;color:rgb(254 202 202 / var(--tw-text-opacity,1))}.dark\:text-red-300:is(.dark *){--tw-text-opacity:1;color:rgb(252 165 165 / var(--tw-text-opacity,1))}.dark\:text-red-400:is(.dark *){--tw-text-opacity:1;color:rgb(248 113 113 / var(--tw-text-opacity,1))}.dark\:text-slate-100:is(.dark *){--tw-text-opacity:1;color:rgb(241 245 249 / var(--tw-text-opacity,1))}.dark\:text-slate-200:is(.dark *){--tw-text-opacity:1;color:rgb(226 232 240 / var(--tw-text-opacity,1))}.dark\:text-slate-300:is(.dark *){--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}.dark\:text-slate-400:is(.dark *){--tw-text-opacity:1;color:rgb(148 163 184 / var(--tw-text-opacity,1))}.dark\:text-slate-500:is(.dark *){--tw-text-opacity:1;color:rgb(100 116 139 / var(--tw-text-opacity,1))}.dark\:text-slate-600:is(.dark *){--tw-text-opacity:1;color:rgb(71 85 105 / var(--tw-text-opacity,1))}.dark\:text-slate-900:is(.dark *){--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.dark\:text-teal-400:is(.dark *){--tw-text-opacity:1;color:rgb(45 212 191 / var(--tw-text-opacity,1))}.dark\:text-white:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.dark\:text-yellow-100:is(.dark *){--tw-text-opacity:1;color:rgb(254 249 195 / var(--tw-text-opacity,1))}.dark\:text-yellow-200:is(.dark *){--tw-text-opacity:1;color:rgb(254 240 138 / var(--tw-text-opacity,1))}.dark\:text-yellow-300:is(.dark *){--tw-text-opacity:1;color:rgb(253 224 71 / var(--tw-text-opacity,1))}.dark\:text-yellow-400:is(.dark *){--tw-text-opacity:1;color:rgb(250 204 21 / var(--tw-text-opacity,1))}.dark\:placeholder-slate-400:is(.dark *)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(148 163 184 / var(--tw-placeholder-opacity,1))}.dark\:placeholder-slate-400:is(.dark *)::placeholder{--tw-placeholder-opacity:1;color:rgb(148 163 184 / var(--tw-placeholder-opacity,1))}.dark\:opacity-0:is(.dark *){opacity:0}.dark\:opacity-100:is(.dark *){opacity:1}.dark\:opacity-5:is(.dark *){opacity:.05}.dark\:shadow-2xl:is(.dark *){--tw-shadow:0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.dark\:shadow-slate-900\/20:is(.dark *){--tw-shadow-color:rgb(15 23 42 / .2);--tw-shadow:var(--tw-shadow-colored)}.dark\:hover\:border-blue-400:hover:is(.dark *){--tw-border-opacity:1;border-color:rgb(96 165 250 / var(--tw-border-opacity,1))}.dark\:hover\:border-blue-500:hover:is(.dark *){--tw-border-opacity:1;border-color:rgb(59 130 246 / var(--tw-border-opacity,1))}.dark\:hover\:border-emerald-400:hover:is(.dark *){--tw-border-opacity:1;border-color:rgb(52 211 153 / var(--tw-border-opacity,1))}.dark\:hover\:bg-blue-500:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(59 130 246 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-blue-600:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(37 99 235 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-blue-700:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(29 78 216 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-blue-800:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 64 175 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-gray-500:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(107 114 128 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-gray-600:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(75 85 99 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-gray-700:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(55 65 81 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-green-500:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(34 197 94 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-green-700:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(21 128 61 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-purple-700:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(126 34 206 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-red-500:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(239 68 68 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-red-700:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(185 28 28 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-red-900\/10:hover:is(.dark *){background-color:rgb(127 29 29 / .1)}.dark\:hover\:bg-red-900\/20:hover:is(.dark *){background-color:rgb(127 29 29 / .2)}.dark\:hover\:bg-slate-500:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(100 116 139 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-slate-600:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(71 85 105 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-slate-700:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(51 65 85 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-slate-700\/50:hover:is(.dark *){background-color:rgb(51 65 85 / .5)}.dark\:hover\:bg-slate-700\/60:hover:is(.dark *){background-color:rgb(51 65 85 / .6)}.dark\:hover\:bg-slate-800:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.dark\:hover\:bg-slate-800\/50:hover:is(.dark *){background-color:rgb(30 41 59 / .5)}.dark\:hover\:bg-white\/15:hover:is(.dark *){background-color:rgb(255 255 255 / .15)}.dark\:hover\:bg-white\/70:hover:is(.dark *){background-color:rgb(255 255 255 / .7)}.dark\:hover\:text-blue-200:hover:is(.dark *){--tw-text-opacity:1;color:rgb(191 219 254 / var(--tw-text-opacity,1))}.dark\:hover\:text-blue-300:hover:is(.dark *){--tw-text-opacity:1;color:rgb(147 197 253 / var(--tw-text-opacity,1))}.dark\:hover\:text-blue-400:hover:is(.dark *){--tw-text-opacity:1;color:rgb(96 165 250 / var(--tw-text-opacity,1))}.dark\:hover\:text-emerald-400:hover:is(.dark *){--tw-text-opacity:1;color:rgb(52 211 153 / var(--tw-text-opacity,1))}.dark\:hover\:text-gray-200:hover:is(.dark *){--tw-text-opacity:1;color:rgb(229 231 235 / var(--tw-text-opacity,1))}.dark\:hover\:text-gray-300:hover:is(.dark *){--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity,1))}.dark\:hover\:text-green-300:hover:is(.dark *){--tw-text-opacity:1;color:rgb(134 239 172 / var(--tw-text-opacity,1))}.dark\:hover\:text-red-200:hover:is(.dark *){--tw-text-opacity:1;color:rgb(254 202 202 / var(--tw-text-opacity,1))}.dark\:hover\:text-red-300:hover:is(.dark *){--tw-text-opacity:1;color:rgb(252 165 165 / var(--tw-text-opacity,1))}.dark\:hover\:text-slate-200:hover:is(.dark *){--tw-text-opacity:1;color:rgb(226 232 240 / var(--tw-text-opacity,1))}.dark\:hover\:text-slate-300:hover:is(.dark *){--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}.dark\:hover\:text-slate-900:hover:is(.dark *){--tw-text-opacity:1;color:rgb(15 23 42 / var(--tw-text-opacity,1))}.dark\:hover\:text-white:hover:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity,1))}.dark\:hover\:shadow-slate-900\/50:hover:is(.dark *){--tw-shadow-color:rgb(15 23 42 / .5);--tw-shadow:var(--tw-shadow-colored)}.dark\:focus\:ring-blue-400:focus:is(.dark *){--tw-ring-opacity:1;--tw-ring-color:rgb(96 165 250 / var(--tw-ring-opacity,1))}.dark\:disabled\:bg-slate-800:disabled:is(.dark *){--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity,1))}.group:hover .dark\:group-hover\:text-slate-300:is(.dark *){--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity,1))}@media(min-width:640px){.sm\:mx-0{margin-left:0;margin-right:0}.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:ml-3{margin-left:.75rem}.sm\:ml-4{margin-left:1rem}.sm\:mt-0{margin-top:0}.sm\:mt-12{margin-top:3rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:h-10{height:2.5rem}.sm\:h-4{height:1rem}.sm\:h-5{height:1.25rem}.sm\:h-6{height:1.5rem}.sm\:w-10{width:2.5rem}.sm\:w-4{width:1rem}.sm\:w-5{width:1.25rem}.sm\:w-6{width:1.5rem}.sm\:w-80{width:20rem}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-lg{max-width:32rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:items-start{align-items:flex-start}.sm\:justify-center{justify-content:center}.sm\:gap-8{gap:2rem}.sm\:space-x-3>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.75rem * var(--tw-space-x-reverse));margin-left:calc(0.75rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.sm\:space-x-4>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.sm\:space-y-0>:not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px * calc(1 - var(--tw-space-y-reverse)))calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px * var(--tw-space-y-reverse))}.sm\:p-0{padding:0}.sm\:p-6{padding:1.5rem}.sm\:px-4{padding-left:1rem;padding-right:1rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-8{padding-top:2rem;padding-bottom:2rem}.sm\:pb-4{padding-bottom:1rem}.sm\:pt-8{padding-top:2rem}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media(min-width:768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-between{justify-content:space-between}.md\:space-x-3>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.75rem * var(--tw-space-x-reverse));margin-left:calc(0.75rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.md\:p-12{padding:3rem}.md\:text-2xl{font-size:1.5rem;line-height:2rem}.md\:text-4xl{font-size:2.25rem;line-height:2.5rem}.md\:text-5xl{font-size:3rem;line-height:1}.md\:text-6xl{font-size:3.75rem;line-height:1}.md\:text-8xl{font-size:6rem;line-height:1}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media(min-width:1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:mt-0{margin-top:0}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:h-20{height:5rem}.lg\:h-7{height:1.75rem}.lg\:w-7{width:1.75rem}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-between{justify-content:space-between}.lg\:space-x-6>:not([hidden]) ~ :not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.5rem * var(--tw-space-x-reverse));margin-left:calc(1.5rem * calc(1 - var(--tw-space-x-reverse)))calc(1 - var(--tw-space-x-reverse)))}.lg\:space-y-0>:not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px * calc(1 - var(--tw-space-y-reverse)))calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px * var(--tw-space-y-reverse))}.lg\:p-12{padding:3rem}.lg\:px-6{padding-left:1.5rem;padding-right:1.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:text-right{text-align:right}.lg\:text-6xl{font-size:3.75rem;line-height:1}.lg\:text-base{font-size:1rem;line-height:1.5rem}}@media(min-width:1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}} \ No newline at end of file diff --git a/backend/static/css/output.min.css.gz b/backend/static/css/output.min.css.gz new file mode 100644 index 00000000..4fd8fc57 Binary files /dev/null and b/backend/static/css/output.min.css.gz differ diff --git a/backend/static/css/printers.css.gz b/backend/static/css/printers.css.gz new file mode 100644 index 00000000..7fc5e94d Binary files /dev/null and b/backend/static/css/printers.css.gz differ diff --git a/backend/static/css/printers.min.css b/backend/static/css/printers.min.css new file mode 100644 index 00000000..9b115f9c --- /dev/null +++ b/backend/static/css/printers.min.css @@ -0,0 +1 @@ +.filter-btn{transition:all .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}.printer-card-online{background:linear-gradient(135deg,#f0fdf4 0,#fff 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-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-count-change{animation:count-change .5s ease-in-out}@keyframes count-change{0%{transform:scale(1)}50%{transform:scale(1.1)}100%{transform:scale(1)}}.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%}}.printer-card{transition:all .3s cubic-bezier(0.4,0,0.2,1)}.printer-card:hover{transform:translateY(-2px)}.live-update-spinner{animation:spin 1s linear infinite}@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@media(max-width:640px){.filter-btn{padding:.375rem .75rem;font-size:.75rem}.status-overview{flex-direction:column;gap:.5rem}.printer-card{padding:1rem}}.dark .printer-card{background-color:#1e293b;border-color:#334155}.dark .printer-card:hover{background-color:#334155}.filter-btn:focus{outline:2px solid #3b82f6;outline-offset:2px}.printer-card:focus-within{outline:2px solid #3b82f6;outline-offset:2px}@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}}@media(prefers-contrast:high){.printer-card-online{border:2px solid #059669}.online-indicator{border:1px solid #000}}@media(prefers-reduced-motion:reduce){.online-indicator,.auto-refresh-active,.live-update-spinner{animation:none}.printer-card{transition:none}.printer-card:hover{transform:none}} \ No newline at end of file diff --git a/backend/static/css/printers.min.css.gz b/backend/static/css/printers.min.css.gz new file mode 100644 index 00000000..1ba431d4 Binary files /dev/null and b/backend/static/css/printers.min.css.gz differ diff --git a/backend/static/css/professional-theme.css b/backend/static/css/professional-theme.css index 7e359b22..ecb64eff 100644 --- a/backend/static/css/professional-theme.css +++ b/backend/static/css/professional-theme.css @@ -1,12 +1,11 @@ /** - * Mercedes-Benz MYP Platform - Erweiterte Professional Theme - * Verbesserte Light/Dark Mode Implementierung mit optimierten Kontrasten - * OPTIMIERT: Light Mode deutlich verbessert für bessere Lesbarkeit + * Mercedes-Benz MYP Platform - Optimierte Professional Theme + * Vereinfachte Light/Dark Mode Implementierung für bessere Performance */ /* Globale CSS-Variablen für konsistente Theming */ :root { - /* Mercedes-Benz Markenfarben - Erweitert */ + /* Mercedes-Benz Markenfarben */ --mb-primary: #0073ce; --mb-primary-dark: #005a9f; --mb-secondary: #64748b; @@ -14,32 +13,22 @@ --mb-black: #000000; --mb-silver: #c0c0c0; - /* Light Mode Farbpalette - DEUTLICH VERBESSERT für optimale Lesbarkeit */ + /* Light Mode Farbpalette */ --light-bg-primary: #ffffff; --light-bg-secondary: #fafbfc; --light-bg-tertiary: #f3f5f7; - --light-bg-accent: #fbfcfd; --light-surface: #ffffff; --light-surface-hover: #fafbfc; - --light-surface-active: #f3f5f7; - --light-text-primary: #111827; /* Deutlich verstärkter Kontrast */ - --light-text-secondary: #374151; /* Erhöhter Kontrast für bessere Lesbarkeit */ - --light-text-muted: #6b7280; /* Optimierter Muted-Text, immer noch lesbar */ + --light-text-primary: #111827; + --light-text-secondary: #374151; + --light-text-muted: #6b7280; --light-text-accent: #0073ce; - --light-border: #e5e7eb; /* Sichtbarere aber elegante Borders */ + --light-border: #e5e7eb; --light-border-strong: #d1d5db; - --light-border-accent: #0073ce15; /* Subtilerer Accent-Border */ - --light-shadow: rgba(0, 0, 0, 0.04); /* Sanftere, natürlichere Schatten */ + --light-shadow: rgba(0, 0, 0, 0.04); --light-shadow-strong: rgba(0, 0, 0, 0.08); - --light-shadow-accent: rgba(0, 115, 206, 0.08); - /* Neue Light Mode Gradients - VERBESSERT für sanftere, harmonischere Optik */ - --light-gradient-primary: linear-gradient(135deg, #ffffff 0%, #fafbfc 25%, #f8fafc 75%, #f3f5f7 100%); - --light-gradient-card: linear-gradient(135deg, #ffffff 0%, #fdfdfe 50%, #fafbfc 100%); - --light-gradient-hero: linear-gradient(135deg, #fafbfc 0%, #f5f7f9 30%, #f0f3f6 70%, #f8fafc 100%); - --light-gradient-accent: linear-gradient(135deg, #0073ce 0%, #005a9f 100%); - - /* Dark Mode Farbpalette - UNVERÄNDERT (bereits perfekt) */ + /* Dark Mode Farbpalette */ --dark-bg-primary: #0f172a; --dark-bg-secondary: #1e293b; --dark-bg-tertiary: #334155; @@ -54,118 +43,81 @@ --dark-shadow-strong: rgba(0, 0, 0, 0.5); } -/* Professionelle Hero-Header Stile - VERBESSERT für Light Mode */ +/* Vereinfachte Hero-Header */ .professional-hero { position: relative; overflow: hidden; - border-radius: 1.5rem; + border-radius: 1rem; margin: 1.5rem; margin-bottom: 2rem; - background: var(--light-gradient-hero); + background: var(--light-bg-secondary); border: 1px solid var(--light-border); - box-shadow: - 0 8px 25px var(--light-shadow), - 0 4px 12px rgba(0, 115, 206, 0.03), - inset 0 1px 0 rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; + box-shadow: 0 4px 12px var(--light-shadow); + transition: transform 0.2s ease, box-shadow 0.2s ease; } .professional-hero:hover { transform: translateY(-1px); - box-shadow: - 0 12px 35px var(--light-shadow-strong), - 0 6px 16px var(--light-shadow-accent), - inset 0 1px 0 rgba(255, 255, 255, 0.95); + box-shadow: 0 8px 20px var(--light-shadow-strong); } .dark .professional-hero { - background: linear-gradient(135deg, var(--dark-bg-primary) 0%, var(--dark-bg-secondary) 100%); + background: var(--dark-bg-secondary); border: 1px solid var(--dark-border); - box-shadow: 0 20px 40px var(--dark-shadow-strong); + box-shadow: 0 8px 20px var(--dark-shadow); } -.professional-hero::before { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.06) 50%, transparent 70%); - opacity: 0.6; -} - -.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 - OPTIMIERT */ -.hero-pattern { - background-image: - radial-gradient(circle at 20% 20%, var(--light-border) 0.8px, transparent 0.8px), - radial-gradient(circle at 80% 80%, var(--light-border) 0.8px, transparent 0.8px); - background-size: 48px 48px; - background-position: 0 0, 24px 24px; -} - -.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 - VERBESSERT */ +/* Vereinfachte Container */ .professional-container { background: var(--light-surface); border: 1px solid var(--light-border); - border-radius: 1rem; - box-shadow: 0 4px 20px var(--light-shadow); - backdrop-filter: blur(16px); - transition: all 0.3s ease; + border-radius: 0.75rem; + box-shadow: 0 2px 8px var(--light-shadow); + transition: transform 0.2s ease, box-shadow 0.2s ease; } .dark .professional-container { background: var(--dark-surface); border: 1px solid var(--dark-border); - box-shadow: 0 10px 30px var(--dark-shadow); + box-shadow: 0 4px 12px var(--dark-shadow); } .professional-container:hover { - transform: translateY(-2px); - box-shadow: 0 8px 30px var(--light-shadow-strong); + transform: translateY(-1px); + box-shadow: 0 4px 16px var(--light-shadow-strong); } .dark .professional-container:hover { - box-shadow: 0 20px 40px var(--dark-shadow-strong); + box-shadow: 0 8px 20px var(--dark-shadow-strong); } -/* Mercedes-Benz Glassmorphism Effekt - OPTIMIERT für Light Mode */ +/* Vereinfachte Glassmorphism-Effekte */ .mb-glass { - background: rgba(255, 255, 255, 0.94); - backdrop-filter: blur(16px) saturate(180%); + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(8px); border: 1px solid rgba(229, 231, 235, 0.4); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04); - transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + transition: transform 0.2s ease, background 0.2s ease; } .dark .mb-glass { - background: rgba(15, 23, 42, 0.9); + background: rgba(15, 23, 42, 0.85); border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); } .mb-glass:hover { - background: rgba(255, 255, 255, 0.97); + background: rgba(255, 255, 255, 0.95); transform: translateY(-1px); - box-shadow: 0 8px 28px rgba(0, 0, 0, 0.06); } .dark .mb-glass:hover { - background: rgba(15, 23, 42, 0.95); - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); + background: rgba(15, 23, 42, 0.9); } -/* Professional Buttons - VERBESSERT */ +/* Vereinfachte Buttons */ .btn-professional { - background: var(--light-gradient-accent); + background: linear-gradient(135deg, var(--mb-primary) 0%, var(--mb-primary-dark) 100%); color: white; border: none; border-radius: 0.5rem; @@ -174,73 +126,50 @@ font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.025em; - transition: all 0.2s ease; - box-shadow: - 0 2px 8px rgba(0, 115, 206, 0.2), - 0 1px 3px rgba(0, 115, 206, 0.08); - position: relative; - overflow: hidden; -} - -.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 ease; + transition: transform 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0 2px 8px rgba(0, 115, 206, 0.2); } .btn-professional:hover { transform: translateY(-1px); - box-shadow: - 0 4px 15px rgba(0, 115, 206, 0.25), - 0 2px 8px rgba(0, 115, 206, 0.15); -} - -.btn-professional:hover::before { - left: 100%; + box-shadow: 0 4px 12px rgba(0, 115, 206, 0.3); } .btn-professional:active { transform: translateY(0); } -/* Secondary Button Style - VERBESSERT */ +/* Secondary Button */ .btn-secondary-professional { background: var(--light-surface); color: var(--light-text-primary); border: 1px solid var(--light-border-strong); - border-radius: 0.75rem; - padding: 0.75rem 1.75rem; + border-radius: 0.5rem; + padding: 0.75rem 1.5rem; font-weight: 600; font-size: 0.875rem; - transition: all 0.3s ease; - box-shadow: 0 2px 8px var(--light-shadow); + transition: all 0.2s ease; + box-shadow: 0 1px 4px 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); + box-shadow: 0 2px 8px var(--dark-shadow); } .btn-secondary-professional:hover { background: var(--light-surface-hover); border-color: var(--mb-primary); transform: translateY(-1px); - box-shadow: 0 4px 15px 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 - VERBESSERT */ +/* Vereinfachte Input Fields */ .input-professional { background: var(--light-surface); border: 1px solid var(--light-border); @@ -248,20 +177,20 @@ padding: 0.75rem 1rem; color: var(--light-text-primary); font-size: 0.875rem; - transition: all 0.3s ease; - box-shadow: 0 1px 6px var(--light-shadow); + transition: border-color 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0 1px 4px 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); + box-shadow: 0 2px 4px var(--dark-shadow); } .input-professional:focus { border-color: var(--mb-primary); - box-shadow: 0 0 0 3px rgba(0, 115, 206, 0.06); + box-shadow: 0 0 0 3px rgba(0, 115, 206, 0.1); transform: translateY(-1px); } @@ -273,77 +202,51 @@ color: var(--dark-text-muted); } -/* Professional Cards - VERBESSERT */ +/* Vereinfachte Cards */ .card-professional { - background: var(--light-gradient-card); + background: var(--light-surface); border: 1px solid var(--light-border); border-radius: 0.75rem; padding: 1.5rem; - box-shadow: - 0 2px 12px var(--light-shadow), - 0 1px 4px rgba(0, 115, 206, 0.02), - inset 0 1px 0 rgba(255, 255, 255, 0.8); - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.card-professional::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 1px; - background: var(--light-gradient-accent); - opacity: 0; - transition: opacity 0.3s ease; + box-shadow: 0 2px 8px var(--light-shadow); + transition: transform 0.2s ease, box-shadow 0.2s ease; } .card-professional:hover { transform: translateY(-2px); - box-shadow: - 0 8px 25px var(--light-shadow-strong), - 0 4px 12px var(--light-shadow-accent), - inset 0 1px 0 rgba(255, 255, 255, 0.9); -} - -.card-professional:hover::before { - opacity: 1; + box-shadow: 0 4px 16px var(--light-shadow-strong); } .dark .card-professional { background: var(--dark-surface); border-color: var(--dark-border); - box-shadow: 0 4px 15px var(--dark-shadow); + box-shadow: 0 4px 12px var(--dark-shadow); } -/* Professional Statistics Cards - VERBESSERT */ +/* Vereinfachte Statistics Cards */ .stat-card { background: var(--light-surface); border: 1px solid var(--light-border); border-radius: 0.75rem; padding: 1.5rem; text-align: center; - transition: all 0.3s ease; - box-shadow: 0 2px 12px var(--light-shadow); - position: relative; - overflow: hidden; + transition: transform 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0 2px 8px var(--light-shadow); } .dark .stat-card { background: var(--dark-surface); border-color: var(--dark-border); - box-shadow: 0 4px 15px var(--dark-shadow); + box-shadow: 0 4px 12px var(--dark-shadow); } .stat-card:hover { - transform: translateY(-1px) scale(1.01); - box-shadow: 0 6px 20px var(--light-shadow-strong); + transform: translateY(-1px); + box-shadow: 0 4px 16px var(--light-shadow-strong); } .dark .stat-card:hover { - box-shadow: 0 8px 30px var(--dark-shadow-strong); + box-shadow: 0 8px 20px var(--dark-shadow-strong); } .stat-number { @@ -370,7 +273,7 @@ color: var(--dark-text-muted); } -/* Professional Status Badges */ +/* Vereinfachte Status Badges */ .status-professional { display: inline-flex; align-items: center; @@ -381,121 +284,61 @@ font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; - transition: all 0.2s ease; + transition: transform 0.2s ease; border: 1px solid transparent; } .status-professional:hover { - transform: scale(1.05); + transform: scale(1.02); } -/* Verbesserte Status Indicators */ +/* Status Indicators */ .status-online { - background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); + background: #ecfdf5; color: #065f46; border: 1px solid rgba(16, 185, 129, 0.2); - box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1); } .dark .status-online { - background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(16, 185, 129, 0.05) 100%); + background: rgba(16, 185, 129, 0.15); color: #10b981; border-color: rgba(16, 185, 129, 0.3); } .status-offline { - background: linear-gradient(135deg, #fef2f2 0%, #fecaca 100%); + background: #fef2f2; color: #991b1b; border: 1px solid rgba(239, 68, 68, 0.2); - box-shadow: 0 2px 8px rgba(239, 68, 68, 0.1); } .dark .status-offline { - background: linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(239, 68, 68, 0.05) 100%); + background: rgba(239, 68, 68, 0.15); color: #ef4444; border-color: rgba(239, 68, 68, 0.3); } .status-printing { - background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); + background: #eff6ff; color: #1d4ed8; border: 1px solid rgba(59, 130, 246, 0.2); - box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1); } .dark .status-printing { - background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(59, 130, 246, 0.05) 100%); + background: rgba(59, 130, 246, 0.15); color: #3b82f6; border-color: rgba(59, 130, 246, 0.3); } -.status-maintenance { - background: linear-gradient(135deg, #faf5ff 0%, #ede9fe 100%); - color: #6b21a8; - border: 1px solid rgba(139, 92, 246, 0.2); - box-shadow: 0 2px 8px rgba(139, 92, 246, 0.1); -} - -.dark .status-maintenance { - background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(139, 92, 246, 0.05) 100%); - color: #8b5cf6; - border-color: rgba(139, 92, 246, 0.3); -} - -.status-pending { - background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); - color: #92400e; - border: 1px solid rgba(251, 191, 36, 0.2); - box-shadow: 0 2px 8px rgba(251, 191, 36, 0.1); -} - -.dark .status-pending { - background: linear-gradient(135deg, rgba(251, 191, 36, 0.15) 0%, rgba(251, 191, 36, 0.05) 100%); - color: #fbbf24; - border-color: rgba(251, 191, 36, 0.3); -} - -.status-approved { - background: linear-gradient(135deg, #ecfdf5 0%, #a7f3d0 100%); - color: #065f46; - border: 1px solid rgba(16, 185, 129, 0.3); - box-shadow: 0 2px 8px rgba(16, 185, 129, 0.15); -} - -.dark .status-approved { - background: linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(16, 185, 129, 0.1) 100%); - color: #10b981; -} - -.status-denied { - background: linear-gradient(135deg, #fef2f2 0%, #fca5a5 100%); - color: #991b1b; - border: 1px solid rgba(239, 68, 68, 0.3); - box-shadow: 0 2px 8px rgba(239, 68, 68, 0.15); -} - -.dark .status-denied { - background: linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(239, 68, 68, 0.1) 100%); - color: #ef4444; -} - -/* Professional Typography */ +/* Vereinfachte Typography */ .title-professional { - background: linear-gradient(135deg, var(--light-text-primary) 0%, var(--light-text-accent) 50%, var(--light-text-secondary) 100%); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; + color: var(--light-text-primary); font-weight: 700; letter-spacing: -0.025em; line-height: 1.1; - text-shadow: 0 2px 4px rgba(0, 0, 0, 0.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; + color: var(--dark-text-primary); } .subtitle-professional { @@ -512,21 +355,17 @@ /* Professional Navigation */ .nav-professional { - background: var(--light-gradient-card); + background: var(--light-bg-secondary); border: 1px solid var(--light-border); - border-radius: 1rem; + border-radius: 0.75rem; padding: 0.5rem; - box-shadow: - 0 4px 15px var(--light-shadow), - 0 2px 8px rgba(0, 115, 206, 0.05), - inset 0 1px 0 rgba(255, 255, 255, 0.6); - backdrop-filter: blur(20px); + box-shadow: 0 2px 8px var(--light-shadow); } .dark .nav-professional { - background: var(--dark-surface); + background: var(--dark-bg-secondary); border-color: var(--dark-border); - box-shadow: 0 4px 15px var(--dark-shadow); + box-shadow: 0 4px 12px var(--dark-shadow); } .nav-item-professional { @@ -563,8 +402,8 @@ background: linear-gradient(135deg, rgba(0, 115, 206, 0.1) 0%, rgba(0, 115, 206, 0.05) 100%); color: var(--mb-primary); font-weight: 600; - border: 1px solid var(--light-border-accent); - box-shadow: 0 2px 8px var(--light-shadow-accent); + border: 1px solid var(--light-border-strong); + box-shadow: 0 2px 8px var(--light-shadow); } .dark .nav-item-professional.active { @@ -575,23 +414,20 @@ .table-professional { width: 100%; border-collapse: collapse; - background: var(--light-gradient-card); - border-radius: 1rem; + background: var(--light-bg-secondary); + border-radius: 0.75rem; overflow: hidden; - box-shadow: - 0 4px 20px var(--light-shadow), - 0 2px 8px rgba(0, 115, 206, 0.05), - inset 0 1px 0 rgba(255, 255, 255, 0.6); + box-shadow: 0 2px 8px var(--light-shadow); border: 1px solid var(--light-border); } .dark .table-professional { - background: var(--dark-surface); - box-shadow: 0 4px 20px var(--dark-shadow); + background: var(--dark-bg-secondary); + box-shadow: 0 4px 12px var(--dark-shadow); } .table-professional th { - background: linear-gradient(135deg, var(--light-bg-secondary) 0%, var(--light-bg-tertiary) 100%); + background: linear-gradient(135deg, var(--light-bg-tertiary) 0%, var(--light-bg-secondary) 100%); color: var(--light-text-primary); font-weight: 600; text-align: left; @@ -611,7 +447,7 @@ } .dark .table-professional th { - background: var(--dark-bg-secondary); + background: var(--dark-bg-tertiary); color: var(--dark-text-primary); border-bottom-color: var(--dark-border); } @@ -723,7 +559,7 @@ /* Background Gradients für verschiedene Seiten */ .bg-professional { - background: var(--light-gradient-primary); + background: var(--light-bg-secondary); min-height: 100vh; position: relative; } diff --git a/backend/static/css/professional-theme.css.gz b/backend/static/css/professional-theme.css.gz new file mode 100644 index 00000000..352cee58 Binary files /dev/null and b/backend/static/css/professional-theme.css.gz differ diff --git a/backend/static/css/professional-theme.min.css b/backend/static/css/professional-theme.min.css new file mode 100644 index 00000000..308b9280 --- /dev/null +++ b/backend/static/css/professional-theme.min.css @@ -0,0 +1 @@ +:root{--mb-primary:#0073ce;--mb-primary-dark:#005a9f;--mb-secondary:#64748b;--mb-accent:#0ea5e9;--mb-black:#000;--mb-silver:silver;--light-bg-primary:#fff;--light-bg-secondary:#fafbfc;--light-bg-tertiary:#f3f5f7;--light-surface:#fff;--light-surface-hover:#fafbfc;--light-text-primary:#111827;--light-text-secondary:#374151;--light-text-muted:#6b7280;--light-text-accent:#0073ce;--light-border:#e5e7eb;--light-border-strong:#d1d5db;--light-shadow:rgba(0,0,0,0.04);--light-shadow-strong:rgba(0,0,0,0.08);--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)}.professional-hero{position:relative;overflow:hidden;border-radius:1rem;margin:1.5rem;margin-bottom:2rem;background:var(--light-bg-secondary);border:1px solid var(--light-border);box-shadow:0 4px 12px var(--light-shadow);transition:transform .2s ease,box-shadow .2s ease}.professional-hero:hover{transform:translateY(-1px);box-shadow:0 8px 20px var(--light-shadow-strong)}.dark .professional-hero{background:var(--dark-bg-secondary);border:1px solid var(--dark-border);box-shadow:0 8px 20px var(--dark-shadow)}.professional-container{background:var(--light-surface);border:1px solid var(--light-border);border-radius:.75rem;box-shadow:0 2px 8px var(--light-shadow);transition:transform .2s ease,box-shadow .2s ease}.dark .professional-container{background:var(--dark-surface);border:1px solid var(--dark-border);box-shadow:0 4px 12px var(--dark-shadow)}.professional-container:hover{transform:translateY(-1px);box-shadow:0 4px 16px var(--light-shadow-strong)}.dark .professional-container:hover{box-shadow:0 8px 20px var(--dark-shadow-strong)}.mb-glass{background:rgba(255,255,255,0.9);backdrop-filter:blur(8px);border:1px solid rgba(229,231,235,0.4);box-shadow:0 2px 8px rgba(0,0,0,0.04);transition:transform .2s ease,background .2s ease}.dark .mb-glass{background:rgba(15,23,42,0.85);border:1px solid rgba(255,255,255,0.1);box-shadow:0 4px 12px rgba(0,0,0,0.25)}.mb-glass:hover{background:rgba(255,255,255,0.95);transform:translateY(-1px)}.dark .mb-glass:hover{background:rgba(15,23,42,0.9)}.btn-professional{background:linear-gradient(135deg,var(--mb-primary) 0,var(--mb-primary-dark) 100%);color:white;border:0;border-radius:.5rem;padding:.75rem 1.5rem;font-weight:600;font-size:.875rem;text-transform:uppercase;letter-spacing:.025em;transition:transform .2s ease,box-shadow .2s ease;box-shadow:0 2px 8px rgba(0,115,206,0.2)}.btn-professional:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,115,206,0.3)}.btn-professional:active{transform:translateY(0)}.btn-secondary-professional{background:var(--light-surface);color:var(--light-text-primary);border:1px solid var(--light-border-strong);border-radius:.5rem;padding:.75rem 1.5rem;font-weight:600;font-size:.875rem;transition:all .2s ease;box-shadow:0 1px 4px 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 2px 8px var(--dark-shadow)}.btn-secondary-professional:hover{background:var(--light-surface-hover);border-color:var(--mb-primary);transform:translateY(-1px)}.dark .btn-secondary-professional:hover{background:var(--dark-surface-hover)}.input-professional{background:var(--light-surface);border:1px solid var(--light-border);border-radius:.5rem;padding:.75rem 1rem;color:var(--light-text-primary);font-size:.875rem;transition:border-color .2s ease,box-shadow .2s ease;box-shadow:0 1px 4px var(--light-shadow)}.dark .input-professional{background:var(--dark-surface);border-color:var(--dark-border);color:var(--dark-text-primary);box-shadow:0 2px 4px var(--dark-shadow)}.input-professional:focus{border-color:var(--mb-primary);box-shadow:0 0 0 3px rgba(0,115,206,0.1);transform:translateY(-1px)}.input-professional::placeholder{color:var(--light-text-muted)}.dark .input-professional::placeholder{color:var(--dark-text-muted)}.card-professional{background:var(--light-surface);border:1px solid var(--light-border);border-radius:.75rem;padding:1.5rem;box-shadow:0 2px 8px var(--light-shadow);transition:transform .2s ease,box-shadow .2s ease}.card-professional:hover{transform:translateY(-2px);box-shadow:0 4px 16px var(--light-shadow-strong)}.dark .card-professional{background:var(--dark-surface);border-color:var(--dark-border);box-shadow:0 4px 12px var(--dark-shadow)}.stat-card{background:var(--light-surface);border:1px solid var(--light-border);border-radius:.75rem;padding:1.5rem;text-align:center;transition:transform .2s ease,box-shadow .2s ease;box-shadow:0 2px 8px var(--light-shadow)}.dark .stat-card{background:var(--dark-surface);border-color:var(--dark-border);box-shadow:0 4px 12px var(--dark-shadow)}.stat-card:hover{transform:translateY(-1px);box-shadow:0 4px 16px var(--light-shadow-strong)}.dark .stat-card:hover{box-shadow:0 8px 20px var(--dark-shadow-strong)}.stat-number{font-size:2.25rem;font-weight:700;color:var(--light-text-primary);line-height:1;margin-bottom:.5rem}.dark .stat-number{color:var(--dark-text-primary)}.stat-label{font-size:.875rem;font-weight:500;color:var(--light-text-muted);text-transform:uppercase;letter-spacing:.05em}.dark .stat-label{color:var(--dark-text-muted)}.status-professional{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:9999px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;transition:transform .2s ease;border:1px solid transparent}.status-professional:hover{transform:scale(1.02)}.status-online{background:#ecfdf5;color:#065f46;border:1px solid rgba(16,185,129,0.2)}.dark .status-online{background:rgba(16,185,129,0.15);color:#10b981;border-color:rgba(16,185,129,0.3)}.status-offline{background:#fef2f2;color:#991b1b;border:1px solid rgba(239,68,68,0.2)}.dark .status-offline{background:rgba(239,68,68,0.15);color:#ef4444;border-color:rgba(239,68,68,0.3)}.status-printing{background:#eff6ff;color:#1d4ed8;border:1px solid rgba(59,130,246,0.2)}.dark .status-printing{background:rgba(59,130,246,0.15);color:#3b82f6;border-color:rgba(59,130,246,0.3)}.title-professional{color:var(--light-text-primary);font-weight:700;letter-spacing:-.025em;line-height:1.1}.dark .title-professional{color:var(--dark-text-primary)}.subtitle-professional{color:var(--light-text-muted);font-size:1.125rem;line-height:1.6;font-weight:400;opacity:.9}.dark .subtitle-professional{color:var(--dark-text-muted)}.nav-professional{background:var(--light-bg-secondary);border:1px solid var(--light-border);border-radius:.75rem;padding:.5rem;box-shadow:0 2px 8px var(--light-shadow)}.dark .nav-professional{background:var(--dark-bg-secondary);border-color:var(--dark-border);box-shadow:0 4px 12px var(--dark-shadow)}.nav-item-professional{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;border-radius:.5rem;color:var(--light-text-secondary);text-decoration:none;font-weight:500;transition:all .2s ease;position:relative;background:transparent}.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);box-shadow:0 2px 8px var(--light-shadow)}.dark .nav-item-professional:hover{background:var(--dark-surface-hover);color:var(--dark-text-primary)}.nav-item-professional.active{background:linear-gradient(135deg,rgba(0,115,206,0.1) 0,rgba(0,115,206,0.05) 100%);color:var(--mb-primary);font-weight:600;border:1px solid var(--light-border-strong);box-shadow:0 2px 8px var(--light-shadow)}.dark .nav-item-professional.active{background:rgba(59,130,246,0.2)}.table-professional{width:100%;border-collapse:collapse;background:var(--light-bg-secondary);border-radius:.75rem;overflow:hidden;box-shadow:0 2px 8px var(--light-shadow);border:1px solid var(--light-border)}.dark .table-professional{background:var(--dark-bg-secondary);box-shadow:0 4px 12px var(--dark-shadow)}.table-professional th{background:linear-gradient(135deg,var(--light-bg-tertiary) 0,var(--light-bg-secondary) 100%);color:var(--light-text-primary);font-weight:600;text-align:left;padding:1rem 1.5rem;border-bottom:1px solid var(--light-border);position:relative}.table-professional th::after{content:'';position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent 0,var(--light-border-strong) 50%,transparent 100%)}.dark .table-professional th{background:var(--dark-bg-tertiary);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);transition:all .2s ease}.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);transform:scale(1.005);box-shadow:0 2px 8px var(--light-shadow)}.dark .table-professional tbody tr:hover{background:var(--dark-surface-hover)}.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);position:relative;overflow:hidden}.alert-professional::before{content:'';position:absolute;top:0;left:0;bottom:0;width:4px}.alert-info{background:linear-gradient(135deg,rgba(59,130,246,0.08) 0,rgba(59,130,246,0.03) 100%);border-color:rgba(59,130,246,0.2);color:#1e40af}.alert-info::before{background:linear-gradient(180deg,#3b82f6 0,#1d4ed8 100%)}.dark .alert-info{background:rgba(59,130,246,0.2);color:#60a5fa}.alert-success{background:linear-gradient(135deg,rgba(16,185,129,0.08) 0,rgba(16,185,129,0.03) 100%);border-color:rgba(16,185,129,0.2);color:#065f46}.alert-success::before{background:linear-gradient(180deg,#10b981 0,#059669 100%)}.dark .alert-success{background:rgba(16,185,129,0.2);color:#10b981}.alert-warning{background:linear-gradient(135deg,rgba(251,191,36,0.08) 0,rgba(251,191,36,0.03) 100%);border-color:rgba(251,191,36,0.2);color:#92400e}.alert-warning::before{background:linear-gradient(180deg,#fbbf24 0,#f59e0b 100%)}.dark .alert-warning{background:rgba(251,191,36,0.2);color:#fbbf24}.alert-error{background:linear-gradient(135deg,rgba(239,68,68,0.08) 0,rgba(239,68,68,0.03) 100%);border-color:rgba(239,68,68,0.2);color:#991b1b}.alert-error::before{background:linear-gradient(180deg,#ef4444 0,#dc2626 100%)}.dark .alert-error{background:rgba(239,68,68,0.2);color:#ef4444}.bg-professional{background:var(--light-bg-secondary);min-height:100vh;position:relative}.bg-professional::before{content:'';position:fixed;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at 20% 50%,rgba(0,115,206,0.03) 0,transparent 50%),radial-gradient(circle at 80% 20%,rgba(0,115,206,0.02) 0,transparent 50%),radial-gradient(circle at 40% 80%,rgba(0,115,206,0.01) 0,transparent 50%);pointer-events:none;z-index:-1}.dark .bg-professional{background:linear-gradient(135deg,var(--dark-bg-primary) 0,var(--dark-bg-secondary) 50%,var(--dark-bg-tertiary) 100%)}.text-professional-primary{color:var(--light-text-primary);font-weight:600}.dark .text-professional-primary{color:var(--dark-text-primary)}.text-professional-secondary{color:var(--light-text-secondary);font-weight:500}.dark .text-professional-secondary{color:var(--dark-text-secondary)}.text-professional-muted{color:var(--light-text-muted);font-weight:400}.dark .text-professional-muted{color:var(--dark-text-muted)}.text-professional-accent{color:var(--light-text-accent);font-weight:600}.dark .text-professional-accent{color:#60a5fa}.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;position:relative}.censored-text::after{content:'';position:absolute;top:50%;left:0;right:0;height:1px;background:currentColor;opacity:.3}.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 .3s cubic-bezier(0.4,0,0.2,1)}@media(max-width:768px){.professional-hero{margin:1rem;border-radius:1.5rem}.card-professional{padding:1rem}.nav-professional{padding:.25rem}.nav-item-professional{padding:.5rem .75rem}.stat-number{font-size:2rem}.btn-professional,.btn-secondary-professional{padding:.625rem 1.5rem;font-size:.8rem}}.animate-fade-in{animation:fadeInProfessional .6s ease-out}.animate-slide-up{animation:slideUpProfessional .6s ease-out}.animate-scale-in{animation:scaleInProfessional .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)}}.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:.75rem;padding:.5rem 1rem;font-weight:500;font-size:.75rem;text-transform:uppercase;letter-spacing:.025em;transition:all .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)}.btn-danger-professional-mini{background:rgba(239,68,68,0.1);color:#dc2626;border:1px solid rgba(239,68,68,0.2);border-radius:.5rem;padding:.5rem;font-weight:500;font-size:.75rem;transition:all .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}.btn-success-professional{background:linear-gradient(135deg,#10b981 0,#059669 100%);color:white;border:0;border-radius:1rem;padding:.75rem 2rem;font-weight:600;font-size:.875rem;letter-spacing:.025em;text-transform:uppercase;transition:all .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 .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)}.btn-danger-professional{background:linear-gradient(135deg,#ef4444 0,#dc2626 100%);color:white;border:0;border-radius:1rem;padding:.75rem 2rem;font-weight:600;font-size:.875rem;letter-spacing:.025em;text-transform:uppercase;transition:all .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 .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-btn-mercedes{background:transparent;color:var(--light-text-muted);border:0;border-radius:.75rem;padding:.5rem 1rem;font-weight:500;font-size:.875rem;transition:all .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-dot{width:.75rem;height:.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{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)}.bg-professional-accent{background-color:var(--mb-accent)}.animate-spin-slow{animation:spin 3s linear infinite}@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse-professional{0%,100%{opacity:1;transform:translate(-50%,-50%) scale(1)}50%{opacity:.5;transform:translate(-50%,-50%) scale(1.2)}}.glass-professional{background:rgba(255,255,255,0.8);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.3);box-shadow:0 25px 50px rgba(0,0,0,0.15),0 8px 16px rgba(0,115,206,0.1),inset 0 1px 0 rgba(255,255,255,0.6);border-radius:1rem}.dark .glass-professional{background:rgba(30,41,59,0.8);border-color:rgba(255,255,255,0.1);box-shadow:0 25px 50px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.1)}.hover-lift{transition:all .3s cubic-bezier(0.4,0,0.2,1)}.hover-lift:hover{transform:translateY(-2px);box-shadow:0 10px 25px var(--light-shadow-strong),0 4px 12px var(--light-shadow-accent)}.dark .hover-lift:hover{box-shadow:0 10px 25px var(--dark-shadow-strong)}@media(prefers-reduced-motion:reduce){*{transition:none !important;animation:none !important}}.focus-professional:focus{outline:2px solid var(--light-text-accent);outline-offset:2px;box-shadow:0 0 0 4px rgba(0,115,206,0.15)}.dark .focus-professional:focus{outline-color:#60a5fa;box-shadow:0 0 0 4px rgba(96,165,250,0.15)} \ No newline at end of file diff --git a/backend/static/css/professional-theme.min.css.gz b/backend/static/css/professional-theme.min.css.gz new file mode 100644 index 00000000..4f0ce7c9 Binary files /dev/null and b/backend/static/css/professional-theme.min.css.gz differ diff --git a/backend/static/css/tailwind.min.css.gz b/backend/static/css/tailwind.min.css.gz new file mode 100644 index 00000000..8c4fb1a6 Binary files /dev/null and b/backend/static/css/tailwind.min.css.gz differ diff --git a/backend/static/fontawesome/css/all.css.gz b/backend/static/fontawesome/css/all.css.gz new file mode 100644 index 00000000..e4d4b0c5 Binary files /dev/null and b/backend/static/fontawesome/css/all.css.gz differ diff --git a/backend/static/fontawesome/css/all.min.css.gz b/backend/static/fontawesome/css/all.min.css.gz new file mode 100644 index 00000000..8271dba7 Binary files /dev/null and b/backend/static/fontawesome/css/all.min.css.gz differ diff --git a/backend/static/fontawesome/css/brands.css.gz b/backend/static/fontawesome/css/brands.css.gz new file mode 100644 index 00000000..676f95eb Binary files /dev/null and b/backend/static/fontawesome/css/brands.css.gz differ diff --git a/backend/static/fontawesome/css/brands.min.css.gz b/backend/static/fontawesome/css/brands.min.css.gz new file mode 100644 index 00000000..df23d764 Binary files /dev/null and b/backend/static/fontawesome/css/brands.min.css.gz differ diff --git a/backend/static/fontawesome/css/fontawesome.css.gz b/backend/static/fontawesome/css/fontawesome.css.gz new file mode 100644 index 00000000..8d6fd034 Binary files /dev/null and b/backend/static/fontawesome/css/fontawesome.css.gz differ diff --git a/backend/static/fontawesome/css/fontawesome.min.css.gz b/backend/static/fontawesome/css/fontawesome.min.css.gz new file mode 100644 index 00000000..f2879f51 Binary files /dev/null and b/backend/static/fontawesome/css/fontawesome.min.css.gz differ diff --git a/backend/static/fontawesome/css/regular.css.gz b/backend/static/fontawesome/css/regular.css.gz new file mode 100644 index 00000000..7f3a47a6 Binary files /dev/null and b/backend/static/fontawesome/css/regular.css.gz differ diff --git a/backend/static/fontawesome/css/regular.min.css.gz b/backend/static/fontawesome/css/regular.min.css.gz new file mode 100644 index 00000000..1e9fe762 Binary files /dev/null and b/backend/static/fontawesome/css/regular.min.css.gz differ diff --git a/backend/static/fontawesome/css/solid.css.gz b/backend/static/fontawesome/css/solid.css.gz new file mode 100644 index 00000000..569ab28d Binary files /dev/null and b/backend/static/fontawesome/css/solid.css.gz differ diff --git a/backend/static/fontawesome/css/solid.min.css.gz b/backend/static/fontawesome/css/solid.min.css.gz new file mode 100644 index 00000000..65169ba7 Binary files /dev/null and b/backend/static/fontawesome/css/solid.min.css.gz differ diff --git a/backend/static/fontawesome/css/svg-with-js.css.gz b/backend/static/fontawesome/css/svg-with-js.css.gz new file mode 100644 index 00000000..05a503ea Binary files /dev/null and b/backend/static/fontawesome/css/svg-with-js.css.gz differ diff --git a/backend/static/fontawesome/css/svg-with-js.min.css.gz b/backend/static/fontawesome/css/svg-with-js.min.css.gz new file mode 100644 index 00000000..0c2c977b Binary files /dev/null and b/backend/static/fontawesome/css/svg-with-js.min.css.gz differ diff --git a/backend/static/fontawesome/css/v4-font-face.css.gz b/backend/static/fontawesome/css/v4-font-face.css.gz new file mode 100644 index 00000000..a4f9b8b3 Binary files /dev/null and b/backend/static/fontawesome/css/v4-font-face.css.gz differ diff --git a/backend/static/fontawesome/css/v4-font-face.min.css.gz b/backend/static/fontawesome/css/v4-font-face.min.css.gz new file mode 100644 index 00000000..498c04ec Binary files /dev/null and b/backend/static/fontawesome/css/v4-font-face.min.css.gz differ diff --git a/backend/static/fontawesome/css/v4-shims.css.gz b/backend/static/fontawesome/css/v4-shims.css.gz new file mode 100644 index 00000000..3458b891 Binary files /dev/null and b/backend/static/fontawesome/css/v4-shims.css.gz differ diff --git a/backend/static/fontawesome/css/v4-shims.min.css.gz b/backend/static/fontawesome/css/v4-shims.min.css.gz new file mode 100644 index 00000000..446ae647 Binary files /dev/null and b/backend/static/fontawesome/css/v4-shims.min.css.gz differ diff --git a/backend/static/fontawesome/css/v5-font-face.css.gz b/backend/static/fontawesome/css/v5-font-face.css.gz new file mode 100644 index 00000000..22e606b2 Binary files /dev/null and b/backend/static/fontawesome/css/v5-font-face.css.gz differ diff --git a/backend/static/fontawesome/css/v5-font-face.min.css.gz b/backend/static/fontawesome/css/v5-font-face.min.css.gz new file mode 100644 index 00000000..1db50dce Binary files /dev/null and b/backend/static/fontawesome/css/v5-font-face.min.css.gz differ diff --git a/backend/static/fontawesome/js/all.js.gz b/backend/static/fontawesome/js/all.js.gz new file mode 100644 index 00000000..880f582e Binary files /dev/null and b/backend/static/fontawesome/js/all.js.gz differ diff --git a/backend/static/fontawesome/js/all.min.js.gz b/backend/static/fontawesome/js/all.min.js.gz new file mode 100644 index 00000000..6b95e29a Binary files /dev/null and b/backend/static/fontawesome/js/all.min.js.gz differ diff --git a/backend/static/fontawesome/js/brands.js.gz b/backend/static/fontawesome/js/brands.js.gz new file mode 100644 index 00000000..b379ae49 Binary files /dev/null and b/backend/static/fontawesome/js/brands.js.gz differ diff --git a/backend/static/fontawesome/js/brands.min.js.gz b/backend/static/fontawesome/js/brands.min.js.gz new file mode 100644 index 00000000..4bb798c7 Binary files /dev/null and b/backend/static/fontawesome/js/brands.min.js.gz differ diff --git a/backend/static/fontawesome/js/conflict-detection.js.gz b/backend/static/fontawesome/js/conflict-detection.js.gz new file mode 100644 index 00000000..4caa7904 Binary files /dev/null and b/backend/static/fontawesome/js/conflict-detection.js.gz differ diff --git a/backend/static/fontawesome/js/conflict-detection.min.js.gz b/backend/static/fontawesome/js/conflict-detection.min.js.gz new file mode 100644 index 00000000..44f1d832 Binary files /dev/null and b/backend/static/fontawesome/js/conflict-detection.min.js.gz differ diff --git a/backend/static/fontawesome/js/fontawesome.js.gz b/backend/static/fontawesome/js/fontawesome.js.gz new file mode 100644 index 00000000..aa8015fe Binary files /dev/null and b/backend/static/fontawesome/js/fontawesome.js.gz differ diff --git a/backend/static/fontawesome/js/fontawesome.min.js.gz b/backend/static/fontawesome/js/fontawesome.min.js.gz new file mode 100644 index 00000000..94ea72e2 Binary files /dev/null and b/backend/static/fontawesome/js/fontawesome.min.js.gz differ diff --git a/backend/static/fontawesome/js/regular.js.gz b/backend/static/fontawesome/js/regular.js.gz new file mode 100644 index 00000000..bd3e54a1 Binary files /dev/null and b/backend/static/fontawesome/js/regular.js.gz differ diff --git a/backend/static/fontawesome/js/regular.min.js.gz b/backend/static/fontawesome/js/regular.min.js.gz new file mode 100644 index 00000000..575cf03b Binary files /dev/null and b/backend/static/fontawesome/js/regular.min.js.gz differ diff --git a/backend/static/fontawesome/js/solid.js.gz b/backend/static/fontawesome/js/solid.js.gz new file mode 100644 index 00000000..d7205d19 Binary files /dev/null and b/backend/static/fontawesome/js/solid.js.gz differ diff --git a/backend/static/fontawesome/js/solid.min.js.gz b/backend/static/fontawesome/js/solid.min.js.gz new file mode 100644 index 00000000..d714e489 Binary files /dev/null and b/backend/static/fontawesome/js/solid.min.js.gz differ diff --git a/backend/static/fontawesome/js/v4-shims.js.gz b/backend/static/fontawesome/js/v4-shims.js.gz new file mode 100644 index 00000000..7bef8f62 Binary files /dev/null and b/backend/static/fontawesome/js/v4-shims.js.gz differ diff --git a/backend/static/fontawesome/js/v4-shims.min.js.gz b/backend/static/fontawesome/js/v4-shims.min.js.gz new file mode 100644 index 00000000..3f377b69 Binary files /dev/null and b/backend/static/fontawesome/js/v4-shims.min.js.gz differ diff --git a/backend/static/fontawesome/metadata/icon-families.json.gz b/backend/static/fontawesome/metadata/icon-families.json.gz new file mode 100644 index 00000000..7bb74505 Binary files /dev/null and b/backend/static/fontawesome/metadata/icon-families.json.gz differ diff --git a/backend/static/fontawesome/package.json.gz b/backend/static/fontawesome/package.json.gz new file mode 100644 index 00000000..80c2a1fa Binary files /dev/null and b/backend/static/fontawesome/package.json.gz differ diff --git a/backend/static/icons/apple-touch-icon.webp b/backend/static/icons/apple-touch-icon.webp new file mode 100644 index 00000000..eec6d4b3 Binary files /dev/null and b/backend/static/icons/apple-touch-icon.webp differ diff --git a/backend/static/icons/favicon-16x16.webp b/backend/static/icons/favicon-16x16.webp new file mode 100644 index 00000000..3ebb91fa Binary files /dev/null and b/backend/static/icons/favicon-16x16.webp differ diff --git a/backend/static/icons/favicon-32x32.webp b/backend/static/icons/favicon-32x32.webp new file mode 100644 index 00000000..ccc48799 Binary files /dev/null and b/backend/static/icons/favicon-32x32.webp differ diff --git a/backend/static/icons/icon-128x128.webp b/backend/static/icons/icon-128x128.webp new file mode 100644 index 00000000..76681f54 Binary files /dev/null and b/backend/static/icons/icon-128x128.webp differ diff --git a/backend/static/icons/icon-144x144.webp b/backend/static/icons/icon-144x144.webp new file mode 100644 index 00000000..c8154cb3 Binary files /dev/null and b/backend/static/icons/icon-144x144.webp differ diff --git a/backend/static/icons/icon-152x152.webp b/backend/static/icons/icon-152x152.webp new file mode 100644 index 00000000..7954a73a Binary files /dev/null and b/backend/static/icons/icon-152x152.webp differ diff --git a/backend/static/icons/icon-192x192.webp b/backend/static/icons/icon-192x192.webp new file mode 100644 index 00000000..b290a4f2 Binary files /dev/null and b/backend/static/icons/icon-192x192.webp differ diff --git a/backend/static/icons/icon-384x384.webp b/backend/static/icons/icon-384x384.webp new file mode 100644 index 00000000..5d455c6a Binary files /dev/null and b/backend/static/icons/icon-384x384.webp differ diff --git a/backend/static/icons/icon-512x512.webp b/backend/static/icons/icon-512x512.webp new file mode 100644 index 00000000..4f3017c9 Binary files /dev/null and b/backend/static/icons/icon-512x512.webp differ diff --git a/backend/static/icons/icon-72x72.webp b/backend/static/icons/icon-72x72.webp new file mode 100644 index 00000000..7f287318 Binary files /dev/null and b/backend/static/icons/icon-72x72.webp differ diff --git a/backend/static/icons/icon-96x96.webp b/backend/static/icons/icon-96x96.webp new file mode 100644 index 00000000..eec6d4b3 Binary files /dev/null and b/backend/static/icons/icon-96x96.webp differ diff --git a/backend/static/js/admin-guest-requests.js.gz b/backend/static/js/admin-guest-requests.js.gz new file mode 100644 index 00000000..9774f14f Binary files /dev/null and b/backend/static/js/admin-guest-requests.js.gz differ diff --git a/backend/static/js/admin-guest-requests.min.js b/backend/static/js/admin-guest-requests.min.js new file mode 100644 index 00000000..a18d27b3 --- /dev/null +++ b/backend/static/js/admin-guest-requests.min.js @@ -0,0 +1,223 @@ +let currentRequests=[];let filteredRequests=[];let currentPage=0;let totalPages=0;let totalRequests=0;let refreshInterval=null;let csrfToken='';function detectApiBaseUrl(){return'';} +const API_BASE_URL=detectApiBaseUrl();document.addEventListener('DOMContentLoaded',function(){csrfToken=document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')||'';initEventListeners();loadGuestRequests();startAutoRefresh();console.log('🎯 Admin Guest Requests Management geladen');});function initEventListeners(){const searchInput=document.getElementById('search-requests');if(searchInput){searchInput.addEventListener('input',debounce(handleSearch,300));} +const statusFilter=document.getElementById('status-filter');if(statusFilter){statusFilter.addEventListener('change',handleFilterChange);} +const sortOrder=document.getElementById('sort-order');if(sortOrder){sortOrder.addEventListener('change',handleSortChange);} +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);} +const selectAllCheckbox=document.getElementById('select-all');if(selectAllCheckbox){selectAllCheckbox.addEventListener('change',handleSelectAll);}} +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;updateStats(data.stats||{});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);}} +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);}});} +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);} +function applyFiltersAndSort(){let requests=[...currentRequests];const statusFilter=document.getElementById('status-filter')?.value;if(statusFilter&&statusFilter!=='all'){requests=requests.filter(req=>req.status===statusFilter);} +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));} +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();} +function getPriorityValue(request){const now=new Date();const created=new Date(request.created_at);const hoursOld=(now-created)/(1000*60*60);let priority=0;if(request.status==='pending')priority+=10;else if(request.status==='approved')priority+=5;if(hoursOld>24)priority+=5;else if(hoursOld>8)priority+=3;else if(hoursOld>2)priority+=1;return priority;} +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;addRowEventListeners();} +function createRequestRow(request){const statusColor=getStatusColor(request.status);const priorityLevel=getPriorityLevel(request);const timeAgo=getTimeAgo(request.created_at);return`
${request.name?request.name[0].toUpperCase():'G'}
${escapeHtml(request.name||'Unbekannt')}
${escapeHtml(request.email||'Keine E-Mail')}
${escapeHtml(request.file_name||'Keine Datei')}
${request.duration_minutes?`${request.duration_minutes}Min.`:'Unbekannte Dauer'} +${request.copies?`• ${request.copies}Kopien`:''}
${request.reason?`
${escapeHtml(request.reason)}
`:''}${getStatusText(request.status)}
${timeAgo}
${formatDateTime(request.created_at)}
${getPriorityBadge(priorityLevel)} +${request.is_urgent?'🔥 Dringend':''}
${request.status==='pending'?``:''}
`;} +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':'🔴 Hoch','medium':'🟡 Mittel','low':'🟢 Niedrig'};return badges[level]||badges['low'];} +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({})});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);}} +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=`

Gastauftrag Details

Antragsteller

Name:${escapeHtml(request.name||'Unbekannt')}

E-Mail:${escapeHtml(request.email||'Keine E-Mail')}

Erstellt am:${formatDateTime(request.created_at)}

Auftrag Details

Datei:${escapeHtml(request.file_name||'Keine Datei')}

Dauer:${request.duration_minutes||'Unbekannt'}Minuten

Kopien:${request.copies||1}

Status:${getStatusText(request.status)}

${request.reason?`

Begründung

${escapeHtml(request.reason)}

`:''}
${request.status==='pending'?``:''}
`;modal.classList.remove('hidden');} +function closeDetailModal(){const modal=document.getElementById('detail-modal');modal.classList.add('hidden');} +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();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));} +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);} +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 = ` +
+ + ${type === 'success' ? '✅' : + type === 'error' ? '❌' : + type === 'warning' ? '⚠️' : 'ℹ️'} + + ${message} +
+ `; + + 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' \ No newline at end of file diff --git a/backend/static/js/admin-guest-requests.min.js.gz b/backend/static/js/admin-guest-requests.min.js.gz new file mode 100644 index 00000000..9fced462 Binary files /dev/null and b/backend/static/js/admin-guest-requests.min.js.gz differ diff --git a/backend/static/js/admin-panel.js.gz b/backend/static/js/admin-panel.js.gz new file mode 100644 index 00000000..1d19c8ae Binary files /dev/null and b/backend/static/js/admin-panel.js.gz differ diff --git a/backend/static/js/admin-panel.min.js b/backend/static/js/admin-panel.min.js new file mode 100644 index 00000000..ca87023e --- /dev/null +++ b/backend/static/js/admin-panel.min.js @@ -0,0 +1,77 @@ +document.addEventListener('DOMContentLoaded',function(){initializeAdminPanel();loadAdminStats();initializeActiveTab();initializeEventHandlers();setInterval(function(){if(document.visibilityState==='visible'){refreshActiveTabData();}},30000);});function initializeActiveTab(){const hash=window.location.hash.substring(1);if(hash){activateTab(hash);}else{const defaultTab=document.querySelector('.nav-tab');if(defaultTab){const tabName=defaultTab.getAttribute('data-tab');activateTab(tabName);}}} +function activateTab(tabName){const tabs=document.querySelectorAll('.nav-tab');tabs.forEach(tab=>{if(tab.getAttribute('data-tab')===tabName){tab.classList.add('active');}else{tab.classList.remove('active');}});const tabPanes=document.querySelectorAll('.tab-pane');tabPanes.forEach(pane=>{if(pane.id===`${tabName}-tab`){pane.classList.add('active');pane.classList.remove('hidden');}else{pane.classList.remove('active');pane.classList.add('hidden');}});switch(tabName){case'users':loadUsers();break;case'printers':loadPrinters();break;case'scheduler':loadSchedulerStatus();break;case'system':loadSystemStats();break;case'logs':loadLogs();break;} +window.location.hash=tabName;} +function refreshActiveTabData(){const activeTab=document.querySelector('.nav-tab.active');if(!activeTab)return;const tabName=activeTab.getAttribute('data-tab');switch(tabName){case'users':loadUsers();break;case'printers':loadPrinters();break;case'scheduler':loadSchedulerStatus();break;case'system':loadSystemStats();break;case'logs':loadLogs();break;} +loadAdminStats();} +function initializeEventHandlers(){const addUserBtn=document.getElementById('add-user-btn');if(addUserBtn){addUserBtn.addEventListener('click',showAddUserModal);} +const addPrinterBtn=document.getElementById('add-printer-btn');if(addPrinterBtn){addPrinterBtn.addEventListener('click',showAddPrinterModal);} +const refreshLogsBtn=document.getElementById('refresh-logs-btn');if(refreshLogsBtn){refreshLogsBtn.addEventListener('click',refreshLogs);} +const closeDeleteModalBtn=document.getElementById('close-delete-modal');if(closeDeleteModalBtn){closeDeleteModalBtn.addEventListener('click',closeDeleteModal);} +const cancelDeleteBtn=document.getElementById('cancel-delete');if(cancelDeleteBtn){cancelDeleteBtn.addEventListener('click',closeDeleteModal);} +const closeAddUserBtn=document.getElementById('close-add-user-modal');if(closeAddUserBtn){closeAddUserBtn.addEventListener('click',closeAddUserModal);} +const cancelAddUserBtn=document.getElementById('cancel-add-user');if(cancelAddUserBtn){cancelAddUserBtn.addEventListener('click',closeAddUserModal);} +const confirmAddUserBtn=document.getElementById('confirm-add-user');if(confirmAddUserBtn){confirmAddUserBtn.addEventListener('click',addUser);} +const closeAddPrinterBtn=document.getElementById('close-add-printer-modal');if(closeAddPrinterBtn){closeAddPrinterBtn.addEventListener('click',closeAddPrinterModal);} +const cancelAddPrinterBtn=document.getElementById('cancel-add-printer');if(cancelAddPrinterBtn){cancelAddPrinterBtn.addEventListener('click',closeAddPrinterModal);} +const confirmAddPrinterBtn=document.getElementById('confirm-add-printer');if(confirmAddPrinterBtn){confirmAddPrinterBtn.addEventListener('click',addPrinter);} +const startSchedulerBtn=document.getElementById('start-scheduler');if(startSchedulerBtn){startSchedulerBtn.addEventListener('click',startScheduler);} +const stopSchedulerBtn=document.getElementById('stop-scheduler');if(stopSchedulerBtn){stopSchedulerBtn.addEventListener('click',stopScheduler);} +const refreshButton=document.createElement('button');refreshButton.id='refresh-admin-btn';refreshButton.className='fixed bottom-8 right-8 p-3 bg-primary text-white rounded-full shadow-lg z-40';refreshButton.innerHTML=``;refreshButton.title='Alle Daten aktualisieren';refreshButton.addEventListener('click',refreshAllData);if(!document.getElementById('refresh-admin-btn')){document.body.appendChild(refreshButton);}} +function showAddUserModal(){const modal=document.getElementById('add-user-modal');if(modal){const form=document.getElementById('add-user-form');if(form){form.reset();} +modal.classList.remove('hidden');modal.classList.add('show');modal.style.display='flex';}} +function closeAddUserModal(){const modal=document.getElementById('add-user-modal');if(modal){modal.classList.remove('show');modal.classList.add('hidden');modal.style.display='none';}} +async function addUser(){const form=document.getElementById('add-user-form');if(!form)return;const userData={email:document.getElementById('user-email').value,name:document.getElementById('user-name').value,password:document.getElementById('user-password').value,role:document.getElementById('user-role').value,status:document.getElementById('user-status').value};try{const response=await fetch('/api/users',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':getCSRFToken()},body:JSON.stringify(userData)});if(!response.ok){throw new Error('Fehler beim Hinzufügen des Benutzers');} +showNotification('Benutzer erfolgreich hinzugefügt','success');closeAddUserModal();loadUsers();}catch(error){console.error('Error adding user:',error);showNotification(error.message,'error');}} +function showAddPrinterModal(){const modal=document.getElementById('add-printer-modal');if(modal){const form=document.getElementById('add-printer-form');if(form){form.reset();} +modal.classList.remove('hidden');modal.classList.add('show');modal.style.display='flex';}} +function closeAddPrinterModal(){const modal=document.getElementById('add-printer-modal');if(modal){modal.classList.remove('show');modal.classList.add('hidden');modal.style.display='none';}} +async function addPrinter(){const form=document.getElementById('add-printer-form');if(!form)return;const printerData={name:document.getElementById('printer-name').value,model:document.getElementById('printer-model').value,ip_address:document.getElementById('printer-ip').value,location:document.getElementById('printer-location').value,status:document.getElementById('printer-status').value};try{const response=await fetch('/api/printers',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':getCSRFToken()},body:JSON.stringify(printerData)});if(!response.ok){throw new Error('Fehler beim Hinzufügen des Druckers');} +showNotification('Drucker erfolgreich hinzugefügt','success');closeAddPrinterModal();loadPrinters();}catch(error){console.error('Error adding printer:',error);showNotification(error.message,'error');}} +function refreshLogs(){loadLogs();showNotification('Logs aktualisiert','info');} +function closeDeleteModal(){const modal=document.getElementById('delete-confirm-modal');if(modal){modal.classList.remove('show');modal.style.display='none';}} +function showDeleteConfirmation(id,type,name,callback){const modal=document.getElementById('delete-confirm-modal');const messageEl=document.getElementById('delete-message');const idField=document.getElementById('delete-id');const typeField=document.getElementById('delete-type');const confirmBtn=document.getElementById('confirm-delete');if(modal&&messageEl&&idField&&typeField&&confirmBtn){messageEl.textContent=`Möchten Sie ${type==='user'?'den Benutzer':'den Drucker'}"${name}"wirklich löschen?`;idField.value=id;typeField.value=type;confirmBtn.onclick=function(){if(typeof callback==='function'){callback(id,name);} +closeDeleteModal();};modal.classList.add('show');modal.style.display='flex';}} +function initializeAdminPanel(){const tabs=document.querySelectorAll('.nav-tab');tabs.forEach(tab=>{tab.addEventListener('click',function(){const tabName=this.getAttribute('data-tab');if(!tabName)return;activateTab(tabName);});});const logLevelFilter=document.getElementById('log-level-filter');if(logLevelFilter){logLevelFilter.addEventListener('change',function(){if(window.logsData){const level=this.value;if(level==='all'){window.filteredLogs=[...window.logsData];}else{window.filteredLogs=window.logsData.filter(log=>log.level.toLowerCase()===level);} +renderLogs();}});} +const confirmDeleteBtn=document.getElementById('confirm-delete');if(confirmDeleteBtn){confirmDeleteBtn.addEventListener('click',function(){const id=document.getElementById('delete-id').value;const type=document.getElementById('delete-type').value;if(type==='user'){deleteUser(id);}else if(type==='printer'){deletePrinter(id);} +closeDeleteModal();});}} +async function loadAdminStats(){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();const statsContainer=document.getElementById('admin-stats');if(!statsContainer){console.warn('Stats-Container nicht gefunden');return;} +statsContainer.innerHTML=`
Benutzer
${data.total_users||0}
Registrierte Benutzer
Drucker
${data.total_printers||0}
Verbundene Drucker
Aktive Jobs
${data.active_jobs||0}
Laufende Druckaufträge
Erfolgsrate
${data.success_rate||'0%'}
Erfolgreiche Druckaufträge
`;console.log('✅ Admin-Statistiken erfolgreich aktualisiert:',data);}catch(error){console.error('Error loading admin stats:',error);showNotification('Fehler beim Laden der Admin-Statistiken','error');}} +async function loadUsers(){try{const response=await fetch('/api/users');if(!response.ok){throw new Error('Fehler beim Laden der Benutzer');} +const data=await response.json();const userTableBody=document.querySelector('#users-tab table tbody');if(!userTableBody)return;let html='';if(data.users&&data.users.length>0){data.users.forEach(user=>{html+=`${user.id}${user.name||'-'}${user.email}${user.active?'Aktiv':'Inaktiv'}${user.role||'Benutzer'}
`;});}else{html=`Keine Benutzer gefunden`;} +userTableBody.innerHTML=html;}catch(error){console.error('Error loading users:',error);showNotification('Fehler beim Laden der Benutzer','error');}} +async function loadPrinters(){try{const response=await fetch('/api/printers');if(!response.ok){throw new Error('Fehler beim Laden der Drucker');} +const data=await response.json();const printerTableBody=document.querySelector('#printers-tab table tbody');if(!printerTableBody)return;let html='';if(data.printers&&data.printers.length>0){data.printers.forEach(printer=>{html+=`${printer.id}${printer.name}${printer.model||'-'}${printer.ip_address||'-'}${printer.location||'-'}${printer.status==='online'?'Online':'Offline'}
`;});}else{html=`Keine Drucker gefunden`;} +printerTableBody.innerHTML=html;}catch(error){console.error('Error loading printers:',error);showNotification('Fehler beim Laden der Drucker','error');}} +async function loadSchedulerStatus(){try{const response=await fetch('/api/scheduler/status');if(!response.ok){throw new Error('Fehler beim Laden des Scheduler-Status');} +const data=await response.json();const statusIndicator=document.getElementById('scheduler-status-indicator');const statusText=document.getElementById('scheduler-status-text');const startSchedulerBtn=document.getElementById('start-scheduler');const stopSchedulerBtn=document.getElementById('stop-scheduler');if(statusIndicator&&statusText){if(data.active){statusIndicator.classList.remove('bg-red-500');statusIndicator.classList.add('bg-green-500');statusText.textContent='Aktiv';if(startSchedulerBtn&&stopSchedulerBtn){startSchedulerBtn.disabled=true;stopSchedulerBtn.disabled=false;}}else{statusIndicator.classList.remove('bg-green-500');statusIndicator.classList.add('bg-red-500');statusText.textContent='Inaktiv';if(startSchedulerBtn&&stopSchedulerBtn){startSchedulerBtn.disabled=false;stopSchedulerBtn.disabled=true;}}} +const schedulerDetails=document.getElementById('scheduler-details');if(schedulerDetails){schedulerDetails.innerHTML=`

Letzte Ausführung

${data.last_run?new Date(data.last_run).toLocaleString('de-DE'):'Nie'}

Nächste Ausführung

${data.next_run?new Date(data.next_run).toLocaleString('de-DE'):'Nicht geplant'}

Ausführungsintervall

${data.interval||'60'}Sekunden

Verarbeitete Jobs

${data.processed_jobs||0}Jobs seit dem letzten Neustart

`;} +const jobQueueContainer=document.getElementById('job-queue');if(jobQueueContainer&&data.pending_jobs){let queueHtml='';if(data.pending_jobs.length>0){queueHtml=`
`;data.pending_jobs.forEach(job=>{queueHtml+=``;});queueHtml+=`
Job IDNameGeplant fürStatus
${job.id}${job.name}${new Date(job.start_time).toLocaleString('de-DE')}Warten
`;}else{queueHtml=`

Keine ausstehenden Jobs in der Warteschlange

`;} +jobQueueContainer.innerHTML=queueHtml;}}catch(error){console.error('Error loading scheduler status:',error);showNotification('Fehler beim Laden des Scheduler-Status','error');}} +async function loadSystemStats(){try{const response=await fetch('/api/system/stats');if(!response.ok){throw new Error('Fehler beim Laden der Systemstatistiken');} +const data=await response.json();const cpuUsageElement=document.getElementById('cpu-usage');if(cpuUsageElement){cpuUsageElement.style.width=`${data.cpu_usage||0}%`;cpuUsageElement.textContent=`${data.cpu_usage||0}%`;} +const ramUsageElement=document.getElementById('ram-usage');if(ramUsageElement){ramUsageElement.style.width=`${data.ram_usage_percent||0}%`;ramUsageElement.textContent=`${data.ram_usage_percent||0}%`;} +const diskUsageElement=document.getElementById('disk-usage');if(diskUsageElement){diskUsageElement.style.width=`${data.disk_usage_percent||0}%`;diskUsageElement.textContent=`${data.disk_usage_percent||0}%`;} +const systemDetailsElement=document.getElementById('system-details');if(systemDetailsElement){systemDetailsElement.innerHTML=`

System

${data.os_name||'Unbekannt'}${data.os_version||''}

Laufzeit

${data.uptime||'Unbekannt'}

Python-Version

${data.python_version||'Unbekannt'}

Server-Zeit

${data.server_time?new Date(data.server_time).toLocaleString('de-DE'):'Unbekannt'}

`;} +const systemEventsElement=document.getElementById('system-events');if(systemEventsElement&&data.recent_events){let eventsHtml='';if(data.recent_events.length>0){eventsHtml='
    ';data.recent_events.forEach(event=>{let eventTypeClass='text-blue-600 dark:text-blue-400';switch(event.type.toLowerCase()){case'error':eventTypeClass='text-red-600 dark:text-red-400';break;case'warning':eventTypeClass='text-yellow-600 dark:text-yellow-400';break;case'success':eventTypeClass='text-green-600 dark:text-green-400';break;} +eventsHtml+=`
  • ${event.message}

    ${new Date(event.timestamp).toLocaleString('de-DE')}

  • `;});eventsHtml+='
';}else{eventsHtml='

Keine Ereignisse vorhanden

';} +systemEventsElement.innerHTML=eventsHtml;}}catch(error){console.error('Error loading system stats:',error);showNotification('Fehler beim Laden der Systemstatistiken','error');}} +async function loadLogs(){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();}catch(error){console.error('Error loading logs:',error);showNotification('Fehler beim Laden der Logs','error');}} +function renderLogs(){const logsContainer=document.getElementById('logs-container');if(!logsContainer)return;if(!window.filteredLogs||window.filteredLogs.length===0){logsContainer.innerHTML='
Keine Logs gefunden
';return;} +let html='';window.filteredLogs.forEach(log=>{let levelClass='';switch(log.level.toLowerCase()){case'error':levelClass='bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';break;case'warning':levelClass='bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';break;case'info':levelClass='bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';break;case'debug':levelClass='bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';break;default:levelClass='bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';} +html+=`
${log.level}${log.timestamp?new Date(log.timestamp).toLocaleString('de-DE'):''}
${log.source||'System'}
${log.message}
${log.details?`
${log.details}
`:''}
`;});logsContainer.innerHTML=html;} +async function deleteUser(userId){try{const response=await fetch(`/api/users/${userId}`,{method:'DELETE',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Löschen des Benutzers');} +showNotification('Benutzer erfolgreich gelöscht','success');loadUsers();}catch(error){console.error('Error deleting user:',error);showNotification(error.message,'error');}} +async function deletePrinter(printerId){try{const response=await fetch(`/api/printers/${printerId}`,{method:'DELETE',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Löschen des Druckers');} +showNotification('Drucker erfolgreich gelöscht','success');loadPrinters();}catch(error){console.error('Error deleting printer:',error);showNotification(error.message,'error');}} +async function startScheduler(){try{const response=await fetch('/api/scheduler/start',{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Starten des Schedulers');} +showNotification('Scheduler erfolgreich gestartet','success');loadSchedulerStatus();}catch(error){console.error('Error starting scheduler:',error);showNotification(error.message,'error');}} +async function stopScheduler(){try{const response=await fetch('/api/scheduler/stop',{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}});if(!response.ok){throw new Error('Fehler beim Stoppen des Schedulers');} +showNotification('Scheduler erfolgreich gestoppt','success');loadSchedulerStatus();}catch(error){console.error('Error stopping scheduler:',error);showNotification(error.message,'error');}} +function getCSRFToken(){return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')||'';} +function refreshAllData(){loadAdminStats();const activeTab=document.querySelector('.nav-tab.active');if(activeTab){const tabName=activeTab.getAttribute('data-tab');activateTab(tabName);} +showNotification('Daten wurden aktualisiert','success');} +function showNotification(message,type='info'){const notification=document.getElementById('notification');const messageEl=document.getElementById('notification-message');const iconEl=document.getElementById('notification-icon');if(!notification||!messageEl||!iconEl)return;messageEl.textContent=message;notification.classList.remove('notification-success','notification-error','notification-warning','notification-info');notification.classList.add(`notification-${type}`);let icon='';switch(type){case'success':icon='';break;case'error':icon='';break;case'warning':icon='';break;case'info':default:icon='';break;} +iconEl.innerHTML=icon;notification.classList.remove('hidden');notification.classList.add('show');setTimeout(()=>{notification.classList.remove('show');setTimeout(()=>{notification.classList.add('hidden');},300);},5000);} \ No newline at end of file diff --git a/backend/static/js/admin-panel.min.js.gz b/backend/static/js/admin-panel.min.js.gz new file mode 100644 index 00000000..6bdf4843 Binary files /dev/null and b/backend/static/js/admin-panel.min.js.gz differ diff --git a/backend/static/js/admin-unified.js.gz b/backend/static/js/admin-unified.js.gz new file mode 100644 index 00000000..0127181c Binary files /dev/null and b/backend/static/js/admin-unified.js.gz differ diff --git a/backend/static/js/admin-unified.min.js b/backend/static/js/admin-unified.min.js new file mode 100644 index 00000000..f3533945 --- /dev/null +++ b/backend/static/js/admin-unified.min.js @@ -0,0 +1,101 @@ +class AdminDashboard{constructor(){this.csrfToken=null;this.updateInterval=null;this.eventListenersAttached=false;this.apiBaseUrl=this.detectApiBaseUrl();this.retryCount=0;this.maxRetries=3;this.isInitialized=false;this.init();} +detectApiBaseUrl(){const currentHost=window.location.hostname;const currentPort=window.location.port;if(currentPort==='5000'){return'';} +return`http:} +init(){if(this.isInitialized){console.log('🔄 Admin Dashboard bereits initialisiert, überspringe...');return;} +console.log('🚀 Initialisiere Mercedes-Benz MYP Admin Dashboard');this.csrfToken=this.extractCSRFToken();console.log('🔒 CSRF Token:',this.csrfToken?'verfügbar':'FEHLT!');this.attachEventListeners();this.startLiveUpdates();this.loadInitialData();this.isInitialized=true;console.log('✅ Admin Dashboard erfolgreich initialisiert');} +extractCSRFToken(){const metaToken=document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');if(metaToken){console.log('🔒 CSRF Token aus meta Tag geladen');return metaToken;} +const hiddenInput=document.querySelector('input[name="csrf_token"]')?.value;if(hiddenInput){console.log('🔒 CSRF Token aus hidden input geladen');return hiddenInput;} +const cookieToken=document.cookie.split('; ').find(row=>row.startsWith('csrf_token='))?.split('=')[1];if(cookieToken){console.log('🔒 CSRF Token aus Cookie geladen');return cookieToken;} +const flaskToken=document.querySelector('meta[name="csrf-token"]')?.content;if(flaskToken){console.log('🔒 CSRF Token aus Flask-WTF Meta geladen');return flaskToken;} +console.error('❌ CSRF Token konnte nicht gefunden werden!');return null;} +attachEventListeners(){if(this.eventListenersAttached){console.log('⚠️ Event-Listener bereits registriert, überspringe...');return;} +this.attachSystemButtons();this.attachUserManagement();this.attachPrinterManagement();this.attachJobManagement();this.attachModalEvents();this.eventListenersAttached=true;console.log('📌 Event-Listener erfolgreich registriert');} +attachSystemButtons(){this.addEventListenerSafe('#system-status-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showSystemStatus();});this.addEventListenerSafe('#analytics-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showAnalytics();});this.addEventListenerSafe('#clear-cache-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.clearSystemCache();});this.addEventListenerSafe('#optimize-db-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.optimizeDatabase();});this.addEventListenerSafe('#create-backup-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.createSystemBackup();});this.addEventListenerSafe('#force-init-printers-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.forceInitializePrinters();});} +attachUserManagement(){this.addEventListenerSafe('#add-user-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showUserModal();});document.addEventListener('click',(e)=>{if(e.target.closest('.edit-user-btn')){e.preventDefault();e.stopPropagation();const userId=e.target.closest('button').dataset.userId;this.editUser(userId);} +if(e.target.closest('.delete-user-btn')){e.preventDefault();e.stopPropagation();const userId=e.target.closest('button').dataset.userId;const userName=e.target.closest('button').dataset.userName;this.deleteUser(userId,userName);}});} +attachPrinterManagement(){this.addEventListenerSafe('#add-printer-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.showPrinterModal();});document.addEventListener('click',(e)=>{if(e.target.closest('.manage-printer-btn')){e.preventDefault();e.stopPropagation();const printerId=e.target.closest('button').dataset.printerId;this.managePrinter(printerId);} +if(e.target.closest('.settings-printer-btn')){e.preventDefault();e.stopPropagation();const printerId=e.target.closest('button').dataset.printerId;this.showPrinterSettings(printerId);} +if(e.target.closest('.toggle-printer-power-btn')){e.preventDefault();e.stopPropagation();const button=e.target.closest('button');const printerId=button.dataset.printerId;const printerName=button.dataset.printerName;this.togglePrinterPower(printerId,printerName,button);}});} +attachJobManagement(){document.addEventListener('click',(e)=>{if(e.target.closest('.job-action-btn')){e.preventDefault();e.stopPropagation();const action=e.target.closest('button').dataset.action;const jobId=e.target.closest('button').dataset.jobId;this.handleJobAction(action,jobId);}});} +attachModalEvents(){this.addEventListenerSafe('#fix-errors-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.fixErrors();});this.addEventListenerSafe('#dismiss-errors-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.dismissErrors();});this.addEventListenerSafe('#view-error-details-btn','click',(e)=>{e.preventDefault();e.stopPropagation();window.location.href='/admin-dashboard?tab=logs';});this.addEventListenerSafe('#refresh-logs-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.loadLogs();});this.addEventListenerSafe('#export-logs-btn','click',(e)=>{e.preventDefault();e.stopPropagation();this.exportLogs();});this.addEventListenerSafe('#log-level-filter','change',(e)=>{this.loadLogs();});} +addEventListenerSafe(selector,event,handler){const element=document.querySelector(selector);if(element&&!element.dataset.listenerAttached){element.addEventListener(event,handler);element.dataset.listenerAttached='true';}} +startLiveUpdates(){if(this.updateInterval){clearInterval(this.updateInterval);} +this.updateInterval=setInterval(()=>{this.loadLiveStats();},30000);setInterval(()=>{this.updateLiveTime();},1000);setInterval(()=>{this.checkSystemHealth();},30000);console.log('🔄 Live-Updates gestartet');} +async loadInitialData(){await this.loadLiveStats();await this.checkSystemHealth();setTimeout(()=>{this.testButtons();},1000);if(window.location.search.includes('tab=logs')||document.querySelector('.tabs [href*="logs"]')?.classList.contains('active')){await this.loadLogs();} +const urlParams=new URLSearchParams(window.location.search);const activeTab=urlParams.get('tab');if(activeTab==='logs'){await this.loadLogs();} +const logsContainer=document.getElementById('logs-container');if(logsContainer&&logsContainer.offsetParent!==null){await this.loadLogs();}} +async loadLiveStats(){try{const url=`${this.apiBaseUrl}/api/stats`;const response=await fetch(url);if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const data=await response.json();this.updateStatsDisplay(data);this.retryCount=0;}catch(error){console.error('Fehler beim Laden der Live-Statistiken:',error);this.retryCount++;if(this.retryCount<=this.maxRetries){setTimeout(()=>this.loadLiveStats(),5000);}}} +updateStatsDisplay(data){this.updateElement('live-users-count',data.total_users||0);this.updateElement('live-printers-count',data.total_printers||0);this.updateElement('live-printers-online',`${data.online_printers||0}online`);this.updateElement('live-jobs-active',data.active_jobs||0);this.updateElement('live-jobs-queued',`${data.queued_jobs||0}in Warteschlange`);this.updateElement('live-success-rate',`${data.success_rate||0}%`);this.updateProgressBar('users-progress',data.total_users,20);this.updateProgressBar('printers-progress',data.online_printers,data.total_printers);this.updateProgressBar('jobs-progress',data.active_jobs,10);this.updateProgressBar('success-progress',data.success_rate,100);console.log('📊 Live-Statistiken aktualisiert');} +updateElement(elementId,value){const element=document.getElementById(elementId);if(element){element.textContent=value;}} +updateProgressBar(progressId,currentValue,maxValue){const progressEl=document.getElementById(progressId);if(progressEl&¤tValue!==undefined&&maxValue>0){const percentage=Math.min(100,Math.max(0,(currentValue/maxValue)*100));progressEl.style.width=`${percentage}%`;}} +updateLiveTime(){const timeElement=document.getElementById('live-time');if(timeElement){const now=new Date();timeElement.textContent=now.toLocaleTimeString('de-DE');}} +async showSystemStatus(){console.log('🔧 System Status wird angezeigt');this.showNotification('System Status wird geladen...','info');} +async showAnalytics(){console.log('📈 Analytics wird angezeigt');this.showNotification('Analytics werden geladen...','info');} +async showMaintenance(){console.log('🛠️ Wartung wird angezeigt');const systemTab=document.querySelector('a[href*="tab=system"]');if(systemTab){systemTab.click();}} +async clearSystemCache(){if(!confirm('🗑️ Möchten Sie wirklich den System-Cache leeren?'))return;try{const response=await fetch(`${this.apiBaseUrl}/api/admin/cache/clear`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Cache erfolgreich geleert!','success');setTimeout(()=>window.location.reload(),2000);}else{this.showNotification('❌ Fehler beim Leeren des Cache','error');}}catch(error){this.showNotification('❌ Fehler beim Leeren des Cache','error');}} +async optimizeDatabase(){if(!confirm('🔧 Möchten Sie die Datenbank optimieren?'))return;this.showNotification('🔄 Datenbank wird optimiert...','info');try{const response=await fetch(`${this.apiBaseUrl}/api/admin/database/optimize`,{method:'POST',headers:{'X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Datenbank erfolgreich optimiert!','success');}else{this.showNotification('❌ Fehler bei der Datenbank-Optimierung','error');}}catch(error){this.showNotification('❌ Fehler bei der Datenbank-Optimierung','error');}} +async createSystemBackup(){if(!confirm('💾 Möchten Sie ein System-Backup erstellen?'))return;this.showNotification('🔄 Backup wird erstellt...','info');try{const response=await fetch(`${this.apiBaseUrl}/api/admin/backup/create`,{method:'POST',headers:{'X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Backup erfolgreich erstellt!','success');}else{this.showNotification('❌ Fehler beim Erstellen des Backups','error');}}catch(error){this.showNotification('❌ Fehler beim Erstellen des Backups','error');}} +async forceInitializePrinters(){if(!confirm('🔄 Möchten Sie die Drucker-Initialisierung erzwingen?'))return;this.showNotification('🔄 Drucker werden initialisiert...','info');try{const response=await fetch(`${this.apiBaseUrl}/api/admin/printers/force-init`,{method:'POST',headers:{'X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification('✅ Drucker erfolgreich initialisiert!','success');setTimeout(()=>window.location.reload(),2000);}else{this.showNotification('❌ Fehler bei der Drucker-Initialisierung','error');}}catch(error){this.showNotification('❌ Fehler bei der Drucker-Initialisierung','error');}} +showUserModal(userId=null){const isEdit=userId!==null;const title=isEdit?'Benutzer bearbeiten':'Neuer Benutzer';const modalHtml=`

${title}

${isEdit?`
`:''}
`;document.body.insertAdjacentHTML('beforeend',modalHtml);const form=document.getElementById('user-form');form.addEventListener('submit',(e)=>{e.preventDefault();e.stopPropagation();if(isEdit){this.updateUser(userId,new FormData(form));}else{this.createUser(new FormData(form));}});if(isEdit){this.loadUserData(userId);} +setTimeout(()=>{document.getElementById('user-email').focus();},100);} +async loadUserData(userId){try{const response=await fetch(`${this.apiBaseUrl}/api/admin/users/${userId}`);if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const data=await response.json();if(data.success){const user=data.user;document.getElementById('user-email').value=user.email||'';document.getElementById('user-username').value=user.username||'';document.getElementById('user-name').value=user.name||'';document.getElementById('user-role').value=user.is_admin?'admin':'user';const activeCheckbox=document.getElementById('user-active');if(activeCheckbox){activeCheckbox.checked=user.is_active!==false;}}else{this.showNotification('❌ Fehler beim Laden der Benutzerdaten','error');}}catch(error){console.error('Fehler beim Laden der Benutzerdaten:',error);this.showNotification('❌ Fehler beim Laden der Benutzerdaten','error');}} +async createUser(formData){const submitBtn=document.getElementById('user-submit-btn');const originalText=submitBtn.innerHTML;try{submitBtn.innerHTML='
';submitBtn.disabled=true;const userData={email:formData.get('email'),username:formData.get('username')||formData.get('email').split('@')[0],name:formData.get('name'),password:formData.get('password'),is_admin:formData.get('role')==='admin'};const response=await fetch(`${this.apiBaseUrl}/api/admin/users`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken},body:JSON.stringify(userData)});const data=await response.json();if(data.success){this.showNotification('✅ Benutzer erfolgreich erstellt!','success');document.getElementById('user-modal').remove();setTimeout(()=>{window.location.reload();},1000);}else{this.showNotification(`❌ Fehler:${data.error}`,'error');}}catch(error){console.error('Fehler beim Erstellen des Benutzers:',error);this.showNotification('❌ Fehler beim Erstellen des Benutzers','error');}finally{submitBtn.innerHTML=originalText;submitBtn.disabled=false;}} +async updateUser(userId,formData){const submitBtn=document.getElementById('user-submit-btn');const originalText=submitBtn.innerHTML;try{submitBtn.innerHTML='
';submitBtn.disabled=true;const userData={email:formData.get('email'),username:formData.get('username'),name:formData.get('name'),is_admin:formData.get('role')==='admin',is_active:formData.get('is_active')==='on'};const password=formData.get('password');if(password&&password.trim()){userData.password=password;} +const response=await fetch(`${this.apiBaseUrl}/api/admin/users/${userId}`,{method:'PUT',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken},body:JSON.stringify(userData)});const data=await response.json();if(data.success){this.showNotification('✅ Benutzer erfolgreich aktualisiert!','success');document.getElementById('user-modal').remove();setTimeout(()=>{window.location.reload();},1000);}else{this.showNotification(`❌ Fehler:${data.error}`,'error');}}catch(error){console.error('Fehler beim Aktualisieren des Benutzers:',error);this.showNotification('❌ Fehler beim Aktualisieren des Benutzers','error');}finally{submitBtn.innerHTML=originalText;submitBtn.disabled=false;}} +editUser(userId){console.log(`✏️ Benutzer ${userId}wird bearbeitet`);this.showUserModal(userId);} +async deleteUser(userId,userName){if(!confirm(`🗑️ Möchten Sie den Benutzer"${userName}"wirklich löschen?\n\nDiese Aktion kann nicht rückgängig gemacht werden!`)){return;} +try{this.showNotification(`🔄 Benutzer"${userName}"wird gelöscht...`,'info');const response=await fetch(`${this.apiBaseUrl}/api/admin/users/${userId}`,{method:'DELETE',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken}});const data=await response.json();if(data.success){this.showNotification(`✅ Benutzer"${userName}"erfolgreich gelöscht!`,'success');setTimeout(()=>{window.location.reload();},1000);}else{this.showNotification(`❌ Fehler beim Löschen:${data.error}`,'error');}}catch(error){console.error('Fehler beim Löschen des Benutzers:',error);this.showNotification('❌ Fehler beim Löschen des Benutzers','error');}} +showPrinterModal(){console.log('🖨️ Drucker-Modal wird angezeigt');this.showNotification('Drucker-Funktionen werden geladen...','info');} +managePrinter(printerId){console.log(`🔧 Drucker ${printerId}wird verwaltet`);this.showNotification(`Drucker ${printerId}wird verwaltet...`,'info');} +showPrinterSettings(printerId){console.log(`⚙️ Drucker-Einstellungen ${printerId}werden angezeigt`);this.showNotification(`Drucker-Einstellungen werden geladen...`,'info');} +async togglePrinterPower(printerId,printerName,button){console.log(`🔌 Smart-Plug Toggle für Drucker ${printerId}(${printerName})`);const confirmMessage=`Möchten Sie die Steckdose für"${printerName}"umschalten?\n\nDies schaltet den Drucker ein/aus.`;if(!confirm(confirmMessage))return;const originalContent=button.innerHTML;button.disabled=true;button.innerHTML=`
`;try{const response=await fetch(`${this.apiBaseUrl}/api/admin/printers/${printerId}/toggle`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken},body:JSON.stringify({reason:'Admin-Panel Smart-Plug Toggle'})});const data=await response.json();if(response.ok&&data.success){const action=data.action||'umgeschaltet';this.showNotification(`✅ Steckdose für"${printerName}"erfolgreich ${action}`,'success');button.classList.remove('from-orange-500','to-red-500');button.classList.add('from-green-500','to-green-600');setTimeout(()=>{button.classList.remove('from-green-500','to-green-600');button.classList.add('from-orange-500','to-red-500');},2000);setTimeout(()=>{this.loadLiveStats();},1000);}else{const errorMsg=data.error||'Unbekannter Fehler beim Schalten der Steckdose';this.showNotification(`❌ Fehler:${errorMsg}`,'error');console.error('Smart-Plug Toggle Fehler:',data);}}catch(error){console.error('Netzwerkfehler beim Smart-Plug Toggle:',error);this.showNotification(`❌ Netzwerkfehler beim Schalten der Steckdose für"${printerName}"`,'error');}finally{button.disabled=false;button.innerHTML=originalContent;}} +handleJobAction(action,jobId){console.log(`📋 Job-Aktion"${action}"für Job ${jobId}`);this.showNotification(`Job-Aktion"${action}"wird ausgeführt...`,'info');} +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);}}catch(error){console.error('Fehler bei System-Health-Check:',error);}} +updateHealthDisplay(data){const statusIndicator=document.getElementById('db-status-indicator');const statusText=document.getElementById('db-status-text');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';}} +this.updateElement('last-migration',data.last_migration||'Unbekannt');this.updateElement('schema-integrity',data.schema_integrity||'Prüfung');this.updateElement('recent-errors-count',data.recent_errors_count||0);} +updateErrorAlerts(data){const alertContainer=document.getElementById('critical-errors-alert');if(!alertContainer)return;const allErrors=[...(data.critical_errors||[]),...(data.warnings||[])];if(allErrors.length>0){alertContainer.classList.remove('hidden');}else{alertContainer.classList.add('hidden');}} +async fixErrors(){if(!confirm('🔧 Möchten Sie die automatische Fehlerkorrektur durchführen?'))return;this.showNotification('🔄 Fehler werden automatisch behoben...','info');if(!this.csrfToken){console.error('❌ CSRF Token fehlt! Versuche Token neu zu laden...');this.csrfToken=this.extractCSRFToken();if(!this.csrfToken){this.showNotification('❌ Sicherheitsfehler: CSRF Token nicht verfügbar','error');return;}} +console.log('🔧 Starte automatische Fehlerkorrektur...');console.log('🔒 CSRF Token für Request:',this.csrfToken.substring(0,10)+'...');try{const requestOptions={method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken}};console.log('📡 Sende Request an:','/api/admin/fix-errors');console.log('📝 Request Headers:',requestOptions.headers);const response=await fetch('/api/admin/fix-errors',requestOptions);console.log('📡 Response Status:',response.status);console.log('📡 Response Headers:',Object.fromEntries(response.headers.entries()));if(!response.ok){const errorText=await response.text();console.error('❌ Response Error:',errorText);throw new Error(`HTTP ${response.status}:${response.statusText}-${errorText}`);} +const data=await response.json();console.log('✅ Response Data:',data);if(data.success){this.showNotification('✅ Automatische Reparatur erfolgreich!','success');setTimeout(()=>this.checkSystemHealth(),2000);}else{this.showNotification(`❌ Automatische Reparatur fehlgeschlagen:${data.message||'Unbekannter Fehler'}`,'error');}}catch(error){console.error('❌ Fehler bei automatischer Reparatur:',error);this.showNotification(`❌ Fehler bei der automatischen Reparatur:${error.message}`,'error');}} +dismissErrors(){const alertContainer=document.getElementById('critical-errors-alert');if(alertContainer){alertContainer.classList.add('hidden');}} +showNotification(message,type='info'){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';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 transition-all duration-300 ${colors[type]}`;notification.textContent=message;notification.style.transform='translateX(0)';setTimeout(()=>{if(notification){notification.style.transform='translateX(100%)';setTimeout(()=>{if(notification&¬ification.parentNode){notification.parentNode.removeChild(notification);}},300);}},3000);} +async loadLogs(level=null){const logsContainer=document.getElementById('logs-container');if(!logsContainer)return;logsContainer.innerHTML=`
Logs werden geladen...
`;try{const filter=level||document.getElementById('log-level-filter')?.value||'all';const url=`${this.apiBaseUrl}/api/admin/logs?level=${filter}&limit=100`;const response=await fetch(url,{headers:{'X-CSRFToken':this.csrfToken}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const data=await response.json();this.displayLogs(data.logs||[]);console.log('📋 Logs erfolgreich geladen');}catch(error){console.error('Fehler beim Laden der Logs:',error);logsContainer.innerHTML=`

Fehler beim Laden der Logs

${error.message}

`;}} +displayLogs(logs){const logsContainer=document.getElementById('logs-container');if(!logsContainer)return;if(!logs||logs.length===0){logsContainer.innerHTML=`

Keine Logs gefunden

Es sind keine Logs für die ausgewählten Kriterien vorhanden.

`;return;} +const logsHtml=logs.map(log=>{const levelColors={'error':'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-200','warning':'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-200','info':'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200','debug':'bg-gray-50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800 text-gray-800 dark:text-gray-200','critical':'bg-red-100 dark:bg-red-900/40 border-red-300 dark:border-red-700 text-red-900 dark:text-red-100'};const levelIcons={'error':'❌','warning':'⚠️','info':'ℹ️','debug':'🔍','critical':'🚨'};const levelClass=levelColors[log.level]||levelColors['info'];const levelIcon=levelIcons[log.level]||'ℹ️';return`
${levelIcon}${log.level}${log.component||'System'}
${this.formatLogTimestamp(log.timestamp)}

${this.escapeHtml(log.message)}

${log.details?`

${this.escapeHtml(log.details)}

`:''}
${log.user?`
Benutzer:${this.escapeHtml(log.user)}
`:''} +${log.ip_address?`
IP:${this.escapeHtml(log.ip_address)}
`:''} +${log.request_id?`
Request-ID:${this.escapeHtml(log.request_id)}
`:''}
`;}).join('');logsContainer.innerHTML=logsHtml;} +formatLogTimestamp(timestamp){if(!timestamp)return'Unbekannt';try{const date=new Date(timestamp);return date.toLocaleString('de-DE',{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'});}catch(error){return timestamp;}} +escapeHtml(text){if(!text)return'';const div=document.createElement('div');div.textContent=text;return div.innerHTML;} +async exportLogs(){try{this.showNotification('📥 Logs werden exportiert...','info');const filter=document.getElementById('log-level-filter')?.value||'all';const url=`${this.apiBaseUrl}/api/admin/logs/export?level=${filter}`;const response=await fetch(url,{headers:{'X-CSRFToken':this.csrfToken}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const blob=await response.blob();const downloadUrl=window.URL.createObjectURL(blob);const a=document.createElement('a');a.href=downloadUrl;a.download=`system-logs-${new Date().toISOString().split('T')[0]}.csv`;document.body.appendChild(a);a.click();document.body.removeChild(a);window.URL.revokeObjectURL(downloadUrl);this.showNotification('✅ Logs erfolgreich exportiert!','success');}catch(error){console.error('Fehler beim Exportieren der Logs:',error);this.showNotification('❌ Fehler beim Exportieren der Logs: '+error.message,'error');}} +testButtons(){console.log('🧪 Teste Button-Funktionalität...');const fixBtn=document.querySelector('#fix-errors-btn');if(fixBtn){console.log('✅ Fix-Errors Button gefunden:',fixBtn);console.log('🔗 Event-Listener-Status:',fixBtn.dataset.listenerAttached);fixBtn.addEventListener('click',(e)=>{console.log('🖱️ Fix-Errors Button wurde geklickt (manueller Listener)');e.preventDefault();e.stopPropagation();this.testFixErrors();});}else{console.error('❌ Fix-Errors Button NICHT gefunden!');} +const viewBtn=document.querySelector('#view-error-details-btn');if(viewBtn){console.log('✅ View-Details Button gefunden:',viewBtn);console.log('🔗 Event-Listener-Status:',viewBtn.dataset.listenerAttached);viewBtn.addEventListener('click',(e)=>{console.log('🖱️ View-Details Button wurde geklickt (manueller Listener)');e.preventDefault();e.stopPropagation();console.log('🔄 Weiterleitung zu Logs-Tab...');window.location.href='/admin-dashboard?tab=logs';});}else{console.error('❌ View-Details Button NICHT gefunden!');}} +async testFixErrors(){console.log('🧪 TEST: Fix-Errors wird ausgeführt...');console.log('🔒 Aktueller CSRF Token:',this.csrfToken);if(!this.csrfToken){console.error('❌ CSRF Token fehlt - versuche neu zu laden...');this.csrfToken=this.extractCSRFToken();console.log('🔒 Neu geladener Token:',this.csrfToken);} +this.showNotification('🧪 TEST: Starte automatische Fehlerkorrektur...','info');try{const requestOptions={method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.csrfToken,'X-Requested-With':'XMLHttpRequest'}};console.log('📡 TEST Request an:','/api/admin/fix-errors');console.log('📝 TEST Headers:',requestOptions.headers);const response=await fetch('/api/admin/fix-errors',requestOptions);console.log('📡 TEST Response Status:',response.status);console.log('📡 TEST Response Headers:',Object.fromEntries(response.headers.entries()));if(!response.ok){const errorText=await response.text();console.error('❌ TEST Response Error:',errorText);this.showNotification(`❌ TEST Fehler:${response.status}-${errorText}`,'error');return;} +const data=await response.json();console.log('✅ TEST Response Data:',data);if(data.success){this.showNotification('✅ TEST: Automatische Reparatur erfolgreich!','success');}else{this.showNotification(`❌ TEST:Reparatur fehlgeschlagen-${data.message}`,'error');}}catch(error){console.error('❌ TEST Fehler:',error);this.showNotification(`❌ TEST Netzwerk-Fehler:${error.message}`,'error');}}} +let adminDashboardInstance=null;document.addEventListener('DOMContentLoaded',function(){if(!adminDashboardInstance){adminDashboardInstance=new AdminDashboard();window.AdminDashboard=adminDashboardInstance;console.log('🎯 Admin Dashboard erfolgreich initialisiert (unified)');}});window.AdminDashboard=AdminDashboard; \ No newline at end of file diff --git a/backend/static/js/admin-unified.min.js.gz b/backend/static/js/admin-unified.min.js.gz new file mode 100644 index 00000000..606d48fe Binary files /dev/null and b/backend/static/js/admin-unified.min.js.gz differ diff --git a/backend/static/js/advanced-components.js.gz b/backend/static/js/advanced-components.js.gz new file mode 100644 index 00000000..2f1fbe0b Binary files /dev/null and b/backend/static/js/advanced-components.js.gz differ diff --git a/backend/static/js/advanced-components.min.js b/backend/static/js/advanced-components.min.js new file mode 100644 index 00000000..1c496a94 --- /dev/null +++ b/backend/static/js/advanced-components.min.js @@ -0,0 +1,79 @@ +(function(){'use strict';window.MYP=window.MYP||{};window.MYP.Advanced=window.MYP.Advanced||{};class ProgressBar{constructor(container,options={}){this.container=typeof container==='string'?document.querySelector(container):container;this.options={value:0,max:100,showLabel:true,showPercentage:true,animated:true,color:'blue',size:'md',striped:false,...options};this.currentValue=this.options.value;this.init();} +init(){if(!this.container){console.error('ProgressBar: Container nicht gefunden');return;} +this.render();} +render(){const percentage=Math.round((this.currentValue/this.options.max)*100);const sizeClass=this.getSizeClass();const colorClass=this.getColorClass();this.container.innerHTML=`
${this.options.showLabel?`
${this.options.label||'Fortschritt'}${this.options.showPercentage?`${percentage}%`:''}
`:''}
`;} +getSizeClass(){const sizes={'sm':'h-2','md':'h-3','lg':'h-4','xl':'h-6'};return sizes[this.options.size]||sizes.md;} +getColorClass(){const colors={'blue':'bg-blue-500','green':'bg-green-500','red':'bg-red-500','yellow':'bg-yellow-500','purple':'bg-purple-500','indigo':'bg-indigo-500'};return colors[this.options.color]||colors.blue;} +setValue(value,animate=true){const oldValue=this.currentValue;this.currentValue=Math.max(0,Math.min(this.options.max,value));if(animate){this.animateToValue(oldValue,this.currentValue);}else{this.render();}} +animateToValue(from,to){const duration=500;const steps=60;const stepValue=(to-from)/steps;let currentStep=0;const animate=()=>{if(currentStep

${this.getFileTypeText()}• Max.${this.formatFileSize(this.options.maxSize)}

`;} +setupEventListeners(){const fileInput=this.container.querySelector('#fileInput');const dropzone=this.container.querySelector('#dropzone');fileInput.addEventListener('change',(e)=>{this.handleFiles(Array.from(e.target.files));});if(this.options.dragDrop){dropzone.addEventListener('dragover',(e)=>{e.preventDefault();dropzone.classList.add('drag-over');});dropzone.addEventListener('dragleave',(e)=>{e.preventDefault();dropzone.classList.remove('drag-over');});dropzone.addEventListener('drop',(e)=>{e.preventDefault();dropzone.classList.remove('drag-over');this.handleFiles(Array.from(e.dataTransfer.files));});}} +handleFiles(fileList){for(const file of fileList){if(this.validateFile(file)){this.addFile(file);}} +this.renderFileList();} +validateFile(file){if(file.size>this.options.maxSize){this.showError(`Datei"${file.name}"ist zu groß.Maximum:${this.formatFileSize(this.options.maxSize)}`);return false;} +if(!this.options.multiple&&this.files.length>0){this.files=[];}else if(this.files.length>=this.options.maxFiles){this.showError(`Maximal ${this.options.maxFiles}Dateien erlaubt`);return false;} +return true;} +addFile(file){const fileData={id:this.generateId(),file:file,name:file.name,size:file.size,type:file.type,status:'pending',progress:0,error:null};this.files.push(fileData);if(this.options.showPreview&&file.type.startsWith('image/')){this.generatePreview(fileData);}} +generatePreview(fileData){const reader=new FileReader();reader.onload=(e)=>{fileData.preview=e.target.result;this.renderFileList();};reader.readAsDataURL(fileData.file);} +renderFileList(){const fileListContainer=this.container.querySelector('#fileList');if(this.files.length===0){fileListContainer.innerHTML='';return;} +fileListContainer.innerHTML=this.files.map(fileData=>`
${fileData.preview?`Preview`:`
`}

${fileData.name}

${this.formatFileSize(fileData.size)}• ${this.getStatusText(fileData.status)}

${this.options.showProgress&&fileData.status==='uploading'?`
`:''} +${fileData.error?`

${fileData.error}

`:''}
`).join('');fileListContainer.querySelectorAll('.remove-file').forEach(button=>{button.addEventListener('click',(e)=>{const fileId=e.target.closest('.remove-file').dataset.fileId;this.removeFile(fileId);});});} +removeFile(fileId){this.files=this.files.filter(f=>f.id!==fileId);this.renderFileList();} +async uploadFiles(){const pendingFiles=this.files.filter(f=>f.status==='pending');for(const fileData of pendingFiles){await this.uploadFile(fileData);}} +async uploadFile(fileData){fileData.status='uploading';fileData.progress=0;this.renderFileList();try{const formData=new FormData();formData.append('file',fileData.file);const response=await fetch(this.options.uploadUrl,{method:'POST',body:formData,headers:{'X-CSRFToken':document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')||''}});if(response.ok){fileData.status='completed';fileData.progress=100;const result=await response.json();fileData.url=result.url;}else{throw new Error(`Upload fehlgeschlagen:${response.status}`);}}catch(error){fileData.status='error';fileData.error=error.message;} +this.renderFileList();} +getFileTypeText(){if(this.options.accept==='*/*')return'Alle Dateitypen';if(this.options.accept.includes('image/'))return'Bilder';if(this.options.accept.includes('.pdf'))return'PDF-Dateien';return'Spezifische Dateitypen';} +getStatusText(status){const statusTexts={'pending':'Wartend','uploading':'Wird hochgeladen...','completed':'Abgeschlossen','error':'Fehler'};return statusTexts[status]||status;} +formatFileSize(bytes){if(bytes===0)return'0 Bytes';const k=1024;const sizes=['Bytes','KB','MB','GB'];const i=Math.floor(Math.log(bytes)/Math.log(k));return parseFloat((bytes/Math.pow(k,i)).toFixed(2))+' '+sizes[i];} +generateId(){return'file_'+Math.random().toString(36).substr(2,9);} +showError(message){if(window.showToast){window.showToast(message,'error');}else{alert(message);}} +getFiles(){return this.files;} +getCompletedFiles(){return this.files.filter(f=>f.status==='completed');} +clear(){this.files=[];this.renderFileList();}} +class DatePicker{constructor(input,options={}){this.input=typeof input==='string'?document.querySelector(input):input;this.options={format:'dd.mm.yyyy',minDate:null,maxDate:null,disabledDates:[],language:'de',closeOnSelect:true,showWeekNumbers:false,...options};this.isOpen=false;this.currentDate=new Date();this.selectedDate=null;this.init();} +init(){if(!this.input){console.error('DatePicker: Input-Element nicht gefunden');return;} +this.setupInput();this.createCalendar();this.setupEventListeners();} +setupInput(){this.input.setAttribute('readonly','true');this.input.classList.add('datepicker-input');this.container=document.createElement('div');this.container.className='datepicker-container relative';this.input.parentNode.insertBefore(this.container,this.input);this.container.appendChild(this.input);} +createCalendar(){this.calendar=document.createElement('div');this.calendar.className='datepicker-calendar absolute top-full left-0 mt-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-lg shadow-lg z-50 hidden';this.calendar.innerHTML=this.renderCalendar();this.container.appendChild(this.calendar);} +renderCalendar(){const year=this.currentDate.getFullYear();const month=this.currentDate.getMonth();const monthName=this.getMonthName(month);return`
${monthName}${year}
${this.getWeekdayHeaders()}
${this.getDaysOfMonth(year,month)}
`;} +getWeekdayHeaders(){const weekdays=['Mo','Di','Mi','Do','Fr','Sa','So'];return weekdays.map(day=>`
${day}
`).join('');} +getDaysOfMonth(year,month){const firstDay=new Date(year,month,1);const lastDay=new Date(year,month+1,0);const startDate=new Date(firstDay);startDate.setDate(startDate.getDate()-((firstDay.getDay()+6)%7));const days=[];const current=new Date(startDate);while(current<=lastDay||current.getMonth()===month){const isCurrentMonth=current.getMonth()===month;const isToday=this.isToday(current);const isSelected=this.isSelectedDate(current);const isDisabled=this.isDisabledDate(current);const classes=['w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center transition-colors',isCurrentMonth?'text-slate-900 dark:text-white':'text-slate-400 dark:text-slate-600',isToday?'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100':'',isSelected?'bg-blue-500 text-white':'',!isDisabled&&isCurrentMonth?'hover:bg-slate-100 dark:hover:bg-slate-700':'',isDisabled?'cursor-not-allowed opacity-50':''].filter(Boolean);days.push(`
${current.getDate()}
`);current.setDate(current.getDate()+1);if(days.length>=42)break;} +return days.join('');} +setupEventListeners(){this.input.addEventListener('click',()=>{this.toggle();});this.calendar.addEventListener('click',(e)=>{if(e.target.classList.contains('prev-month')){this.previousMonth();}else if(e.target.classList.contains('next-month')){this.nextMonth();}else if(e.target.dataset.selectable){this.selectDate(new Date(e.target.dataset.date));}});document.addEventListener('click',(e)=>{if(!this.container.contains(e.target)){this.close();}});} +toggle(){if(this.isOpen){this.close();}else{this.open();}} +open(){this.calendar.classList.remove('hidden');this.isOpen=true;this.updateCalendar();} +close(){this.calendar.classList.add('hidden');this.isOpen=false;} +selectDate(date){this.selectedDate=new Date(date);this.input.value=this.formatDate(date);this.input.dispatchEvent(new CustomEvent('dateselected',{detail:{date:new Date(date)}}));if(this.options.closeOnSelect){this.close();}} +previousMonth(){this.currentDate.setMonth(this.currentDate.getMonth()-1);this.updateCalendar();} +nextMonth(){this.currentDate.setMonth(this.currentDate.getMonth()+1);this.updateCalendar();} +updateCalendar(){this.calendar.innerHTML=this.renderCalendar();} +isToday(date){const today=new Date();return date.toDateString()===today.toDateString();} +isSelectedDate(date){return this.selectedDate&&date.toDateString()===this.selectedDate.toDateString();} +isDisabledDate(date){if(this.options.minDate&&datethis.options.maxDate)return true;return this.options.disabledDates.some(disabled=>date.toDateString()===disabled.toDateString());} +formatDate(date){const day=date.getDate().toString().padStart(2,'0');const month=(date.getMonth()+1).toString().padStart(2,'0');const year=date.getFullYear();return this.options.format.replace('dd',day).replace('mm',month).replace('yyyy',year);} +formatDateForData(date){return date.toISOString().split('T')[0];} +getMonthName(monthIndex){const months=['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];return months[monthIndex];} +setValue(date){if(date){this.selectDate(new Date(date));}} +getValue(){return this.selectedDate;} +clear(){this.selectedDate=null;this.input.value='';}} +window.MYP.Advanced={ProgressBar,FileUpload,DatePicker,createProgressBar:(container,options)=>new ProgressBar(container,options),createFileUpload:(container,options)=>new FileUpload(container,options),createDatePicker:(input,options)=>new DatePicker(input,options)};document.addEventListener('DOMContentLoaded',function(){document.querySelectorAll('[data-datepicker]').forEach(input=>{const options=JSON.parse(input.dataset.datepicker||'{}');new DatePicker(input,options);});document.querySelectorAll('[data-file-upload]').forEach(container=>{const options=JSON.parse(container.dataset.fileUpload||'{}');new FileUpload(container,options);});console.log('🚀 MYP Advanced Components geladen');});})(); \ No newline at end of file diff --git a/backend/static/js/advanced-components.min.js.gz b/backend/static/js/advanced-components.min.js.gz new file mode 100644 index 00000000..7ad8bc58 Binary files /dev/null and b/backend/static/js/advanced-components.min.js.gz differ diff --git a/backend/static/js/auto-logout.js.gz b/backend/static/js/auto-logout.js.gz new file mode 100644 index 00000000..d496070a Binary files /dev/null and b/backend/static/js/auto-logout.js.gz differ diff --git a/backend/static/js/auto-logout.min.js b/backend/static/js/auto-logout.min.js new file mode 100644 index 00000000..52ab07bd --- /dev/null +++ b/backend/static/js/auto-logout.min.js @@ -0,0 +1,14 @@ +class AutoLogoutManager{constructor(){this.timer=null;this.warningTimer=null;this.timeout=60;this.warningTime=5;this.isWarningShown=false;this.init();} +async init(){await this.loadSettings();this.setupActivityListeners();this.startTimer();} +async loadSettings(){try{const response=await fetch('/api/user/settings');if(response.ok){const data=await response.json();if(data.success&&data.settings.privacy?.auto_logout){const timeout=parseInt(data.settings.privacy.auto_logout);if(timeout>0&&timeout!=='never'){this.timeout=timeout;}else{this.timeout=0;}}}}catch(error){console.warn('Auto-Logout-Einstellungen konnten nicht geladen werden:',error);}} +setupActivityListeners(){const events=['mousedown','mousemove','keypress','scroll','touchstart','click'];events.forEach(event=>{document.addEventListener(event,()=>this.resetTimer(),{passive:true});});} +startTimer(){if(this.timeout<=0)return;this.clearTimers();const timeoutMs=this.timeout*60*1000;const warningMs=this.warningTime*60*1000;this.warningTimer=setTimeout(()=>this.showWarning(),timeoutMs-warningMs);this.timer=setTimeout(()=>this.performLogout(),timeoutMs);} +resetTimer(){if(this.isWarningShown){this.closeWarning();} +this.startTimer();} +clearTimers(){if(this.timer)clearTimeout(this.timer);if(this.warningTimer)clearTimeout(this.warningTimer);} +showWarning(){if(this.isWarningShown)return;this.isWarningShown=true;const modal=document.createElement('div');modal.id='auto-logout-warning';modal.className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';modal.innerHTML=`

Automatische Abmeldung

Sie werden in ${this.warningTime}Minuten aufgrund von Inaktivität abgemeldet.

`;document.body.appendChild(modal);document.getElementById('stay-logged-in').onclick=()=>{this.closeWarning();this.sendKeepAlive();this.resetTimer();};document.getElementById('logout-now').onclick=()=>{this.performLogout();};} +closeWarning(){const modal=document.getElementById('auto-logout-warning');if(modal)modal.remove();this.isWarningShown=false;} +async sendKeepAlive(){try{await fetch('/api/auth/keep-alive',{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()}});}catch(error){console.warn('Keep-Alive fehlgeschlagen:',error);}} +getCSRFToken(){const metaTag=document.querySelector('meta[name="csrf-token"]');return metaTag?metaTag.getAttribute('content'):'';} +async performLogout(){this.closeWarning();this.clearTimers();window.location.href='/auth/logout';}} +document.addEventListener('DOMContentLoaded',function(){if(!window.location.pathname.includes('/login')){window.autoLogoutManager=new AutoLogoutManager();}}); \ No newline at end of file diff --git a/backend/static/js/auto-logout.min.js.gz b/backend/static/js/auto-logout.min.js.gz new file mode 100644 index 00000000..58c3f77a Binary files /dev/null and b/backend/static/js/auto-logout.min.js.gz differ diff --git a/backend/static/js/charts.js.gz b/backend/static/js/charts.js.gz new file mode 100644 index 00000000..f72e0c78 Binary files /dev/null and b/backend/static/js/charts.js.gz differ diff --git a/backend/static/js/charts.min.js b/backend/static/js/charts.min.js new file mode 100644 index 00000000..09a85227 --- /dev/null +++ b/backend/static/js/charts.min.js @@ -0,0 +1,25 @@ +window.statsCharts={};function getChartTheme(){const isDark=document.documentElement.classList.contains('dark');return{isDark:isDark,backgroundColor:isDark?'rgba(30, 41, 59, 0.8)':'rgba(255, 255, 255, 0.8)',textColor:isDark?'#e2e8f0':'#374151',gridColor:isDark?'rgba(148, 163, 184, 0.1)':'rgba(156, 163, 175, 0.2)',borderColor:isDark?'rgba(148, 163, 184, 0.3)':'rgba(156, 163, 175, 0.5)'};} +function getDefaultChartOptions(){const theme=getChartTheme();return{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:theme.textColor,font:{family:'Inter, sans-serif'}}},tooltip:{backgroundColor:theme.backgroundColor,titleColor:theme.textColor,bodyColor:theme.textColor,borderColor:theme.borderColor,borderWidth:1}},scales:{x:{ticks:{color:theme.textColor,font:{family:'Inter, sans-serif'}},grid:{color:theme.gridColor}},y:{ticks:{color:theme.textColor,font:{family:'Inter, sans-serif'}},grid:{color:theme.gridColor}}}};} +async function createJobStatusChart(){try{const response=await fetch('/api/stats/charts/job-status');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Job-Status-Daten');} +const ctx=document.getElementById('job-status-chart');if(!ctx)return;if(window.statsCharts.jobStatus){window.statsCharts.jobStatus.destroy();} +const theme=getChartTheme();window.statsCharts.jobStatus=new Chart(ctx,{type:'doughnut',data:data,options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{color:theme.textColor,font:{family:'Inter, sans-serif',size:12},padding:15}},tooltip:{backgroundColor:theme.backgroundColor,titleColor:theme.textColor,bodyColor:theme.textColor,borderColor:theme.borderColor,borderWidth:1,callbacks:{label:function(context){const label=context.label||'';const value=context.parsed;const total=context.dataset.data.reduce((a,b)=>a+b,0);const percentage=total>0?Math.round((value/total)*100):0;return`${label}:${value}(${percentage}%)`;}}}},cutout:'60%'}});}catch(error){console.error('Fehler beim Erstellen des Job-Status-Charts:',error);showChartError('job-status-chart','Fehler beim Laden der Job-Status-Daten');}} +async function createPrinterUsageChart(){try{const response=await fetch('/api/stats/charts/printer-usage');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Drucker-Nutzung-Daten');} +const ctx=document.getElementById('printer-usage-chart');if(!ctx)return;if(window.statsCharts.printerUsage){window.statsCharts.printerUsage.destroy();} +const options=getDefaultChartOptions();options.scales.y.title={display:true,text:'Anzahl Jobs',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};window.statsCharts.printerUsage=new Chart(ctx,{type:'bar',data:data,options:options});}catch(error){console.error('Fehler beim Erstellen des Drucker-Nutzung-Charts:',error);showChartError('printer-usage-chart','Fehler beim Laden der Drucker-Nutzung-Daten');}} +async function createJobsTimelineChart(){try{const response=await fetch('/api/stats/charts/jobs-timeline');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Jobs-Timeline-Daten');} +const ctx=document.getElementById('jobs-timeline-chart');if(!ctx)return;if(window.statsCharts.jobsTimeline){window.statsCharts.jobsTimeline.destroy();} +const options=getDefaultChartOptions();options.scales.y.title={display:true,text:'Jobs pro Tag',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};options.scales.x.title={display:true,text:'Datum (letzte 30 Tage)',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};window.statsCharts.jobsTimeline=new Chart(ctx,{type:'line',data:data,options:options});}catch(error){console.error('Fehler beim Erstellen des Jobs-Timeline-Charts:',error);showChartError('jobs-timeline-chart','Fehler beim Laden der Jobs-Timeline-Daten');}} +async function createUserActivityChart(){try{const response=await fetch('/api/stats/charts/user-activity');const data=await response.json();if(!response.ok){throw new Error(data.error||'Fehler beim Laden der Benutzer-Aktivität-Daten');} +const ctx=document.getElementById('user-activity-chart');if(!ctx)return;if(window.statsCharts.userActivity){window.statsCharts.userActivity.destroy();} +const options=getDefaultChartOptions();options.indexAxis='y';options.scales.x.title={display:true,text:'Anzahl Jobs',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};options.scales.y.title={display:true,text:'Benutzer',color:getChartTheme().textColor,font:{family:'Inter, sans-serif'}};window.statsCharts.userActivity=new Chart(ctx,{type:'bar',data:data,options:options});}catch(error){console.error('Fehler beim Erstellen des Benutzer-Aktivität-Charts:',error);showChartError('user-activity-chart','Fehler beim Laden der Benutzer-Aktivität-Daten');}} +function showChartError(chartId,message){const container=document.getElementById(chartId);if(container){container.innerHTML=`

${message}

`;}} +async function initializeAllCharts(){showChartLoading();await Promise.allSettled([createJobStatusChart(),createPrinterUsageChart(),createJobsTimelineChart(),createUserActivityChart()]);} +function showChartLoading(){const chartIds=['job-status-chart','printer-usage-chart','jobs-timeline-chart','user-activity-chart'];chartIds.forEach(chartId=>{const container=document.getElementById(chartId);if(container){container.innerHTML=`

Diagramm wird geladen...

`;}});} +async function refreshAllCharts(){console.log('Aktualisiere alle Diagramme...');Object.values(window.statsCharts).forEach(chart=>{if(chart&&typeof chart.destroy==='function'){chart.destroy();}});await initializeAllCharts();console.log('Alle Diagramme aktualisiert');} +function updateChartsTheme(){refreshAllCharts();} +let chartRefreshInterval;function startChartAutoRefresh(){if(chartRefreshInterval){clearInterval(chartRefreshInterval);} +chartRefreshInterval=setInterval(()=>{refreshAllCharts();},5*60*1000);} +function stopChartAutoRefresh(){if(chartRefreshInterval){clearInterval(chartRefreshInterval);chartRefreshInterval=null;}} +function cleanup(){stopChartAutoRefresh();Object.values(window.statsCharts).forEach(chart=>{if(chart&&typeof chart.destroy==='function'){chart.destroy();}});window.statsCharts={};} +window.refreshAllCharts=refreshAllCharts;window.updateChartsTheme=updateChartsTheme;window.startChartAutoRefresh=startChartAutoRefresh;window.stopChartAutoRefresh=stopChartAutoRefresh;window.cleanup=cleanup;document.addEventListener('DOMContentLoaded',function(){if(document.getElementById('job-status-chart')){initializeAllCharts();startChartAutoRefresh();}});if(typeof window.addEventListener!=='undefined'){window.addEventListener('darkModeChanged',function(e){updateChartsTheme();});} +window.addEventListener('beforeunload',cleanup); \ No newline at end of file diff --git a/backend/static/js/charts.min.js.gz b/backend/static/js/charts.min.js.gz new file mode 100644 index 00000000..4cb85502 Binary files /dev/null and b/backend/static/js/charts.min.js.gz differ diff --git a/backend/static/js/charts/apexcharts.min.js.gz b/backend/static/js/charts/apexcharts.min.js.gz new file mode 100644 index 00000000..b9dcf463 Binary files /dev/null and b/backend/static/js/charts/apexcharts.min.js.gz differ diff --git a/backend/static/js/charts/chart-adapter.js.gz b/backend/static/js/charts/chart-adapter.js.gz new file mode 100644 index 00000000..c9e251d0 Binary files /dev/null and b/backend/static/js/charts/chart-adapter.js.gz differ diff --git a/backend/static/js/charts/chart-config.js.gz b/backend/static/js/charts/chart-config.js.gz new file mode 100644 index 00000000..d678d351 Binary files /dev/null and b/backend/static/js/charts/chart-config.js.gz differ diff --git a/backend/static/js/charts/chart-renderer.js.gz b/backend/static/js/charts/chart-renderer.js.gz new file mode 100644 index 00000000..ad62e651 Binary files /dev/null and b/backend/static/js/charts/chart-renderer.js.gz differ diff --git a/backend/static/js/charts/chart.min.js.gz b/backend/static/js/charts/chart.min.js.gz new file mode 100644 index 00000000..13600c66 Binary files /dev/null and b/backend/static/js/charts/chart.min.js.gz differ diff --git a/backend/static/js/countdown-timer.js.gz b/backend/static/js/countdown-timer.js.gz new file mode 100644 index 00000000..e48ae858 Binary files /dev/null and b/backend/static/js/countdown-timer.js.gz differ diff --git a/backend/static/js/countdown-timer.min.js b/backend/static/js/countdown-timer.min.js new file mode 100644 index 00000000..ba4bc529 --- /dev/null +++ b/backend/static/js/countdown-timer.min.js @@ -0,0 +1,86 @@ +class CountdownTimer{constructor(options={}){this.config={name:options.name||'default_timer',duration:options.duration||1800,autoStart:options.autoStart||false,container:options.container||'countdown-timer',size:options.size||'large',theme:options.theme||'primary',showProgress:options.showProgress!==false,showControls:options.showControls!==false,warningThreshold:options.warningThreshold||30,showWarning:options.showWarning!==false,warningMessage:options.warningMessage||'Timer läuft ab!',forceQuitEnabled:options.forceQuitEnabled!==false,forceQuitAction:options.forceQuitAction||'logout',customEndpoint:options.customEndpoint||null,onTick:options.onTick||null,onWarning:options.onWarning||null,onExpired:options.onExpired||null,onForceQuit:options.onForceQuit||null,apiBase:options.apiBase||'/api/timers',updateInterval:options.updateInterval||1000,syncWithServer:options.syncWithServer!==false};this.state={remaining:this.config.duration,total:this.config.duration,status:'stopped',warningShown:false,lastServerSync:null};this.elements={};this.intervals={countdown:null,serverSync:null};this.listeners=new Map();this.init();} +init(){this.createUI();this.attachEventListeners();if(this.config.syncWithServer){this.syncWithServer();this.startServerSync();} +if(this.config.autoStart){this.start();} +console.log(`Timer'${this.config.name}'initialisiert`);} +createUI(){const container=document.getElementById(this.config.container);if(!container){console.error(`Container'${this.config.container}'nicht gefunden`);return;} +const timerWrapper=document.createElement('div');timerWrapper.className=`countdown-timer-wrapper size-${this.config.size}theme-${this.config.theme}`;timerWrapper.innerHTML=this.getTimerHTML();container.appendChild(timerWrapper);this.elements={wrapper:timerWrapper,display:timerWrapper.querySelector('.timer-display'),timeText:timerWrapper.querySelector('.time-text'),progressBar:timerWrapper.querySelector('.progress-fill'),progressText:timerWrapper.querySelector('.progress-text'),statusIndicator:timerWrapper.querySelector('.status-indicator'),warningBox:timerWrapper.querySelector('.warning-box'),warningText:timerWrapper.querySelector('.warning-text'),controls:timerWrapper.querySelector('.timer-controls'),startBtn:timerWrapper.querySelector('.btn-start'),pauseBtn:timerWrapper.querySelector('.btn-pause'),stopBtn:timerWrapper.querySelector('.btn-stop'),resetBtn:timerWrapper.querySelector('.btn-reset'),extendBtn:timerWrapper.querySelector('.btn-extend')};this.updateDisplay();} +getTimerHTML(){return`
${this.formatTime(this.state.remaining)}verbleibend
Gestoppt
${this.config.showProgress?`
0%abgelaufen
`:''}${this.config.showControls?`
`:''}
`;} +attachEventListeners(){if(this.elements.startBtn){this.elements.startBtn.addEventListener('click',()=>this.start());} +if(this.elements.pauseBtn){this.elements.pauseBtn.addEventListener('click',()=>this.pause());} +if(this.elements.stopBtn){this.elements.stopBtn.addEventListener('click',()=>this.stop());} +if(this.elements.resetBtn){this.elements.resetBtn.addEventListener('click',()=>this.reset());} +if(this.elements.extendBtn){this.elements.extendBtn.addEventListener('click',()=>this.extend(300));} +document.addEventListener('keydown',(e)=>this.handleKeyboardShortcuts(e));document.addEventListener('visibilitychange',()=>this.handleVisibilityChange());window.addEventListener('beforeunload',(e)=>this.handleBeforeUnload(e));} +async start(){try{if(this.state.status==='running'){return true;} +if(this.config.syncWithServer){const response=await this.apiCall('start','POST');if(!response.success){this.showError('Fehler beim Starten des Timers');return false;}} +this.state.status='running';this.startCountdown();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'gestartet`);return true;}catch(error){console.error('Fehler beim Starten des Timers:',error);this.showError('Timer konnte nicht gestartet werden');return false;}} +async pause(){try{if(this.state.status!=='running'){return true;} +if(this.config.syncWithServer){const response=await this.apiCall('pause','POST');if(!response.success){this.showError('Fehler beim Pausieren des Timers');return false;}} +this.state.status='paused';this.stopCountdown();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'pausiert`);return true;}catch(error){console.error('Fehler beim Pausieren des Timers:',error);this.showError('Timer konnte nicht pausiert werden');return false;}} +async stop(){try{if(this.config.syncWithServer){const response=await this.apiCall('stop','POST');if(!response.success){this.showError('Fehler beim Stoppen des Timers');return false;}} +this.state.status='stopped';this.state.remaining=this.state.total;this.state.warningShown=false;this.stopCountdown();this.hideWarning();this.updateDisplay();this.updateControls();this.updateStatusIndicator();console.log(`Timer'${this.config.name}'gestoppt`);return true;}catch(error){console.error('Fehler beim Stoppen des Timers:',error);this.showError('Timer konnte nicht gestoppt werden');return false;}} +async reset(){try{if(this.config.syncWithServer){const response=await this.apiCall('reset','POST');if(!response.success){this.showError('Fehler beim Zurücksetzen des Timers');return false;}} +this.stop();this.state.remaining=this.state.total;this.updateDisplay();console.log(`Timer'${this.config.name}'zurückgesetzt`);return true;}catch(error){console.error('Fehler beim Zurücksetzen des Timers:',error);this.showError('Timer konnte nicht zurückgesetzt werden');return false;}} +async extend(seconds){try{if(this.config.syncWithServer){const response=await this.apiCall('extend','POST',{seconds});if(!response.success){this.showError('Fehler beim Verlängern des Timers');return false;}} +this.state.remaining+=seconds;this.state.total+=seconds;this.state.warningShown=false;this.hideWarning();this.updateDisplay();this.showToast(`Timer um ${Math.floor(seconds/60)}Minuten verlängert`,'success');console.log(`Timer'${this.config.name}'um ${seconds}Sekunden verlängert`);return true;}catch(error){console.error('Fehler beim Verlängern des Timers:',error);this.showError('Timer konnte nicht verlängert werden');return false;}} +startCountdown(){this.stopCountdown();this.intervals.countdown=setInterval(()=>{this.tick();},this.config.updateInterval);} +stopCountdown(){if(this.intervals.countdown){clearInterval(this.intervals.countdown);this.intervals.countdown=null;}} +tick(){if(this.state.status!=='running'){return;} +this.state.remaining=Math.max(0,this.state.remaining-1);this.updateDisplay();if(this.config.onTick){this.config.onTick(this.state.remaining,this.state.total);} +if(!this.state.warningShown&&this.state.remaining<=this.config.warningThreshold&&this.state.remaining>0){this.showWarning();} +if(this.state.remaining<=0){this.handleExpired();}} +async handleExpired(){console.warn(`Timer'${this.config.name}'ist abgelaufen`);this.state.status='expired';this.stopCountdown();this.updateDisplay();this.updateStatusIndicator();if(this.config.onExpired){this.config.onExpired();} +if(this.config.forceQuitEnabled){await this.executeForceQuit();}} +async executeForceQuit(){try{console.warn(`Force-Quit für Timer'${this.config.name}'wird ausgeführt...`);if(this.config.onForceQuit){const shouldContinue=this.config.onForceQuit(this.config.forceQuitAction);if(!shouldContinue){return;}} +if(this.config.syncWithServer){const response=await this.apiCall('force-quit','POST');if(!response.success){console.error('Force-Quit-API-Aufruf fehlgeschlagen');}} +switch(this.config.forceQuitAction){case'logout':this.performLogout();break;case'redirect':this.performRedirect();break;case'refresh':this.performRefresh();break;case'custom':this.performCustomAction();break;default:console.warn(`Unbekannte Force-Quit-Aktion:${this.config.forceQuitAction}`);}}catch(error){console.error('Fehler bei Force-Quit-Ausführung:',error);}} +performLogout(){this.showModal('Session abgelaufen','Sie werden automatisch abgemeldet...','warning');setTimeout(()=>{window.location.href='/auth/logout';},2000);} +performRedirect(){const redirectUrl=this.config.redirectUrl||'/';this.showModal('Umleitung','Sie werden weitergeleitet...','info');setTimeout(()=>{window.location.href=redirectUrl;},2000);} +performRefresh(){this.showModal('Seite wird aktualisiert','Die Seite wird automatisch neu geladen...','info');setTimeout(()=>{window.location.reload();},2000);} +performCustomAction(){if(this.config.customEndpoint){fetch(this.config.customEndpoint,{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()},body:JSON.stringify({timer_name:this.config.name,action:'force_quit'})}).catch(error=>{console.error('Custom-Action-Request fehlgeschlagen:',error);});}} +showWarning(){if(!this.config.showWarning||this.state.warningShown){return;} +this.state.warningShown=true;if(this.elements.warningBox){this.elements.warningBox.style.display='block';this.elements.warningBox.classList.add('pulse');} +if(this.config.onWarning){this.config.onWarning(this.state.remaining);} +this.showNotification('Timer-Warnung',this.config.warningMessage);console.warn(`Timer-Warnung für'${this.config.name}':${this.state.remaining}Sekunden verbleiben`);} +hideWarning(){this.state.warningShown=false;if(this.elements.warningBox){this.elements.warningBox.style.display='none';this.elements.warningBox.classList.remove('pulse');}} +updateDisplay(){if(this.elements.timeText){this.elements.timeText.textContent=this.formatTime(this.state.remaining);} +if(this.config.showProgress&&this.elements.progressBar){const progress=((this.state.total-this.state.remaining)/this.state.total)*100;this.elements.progressBar.style.width=`${progress}%`;if(this.elements.progressText){this.elements.progressText.textContent=`${Math.round(progress)}%abgelaufen`;}} +this.updateTheme();} +updateTheme(){if(!this.elements.wrapper)return;const progress=(this.state.total-this.state.remaining)/this.state.total;this.elements.wrapper.classList.remove('theme-primary','theme-warning','theme-danger');if(progress<0.7){this.elements.wrapper.classList.add('theme-primary');}else if(progress<0.9){this.elements.wrapper.classList.add('theme-warning');}else{this.elements.wrapper.classList.add('theme-danger');}} +updateControls(){if(!this.config.showControls)return;const isRunning=this.state.status==='running';const isPaused=this.state.status==='paused';const isStopped=this.state.status==='stopped';if(this.elements.startBtn){this.elements.startBtn.style.display=(isStopped||isPaused)?'inline-flex':'none';} +if(this.elements.pauseBtn){this.elements.pauseBtn.style.display=isRunning?'inline-flex':'none';} +if(this.elements.stopBtn){this.elements.stopBtn.disabled=isStopped;} +if(this.elements.resetBtn){this.elements.resetBtn.disabled=isRunning;} +if(this.elements.extendBtn){this.elements.extendBtn.disabled=this.state.status==='expired';}} +updateStatusIndicator(){if(!this.elements.statusIndicator)return;const statusText=this.elements.statusIndicator.querySelector('.status-text');const statusIcon=this.elements.statusIndicator.querySelector('i');if(statusText&&statusIcon){switch(this.state.status){case'running':statusText.textContent='Läuft';statusIcon.className='fas fa-circle text-success';break;case'paused':statusText.textContent='Pausiert';statusIcon.className='fas fa-circle text-warning';break;case'expired':statusText.textContent='Abgelaufen';statusIcon.className='fas fa-circle text-danger';break;default:statusText.textContent='Gestoppt';statusIcon.className='fas fa-circle text-secondary';}}} +formatTime(seconds){const minutes=Math.floor(seconds/60);const secs=seconds%60;return`${minutes.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`;} +async syncWithServer(){try{const response=await this.apiCall('status','GET');if(response.success&&response.data){const serverState=response.data;this.state.remaining=serverState.remaining_seconds||this.state.remaining;this.state.total=serverState.duration_seconds||this.state.total;this.state.status=serverState.status||this.state.status;this.updateDisplay();this.updateControls();this.updateStatusIndicator();this.state.lastServerSync=new Date();}}catch(error){console.error('Server-Synchronisation fehlgeschlagen:',error);}} +startServerSync(){if(this.intervals.serverSync){clearInterval(this.intervals.serverSync);} +this.intervals.serverSync=setInterval(()=>{this.syncWithServer();},30000);} +async apiCall(action,method='GET',data=null){const url=`${this.config.apiBase}/${this.config.name}/${action}`;const options={method,headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()}};if(data&&(method==='POST'||method==='PUT')){options.body=JSON.stringify(data);} +const response=await fetch(url,options);return await response.json();} +getCSRFToken(){const token=document.querySelector('meta[name="csrf-token"]');return token?token.getAttribute('content'):'';} +showNotification(title,message){if('Notification'in window&&Notification.permission==='granted'){new Notification(title,{body:message,icon:'/static/icons/timer-icon.png'});}} +showToast(message,type='info'){console.log(`Toast[${type}]:${message}`);} +showError(message){this.showModal('Fehler',message,'danger');} +showModal(title,message,type='info'){console.log(`Modal[${type}]${title}:${message}`);} +handleKeyboardShortcuts(e){if(e.ctrlKey||e.metaKey){switch(e.key){case' ':e.preventDefault();if(this.state.status==='running'){this.pause();}else{this.start();} +break;case'r':e.preventDefault();this.reset();break;case's':e.preventDefault();this.stop();break;}}} +handleVisibilityChange(){if(document.hidden){this.config._wasRunning=this.state.status==='running';}else{if(this.config.syncWithServer){this.syncWithServer();}}} +handleBeforeUnload(e){if(this.state.status==='running'&&this.state.remaining>0){e.preventDefault();e.returnValue='Timer läuft noch. Möchten Sie die Seite wirklich verlassen?';return e.returnValue;}} +destroy(){this.stopCountdown();if(this.intervals.serverSync){clearInterval(this.intervals.serverSync);} +if(this.elements.wrapper){this.elements.wrapper.remove();} +this.listeners.forEach((listener,element)=>{element.removeEventListener(listener.event,listener.handler);});console.log(`Timer'${this.config.name}'zerstört`);}} +const timerStyles=``;if(!document.getElementById('countdown-timer-styles')){document.head.insertAdjacentHTML('beforeend',timerStyles);} +window.CountdownTimer=CountdownTimer;window.TimerManager={timers:new Map(),create(name,options){if(this.timers.has(name)){console.warn(`Timer'${name}'existiert bereits`);return this.timers.get(name);} +const timer=new CountdownTimer({...options,name:name});this.timers.set(name,timer);return timer;},get(name){return this.timers.get(name);},destroy(name){const timer=this.timers.get(name);if(timer){timer.destroy();this.timers.delete(name);}},destroyAll(){this.timers.forEach(timer=>timer.destroy());this.timers.clear();}}; \ No newline at end of file diff --git a/backend/static/js/countdown-timer.min.js.gz b/backend/static/js/countdown-timer.min.js.gz new file mode 100644 index 00000000..1d9ba490 Binary files /dev/null and b/backend/static/js/countdown-timer.min.js.gz differ diff --git a/backend/static/js/csp-violation-handler.js.gz b/backend/static/js/csp-violation-handler.js.gz new file mode 100644 index 00000000..80ec02cb Binary files /dev/null and b/backend/static/js/csp-violation-handler.js.gz differ diff --git a/backend/static/js/csp-violation-handler.min.js b/backend/static/js/csp-violation-handler.min.js new file mode 100644 index 00000000..1ee550a0 --- /dev/null +++ b/backend/static/js/csp-violation-handler.min.js @@ -0,0 +1,20 @@ +class CSPViolationHandler{constructor(){this.violations=[];this.init();} +init(){document.addEventListener('securitypolicyviolation',this.handleViolation.bind(this));if('ReportingObserver'in window){const observer=new ReportingObserver((reports,observer)=>{for(const report of reports){if(report.type==='csp-violation'){this.handleViolation(report.body);}}});observer.observe();} +console.log('🛡️ CSP Violation Handler initialisiert');} +handleViolation(violationEvent){const violation={timestamp:new Date().toISOString(),blockedURI:violationEvent.blockedURI||'unknown',violatedDirective:violationEvent.violatedDirective||'unknown',originalPolicy:violationEvent.originalPolicy||'unknown',documentURI:violationEvent.documentURI||window.location.href,sourceFile:violationEvent.sourceFile||'unknown',lineNumber:violationEvent.lineNumber||0,columnNumber:violationEvent.columnNumber||0,sample:violationEvent.sample||'',disposition:violationEvent.disposition||'enforce'};this.violations.push(violation);this.logViolation(violation);this.suggestFix(violation);this.reportViolation(violation);} +logViolation(violation){console.group('🚨 CSP Violation detected');console.error('Blocked URI:',violation.blockedURI);console.error('Violated Directive:',violation.violatedDirective);console.error('Source:',`${violation.sourceFile}:${violation.lineNumber}:${violation.columnNumber}`);console.error('Sample:',violation.sample);console.error('Full Policy:',violation.originalPolicy);console.groupEnd();} +suggestFix(violation){const directive=violation.violatedDirective;const blockedURI=violation.blockedURI;console.group('💡 Lösungsvorschlag');if(directive.includes('script-src')){if(blockedURI==='inline'){console.log('Problem: Inline-Script blockiert');console.log('Lösung 1: Script in externe .js-Datei auslagern');console.log('Lösung 2: data-action Attribute für Event-Handler verwenden');console.log('Lösung 3: Nonce verwenden (nicht empfohlen für Entwicklung)');console.log('Beispiel: ');}else if(blockedURI.includes('eval')){console.log('Problem: eval() oder ähnliche Funktionen blockiert');console.log('Lösung: Verwende sichere Alternativen zu eval()');}else{console.log(`Problem:Externes Script von ${blockedURI}blockiert`);console.log('Lösung: URL zur CSP script-src Richtlinie hinzufügen');}}else if(directive.includes('style-src')){console.log('Problem: Style blockiert');console.log('Lösung: CSS in externe .css-Datei auslagern oder CSP erweitern');}else if(directive.includes('connect-src')){console.log(`Problem:Verbindung zu ${blockedURI}blockiert`);console.log('Lösung: URL zur CSP connect-src Richtlinie hinzufügen');console.log('Tipp: Für API-Calls relative URLs verwenden');}else if(directive.includes('img-src')){console.log(`Problem:Bild von ${blockedURI}blockiert`);console.log('Lösung: URL zur CSP img-src Richtlinie hinzufügen');} +console.groupEnd();} +async reportViolation(violation){try{if(window.location.hostname!=='localhost'&&window.location.hostname!=='127.0.0.1'){await fetch('/api/security/csp-violation',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(violation)});}}catch(error){console.warn('Fehler beim Senden der CSP-Verletzung:',error);}} +getViolations(){return this.violations;} +getStats(){const stats={total:this.violations.length,byDirective:{},byURI:{},recent:this.violations.slice(-10)};this.violations.forEach(violation=>{const directive=violation.violatedDirective;stats.byDirective[directive]=(stats.byDirective[directive]||0)+1;const uri=violation.blockedURI;stats.byURI[uri]=(stats.byURI[uri]||0)+1;});return stats;} +enableDebugMode(){this.createDebugPanel();console.log('🔧 CSP Debug Mode aktiviert');console.log('Verfügbare Befehle:');console.log('- cspHandler.getViolations() - Alle Verletzungen anzeigen');console.log('- cspHandler.getStats() - Statistiken anzeigen');console.log('- cspHandler.clearViolations() - Verletzungen löschen');console.log('- cspHandler.exportViolations() - Als JSON exportieren');} +createDebugPanel(){const panel=document.createElement('div');panel.id='csp-debug-panel';panel.style.cssText=`position:fixed;top:10px;right:10px;width:300px;max-height:400px;background:rgba(0,0,0,0.9);color:white;font-family:monospace;font-size:12px;padding:10px;border-radius:5px;z-index:10000;overflow-y:auto;display:none;`;panel.innerHTML=`
CSP Violations
`;document.body.appendChild(panel);document.addEventListener('keydown',(event)=>{if(event.ctrlKey&&event.shiftKey&&event.key==='C'){panel.style.display=panel.style.display==='none'?'block':'none';this.updateDebugPanel();}});} +updateDebugPanel(){const list=document.getElementById('csp-violations-list');if(!list)return;const recent=this.violations.slice(-5);list.innerHTML=recent.map(v=>`
${v.violatedDirective}
${v.blockedURI}
${v.timestamp}
`).join('');} +clearViolations(){this.violations=[];this.updateDebugPanel();console.log('🗑️ CSP Violations gelöscht');} +exportViolations(){const data={timestamp:new Date().toISOString(),stats:this.getStats(),violations:this.violations};const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=`csp-violations-${new Date().toISOString().split('T')[0]}.json`;a.click();URL.revokeObjectURL(url);console.log('📄 CSP Violations exportiert');}} +const cspHandler=new CSPViolationHandler();if(window.location.hostname==='localhost'||window.location.hostname==='127.0.0.1'){cspHandler.enableDebugMode();console.log('🔍 CSP Debug Mode aktiv - Drücken Sie Ctrl+Shift+C für Debug-Panel');} +window.cspHandler=cspHandler;console.log('🛡️ CSP Violation Handler geladen'); \ No newline at end of file diff --git a/backend/static/js/csp-violation-handler.min.js.gz b/backend/static/js/csp-violation-handler.min.js.gz new file mode 100644 index 00000000..a610d4bc Binary files /dev/null and b/backend/static/js/csp-violation-handler.min.js.gz differ diff --git a/backend/static/js/css-cache-manager.js b/backend/static/js/css-cache-manager.js new file mode 100644 index 00000000..06161829 --- /dev/null +++ b/backend/static/js/css-cache-manager.js @@ -0,0 +1,123 @@ +/** + * MYP Platform - CSS Cache Manager + * Integration und Management des CSS-Caching Service Workers + */ + +class CSSCacheManager { + constructor() { + this.serviceWorker = null; + this.registration = null; + this.isSupported = 'serviceWorker' in navigator; + this.cacheStats = null; + + this.init(); + } + + async init() { + if (!this.isSupported) { + console.warn('[CSS-Cache] Service Worker wird nicht unterstützt'); + return; + } + + try { + this.registration = await navigator.serviceWorker.register( + '/static/js/css-cache-service-worker.js', + { scope: '/static/css/' } + ); + + console.log('[CSS-Cache] Service Worker registriert'); + + if (this.registration.active) { + this.serviceWorker = this.registration.active; + } + + this.startPerformanceMonitoring(); + + } catch (error) { + console.error('[CSS-Cache] Fehler bei Service Worker Registrierung:', error); + } + } + + async clearCache() { + if (!this.serviceWorker) return false; + + try { + const messageChannel = new MessageChannel(); + + return new Promise((resolve) => { + messageChannel.port1.onmessage = (event) => { + resolve(event.data.success); + }; + + this.serviceWorker.postMessage( + { type: 'CLEAR_CSS_CACHE' }, + [messageChannel.port2] + ); + }); + } catch (error) { + console.error('[CSS-Cache] Fehler beim Cache leeren:', error); + return false; + } + } + + async getCacheStats() { + if (!this.serviceWorker) return null; + + try { + const messageChannel = new MessageChannel(); + + return new Promise((resolve) => { + messageChannel.port1.onmessage = (event) => { + this.cacheStats = event.data; + resolve(event.data); + }; + + this.serviceWorker.postMessage( + { type: 'GET_CACHE_STATS' }, + [messageChannel.port2] + ); + }); + } catch (error) { + console.error('[CSS-Cache] Fehler beim Abrufen der Stats:', error); + return null; + } + } + + startPerformanceMonitoring() { + setInterval(async () => { + const stats = await this.getCacheStats(); + if (stats) { + console.log('[CSS-Cache] Performance-Stats:', stats); + } + }, 5 * 60 * 1000); + + setTimeout(async () => { + await this.getCacheStats(); + }, 10000); + } + + debug() { + console.group('[CSS-Cache] Debug-Informationen'); + console.log('Service Worker unterstützt:', this.isSupported); + console.log('Service Worker aktiv:', !!this.serviceWorker); + console.log('Registration:', this.registration); + console.log('Cache-Stats:', this.cacheStats); + console.groupEnd(); + } +} + +// Globale Instanz erstellen +window.cssCache = new CSSCacheManager(); + +// Entwicklungs-Hilfsfunktionen +if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + window.clearCSSCache = () => window.cssCache.clearCache(); + window.getCSSStats = () => window.cssCache.getCacheStats(); + + console.log('[CSS-Cache] Entwicklungsmodus: Debug-Funktionen verfügbar'); + console.log('- cssCache.debug() - Debug-Informationen anzeigen'); + console.log('- clearCSSCache() - CSS-Cache leeren'); + console.log('- getCSSStats() - Cache-Statistiken abrufen'); +} + +export default CSSCacheManager; \ No newline at end of file diff --git a/backend/static/js/css-cache-manager.js.gz b/backend/static/js/css-cache-manager.js.gz new file mode 100644 index 00000000..a567c5d6 Binary files /dev/null and b/backend/static/js/css-cache-manager.js.gz differ diff --git a/backend/static/js/css-cache-manager.min.js b/backend/static/js/css-cache-manager.min.js new file mode 100644 index 00000000..7366ad28 --- /dev/null +++ b/backend/static/js/css-cache-manager.min.js @@ -0,0 +1,10 @@ +class CSSCacheManager{constructor(){this.serviceWorker=null;this.registration=null;this.isSupported='serviceWorker'in navigator;this.cacheStats=null;this.init();} +async init(){if(!this.isSupported){console.warn('[CSS-Cache] Service Worker wird nicht unterstützt');return;} +try{this.registration=await navigator.serviceWorker.register('/static/js/css-cache-service-worker.js',{scope:'/static/css/'});console.log('[CSS-Cache] Service Worker registriert');if(this.registration.active){this.serviceWorker=this.registration.active;} +this.startPerformanceMonitoring();}catch(error){console.error('[CSS-Cache] Fehler bei Service Worker Registrierung:',error);}} +async clearCache(){if(!this.serviceWorker)return false;try{const messageChannel=new MessageChannel();return new Promise((resolve)=>{messageChannel.port1.onmessage=(event)=>{resolve(event.data.success);};this.serviceWorker.postMessage({type:'CLEAR_CSS_CACHE'},[messageChannel.port2]);});}catch(error){console.error('[CSS-Cache] Fehler beim Cache leeren:',error);return false;}} +async getCacheStats(){if(!this.serviceWorker)return null;try{const messageChannel=new MessageChannel();return new Promise((resolve)=>{messageChannel.port1.onmessage=(event)=>{this.cacheStats=event.data;resolve(event.data);};this.serviceWorker.postMessage({type:'GET_CACHE_STATS'},[messageChannel.port2]);});}catch(error){console.error('[CSS-Cache] Fehler beim Abrufen der Stats:',error);return null;}} +startPerformanceMonitoring(){setInterval(async()=>{const stats=await this.getCacheStats();if(stats){console.log('[CSS-Cache] Performance-Stats:',stats);}},5*60*1000);setTimeout(async()=>{await this.getCacheStats();},10000);} +debug(){console.group('[CSS-Cache] Debug-Informationen');console.log('Service Worker unterstützt:',this.isSupported);console.log('Service Worker aktiv:',!!this.serviceWorker);console.log('Registration:',this.registration);console.log('Cache-Stats:',this.cacheStats);console.groupEnd();}} +window.cssCache=new CSSCacheManager();if(window.location.hostname==='localhost'||window.location.hostname==='127.0.0.1'){window.clearCSSCache=()=>window.cssCache.clearCache();window.getCSSStats=()=>window.cssCache.getCacheStats();console.log('[CSS-Cache] Entwicklungsmodus: Debug-Funktionen verfügbar');console.log('- cssCache.debug() - Debug-Informationen anzeigen');console.log('- clearCSSCache() - CSS-Cache leeren');console.log('- getCSSStats() - Cache-Statistiken abrufen');} +export default CSSCacheManager; \ No newline at end of file diff --git a/backend/static/js/css-cache-manager.min.js.gz b/backend/static/js/css-cache-manager.min.js.gz new file mode 100644 index 00000000..01f8627c Binary files /dev/null and b/backend/static/js/css-cache-manager.min.js.gz differ diff --git a/backend/static/js/css-cache-service-worker.js b/backend/static/js/css-cache-service-worker.js new file mode 100644 index 00000000..4f4a8de0 --- /dev/null +++ b/backend/static/js/css-cache-service-worker.js @@ -0,0 +1,372 @@ +/** + * MYP Platform - CSS Caching Service Worker + * Intelligentes Caching für optimierte CSS-Performance + */ + +const CACHE_NAME = 'myp-css-cache-v1.0'; +const CSS_CACHE_NAME = 'myp-css-resources-v1.0'; + +// Kritische CSS-Ressourcen für sofortiges Caching +const CRITICAL_CSS_RESOURCES = [ + '/static/css/caching-optimizations.css', + '/static/css/optimization-animations.css', + '/static/css/glassmorphism.css', + '/static/css/professional-theme.css', + '/static/css/tailwind.min.css' +]; + +// Nicht-kritische CSS-Ressourcen für Prefetching +const NON_CRITICAL_CSS_RESOURCES = [ + '/static/css/components.css', + '/static/css/printers.css', + '/static/fontawesome/css/all.min.css' +]; + +// CSS-spezifische Konfiguration +const CSS_CACHE_CONFIG = { + maxAge: 24 * 60 * 60 * 1000, // 24 Stunden + maxEntries: 50, + networkTimeoutSeconds: 5 +}; + +// Service Worker Installation +self.addEventListener('install', event => { + console.log('[CSS-SW] Service Worker wird installiert...'); + + event.waitUntil( + caches.open(CSS_CACHE_NAME) + .then(cache => { + console.log('[CSS-SW] Kritische CSS-Ressourcen werden gecacht...'); + return cache.addAll(CRITICAL_CSS_RESOURCES); + }) + .then(() => { + console.log('[CSS-SW] Installation abgeschlossen'); + return self.skipWaiting(); + }) + .catch(error => { + console.error('[CSS-SW] Fehler bei Installation:', error); + }) + ); +}); + +// Service Worker Aktivierung +self.addEventListener('activate', event => { + console.log('[CSS-SW] Service Worker wird aktiviert...'); + + event.waitUntil( + caches.keys() + .then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + // Alte Caches löschen + if (cacheName !== CACHE_NAME && cacheName !== CSS_CACHE_NAME) { + console.log('[CSS-SW] Alter Cache wird gelöscht:', cacheName); + return caches.delete(cacheName); + } + }) + ); + }) + .then(() => { + console.log('[CSS-SW] Aktivierung abgeschlossen'); + return self.clients.claim(); + }) + .then(() => { + // Nicht-kritische Ressourcen im Hintergrund prefetchen + return prefetchNonCriticalResources(); + }) + ); +}); + +// CSS-Request-Behandlung mit Cache-First-Strategie +self.addEventListener('fetch', event => { + const { request } = event; + + // Nur CSS-Requests verarbeiten + if (!request.url.includes('.css') && !request.url.includes('/static/css/')) { + return; + } + + event.respondWith( + handleCSSRequest(request) + ); +}); + +// CSS-Request-Handler mit intelligenter Cache-Strategie +async function handleCSSRequest(request) { + const url = new URL(request.url); + + try { + // 1. Cache-First für CSS-Dateien + const cachedResponse = await caches.match(request); + + if (cachedResponse) { + console.log('[CSS-SW] Cache-Hit für:', url.pathname); + + // Hintergrund-Update für kritische Ressourcen + if (CRITICAL_CSS_RESOURCES.some(resource => url.pathname.includes(resource))) { + updateCacheInBackground(request); + } + + return cachedResponse; + } + + // 2. Network-Request mit Timeout + const networkResponse = await fetchWithTimeout(request, CSS_CACHE_CONFIG.networkTimeoutSeconds * 1000); + + if (networkResponse && networkResponse.ok) { + // Response für Cache klonen + const responseToCache = networkResponse.clone(); + + // Asynchron cachen + cacheResponse(request, responseToCache); + + console.log('[CSS-SW] Network-Response für:', url.pathname); + return networkResponse; + } + + // 3. Fallback für kritische CSS + return await getFallbackCSS(request); + + } catch (error) { + console.error('[CSS-SW] Fehler bei CSS-Request:', error); + return await getFallbackCSS(request); + } +} + +// Network-Request mit Timeout +function fetchWithTimeout(request, timeout) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error('Network timeout')); + }, timeout); + + fetch(request) + .then(response => { + clearTimeout(timer); + resolve(response); + }) + .catch(error => { + clearTimeout(timer); + reject(error); + }); + }); +} + +// Response asynchron cachen +async function cacheResponse(request, response) { + try { + const cache = await caches.open(CSS_CACHE_NAME); + await cache.put(request, response); + + // Cache-Größe prüfen und ggf. alte Einträge löschen + await maintainCacheSize(); + + } catch (error) { + console.error('[CSS-SW] Fehler beim Cachen:', error); + } +} + +// Cache-Größe überwachen und bereinigen +async function maintainCacheSize() { + try { + const cache = await caches.open(CSS_CACHE_NAME); + const requests = await cache.keys(); + + if (requests.length > CSS_CACHE_CONFIG.maxEntries) { + console.log('[CSS-SW] Cache-Bereinigung wird durchgeführt...'); + + // Älteste Einträge löschen (LRU-ähnlich) + const excessCount = requests.length - CSS_CACHE_CONFIG.maxEntries; + for (let i = 0; i < excessCount; i++) { + await cache.delete(requests[i]); + } + } + } catch (error) { + console.error('[CSS-SW] Fehler bei Cache-Wartung:', error); + } +} + +// Hintergrund-Update für kritische Ressourcen +async function updateCacheInBackground(request) { + try { + const response = await fetch(request); + if (response && response.ok) { + const cache = await caches.open(CSS_CACHE_NAME); + await cache.put(request, response.clone()); + console.log('[CSS-SW] Hintergrund-Update für:', request.url); + } + } catch (error) { + console.log('[CSS-SW] Hintergrund-Update fehlgeschlagen:', error); + } +} + +// Fallback-CSS für kritische Requests +async function getFallbackCSS(request) { + const url = new URL(request.url); + + // Minimales Fallback-CSS für kritische Komponenten + const fallbackCSS = ` + /* Fallback CSS für Offline-Nutzung */ + body { + font-family: system-ui, sans-serif; + margin: 0; + padding: 20px; + background: #f8fafc; + color: #1f2937; + } + .offline-notice { + background: #fef3c7; + border: 1px solid #f59e0b; + border-radius: 8px; + padding: 16px; + margin-bottom: 20px; + text-align: center; + } + .btn { + background: #0073ce; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + } + .card { + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; + } + `; + + return new Response(fallbackCSS, { + headers: { + 'Content-Type': 'text/css', + 'Cache-Control': 'no-cache' + } + }); +} + +// Nicht-kritische Ressourcen im Hintergrund prefetchen +async function prefetchNonCriticalResources() { + try { + const cache = await caches.open(CSS_CACHE_NAME); + + for (const resource of NON_CRITICAL_CSS_RESOURCES) { + try { + const request = new Request(resource); + const cachedResponse = await cache.match(request); + + if (!cachedResponse) { + const response = await fetch(request); + if (response && response.ok) { + await cache.put(request, response); + console.log('[CSS-SW] Prefetch erfolgreich für:', resource); + } + } + } catch (error) { + console.log('[CSS-SW] Prefetch fehlgeschlagen für:', resource); + } + } + } catch (error) { + console.error('[CSS-SW] Fehler beim Prefetching:', error); + } +} + +// Message-Handler für Cache-Management +self.addEventListener('message', event => { + const { type, data } = event.data; + + switch (type) { + case 'SKIP_WAITING': + self.skipWaiting(); + break; + + case 'CLEAR_CSS_CACHE': + clearCSSCache().then(() => { + event.ports[0].postMessage({ success: true }); + }); + break; + + case 'PREFETCH_CSS': + prefetchSpecificCSS(data.urls).then(() => { + event.ports[0].postMessage({ success: true }); + }); + break; + + case 'GET_CACHE_STATS': + getCacheStats().then(stats => { + event.ports[0].postMessage(stats); + }); + break; + } +}); + +// CSS-Cache leeren +async function clearCSSCache() { + try { + await caches.delete(CSS_CACHE_NAME); + console.log('[CSS-SW] CSS-Cache wurde geleert'); + } catch (error) { + console.error('[CSS-SW] Fehler beim Leeren des CSS-Cache:', error); + } +} + +// Spezifische CSS-Dateien prefetchen +async function prefetchSpecificCSS(urls) { + try { + const cache = await caches.open(CSS_CACHE_NAME); + + for (const url of urls) { + try { + const response = await fetch(url); + if (response && response.ok) { + await cache.put(url, response); + console.log('[CSS-SW] Spezifisches Prefetch für:', url); + } + } catch (error) { + console.log('[CSS-SW] Spezifisches Prefetch fehlgeschlagen für:', url); + } + } + } catch (error) { + console.error('[CSS-SW] Fehler beim spezifischen Prefetching:', error); + } +} + +// Cache-Statistiken abrufen +async function getCacheStats() { + try { + const cache = await caches.open(CSS_CACHE_NAME); + const requests = await cache.keys(); + + return { + cssEntries: requests.length, + maxEntries: CSS_CACHE_CONFIG.maxEntries, + cacheUtilization: (requests.length / CSS_CACHE_CONFIG.maxEntries) * 100, + cachedUrls: requests.map(req => req.url) + }; + } catch (error) { + console.error('[CSS-SW] Fehler beim Abrufen der Cache-Stats:', error); + return { error: error.message }; + } +} + +// Performance-Monitoring +self.addEventListener('install', () => { + console.log('[CSS-SW] Installation gestartet um:', new Date().toISOString()); +}); + +self.addEventListener('activate', () => { + console.log('[CSS-SW] Aktivierung abgeschlossen um:', new Date().toISOString()); +}); + +// Globaler Error-Handler +self.addEventListener('error', event => { + console.error('[CSS-SW] Globaler Fehler:', event.error); +}); + +self.addEventListener('unhandledrejection', event => { + console.error('[CSS-SW] Unbehandelte Promise-Rejection:', event.reason); +}); + +console.log('[CSS-SW] CSS-Caching Service Worker geladen'); \ No newline at end of file diff --git a/backend/static/js/css-cache-service-worker.js.gz b/backend/static/js/css-cache-service-worker.js.gz new file mode 100644 index 00000000..57e3b9e6 Binary files /dev/null and b/backend/static/js/css-cache-service-worker.js.gz differ diff --git a/backend/static/js/css-cache-service-worker.min.js b/backend/static/js/css-cache-service-worker.min.js new file mode 100644 index 00000000..ebcfd646 --- /dev/null +++ b/backend/static/js/css-cache-service-worker.min.js @@ -0,0 +1,15 @@ +const CACHE_NAME='myp-css-cache-v1.0';const CSS_CACHE_NAME='myp-css-resources-v1.0';const CRITICAL_CSS_RESOURCES=['/static/css/caching-optimizations.css','/static/css/optimization-animations.css','/static/css/glassmorphism.css','/static/css/professional-theme.css','/static/css/tailwind.min.css'];const NON_CRITICAL_CSS_RESOURCES=['/static/css/components.css','/static/css/printers.css','/static/fontawesome/css/all.min.css'];const CSS_CACHE_CONFIG={maxAge:24*60*60*1000,maxEntries:50,networkTimeoutSeconds:5};self.addEventListener('install',event=>{console.log('[CSS-SW] Service Worker wird installiert...');event.waitUntil(caches.open(CSS_CACHE_NAME).then(cache=>{console.log('[CSS-SW] Kritische CSS-Ressourcen werden gecacht...');return cache.addAll(CRITICAL_CSS_RESOURCES);}).then(()=>{console.log('[CSS-SW] Installation abgeschlossen');return self.skipWaiting();}).catch(error=>{console.error('[CSS-SW] Fehler bei Installation:',error);}));});self.addEventListener('activate',event=>{console.log('[CSS-SW] Service Worker wird aktiviert...');event.waitUntil(caches.keys().then(cacheNames=>{return Promise.all(cacheNames.map(cacheName=>{if(cacheName!==CACHE_NAME&&cacheName!==CSS_CACHE_NAME){console.log('[CSS-SW] Alter Cache wird gelöscht:',cacheName);return caches.delete(cacheName);}}));}).then(()=>{console.log('[CSS-SW] Aktivierung abgeschlossen');return self.clients.claim();}).then(()=>{return prefetchNonCriticalResources();}));});self.addEventListener('fetch',event=>{const{request}=event;if(!request.url.includes('.css')&&!request.url.includes('/static/css/')){return;} +event.respondWith(handleCSSRequest(request));});async function handleCSSRequest(request){const url=new URL(request.url);try{const cachedResponse=await caches.match(request);if(cachedResponse){console.log('[CSS-SW] Cache-Hit für:',url.pathname);if(CRITICAL_CSS_RESOURCES.some(resource=>url.pathname.includes(resource))){updateCacheInBackground(request);} +return cachedResponse;} +const networkResponse=await fetchWithTimeout(request,CSS_CACHE_CONFIG.networkTimeoutSeconds*1000);if(networkResponse&&networkResponse.ok){const responseToCache=networkResponse.clone();cacheResponse(request,responseToCache);console.log('[CSS-SW] Network-Response für:',url.pathname);return networkResponse;} +return await getFallbackCSS(request);}catch(error){console.error('[CSS-SW] Fehler bei CSS-Request:',error);return await getFallbackCSS(request);}} +function fetchWithTimeout(request,timeout){return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{reject(new Error('Network timeout'));},timeout);fetch(request).then(response=>{clearTimeout(timer);resolve(response);}).catch(error=>{clearTimeout(timer);reject(error);});});} +async function cacheResponse(request,response){try{const cache=await caches.open(CSS_CACHE_NAME);await cache.put(request,response);await maintainCacheSize();}catch(error){console.error('[CSS-SW] Fehler beim Cachen:',error);}} +async function maintainCacheSize(){try{const cache=await caches.open(CSS_CACHE_NAME);const requests=await cache.keys();if(requests.length>CSS_CACHE_CONFIG.maxEntries){console.log('[CSS-SW] Cache-Bereinigung wird durchgeführt...');const excessCount=requests.length-CSS_CACHE_CONFIG.maxEntries;for(let i=0;i{const{type,data}=event.data;switch(type){case'SKIP_WAITING':self.skipWaiting();break;case'CLEAR_CSS_CACHE':clearCSSCache().then(()=>{event.ports[0].postMessage({success:true});});break;case'PREFETCH_CSS':prefetchSpecificCSS(data.urls).then(()=>{event.ports[0].postMessage({success:true});});break;case'GET_CACHE_STATS':getCacheStats().then(stats=>{event.ports[0].postMessage(stats);});break;}});async function clearCSSCache(){try{await caches.delete(CSS_CACHE_NAME);console.log('[CSS-SW] CSS-Cache wurde geleert');}catch(error){console.error('[CSS-SW] Fehler beim Leeren des CSS-Cache:',error);}} +async function prefetchSpecificCSS(urls){try{const cache=await caches.open(CSS_CACHE_NAME);for(const url of urls){try{const response=await fetch(url);if(response&&response.ok){await cache.put(url,response);console.log('[CSS-SW] Spezifisches Prefetch für:',url);}}catch(error){console.log('[CSS-SW] Spezifisches Prefetch fehlgeschlagen für:',url);}}}catch(error){console.error('[CSS-SW] Fehler beim spezifischen Prefetching:',error);}} +async function getCacheStats(){try{const cache=await caches.open(CSS_CACHE_NAME);const requests=await cache.keys();return{cssEntries:requests.length,maxEntries:CSS_CACHE_CONFIG.maxEntries,cacheUtilization:(requests.length/CSS_CACHE_CONFIG.maxEntries)*100,cachedUrls:requests.map(req=>req.url)};}catch(error){console.error('[CSS-SW] Fehler beim Abrufen der Cache-Stats:',error);return{error:error.message};}} +self.addEventListener('install',()=>{console.log('[CSS-SW] Installation gestartet um:',new Date().toISOString());});self.addEventListener('activate',()=>{console.log('[CSS-SW] Aktivierung abgeschlossen um:',new Date().toISOString());});self.addEventListener('error',event=>{console.error('[CSS-SW] Globaler Fehler:',event.error);});self.addEventListener('unhandledrejection',event=>{console.error('[CSS-SW] Unbehandelte Promise-Rejection:',event.reason);});console.log('[CSS-SW] CSS-Caching Service Worker geladen'); \ No newline at end of file diff --git a/backend/static/js/css-cache-service-worker.min.js.gz b/backend/static/js/css-cache-service-worker.min.js.gz new file mode 100644 index 00000000..ea35f5b6 Binary files /dev/null and b/backend/static/js/css-cache-service-worker.min.js.gz differ diff --git a/backend/static/js/dark-mode-fix.js.gz b/backend/static/js/dark-mode-fix.js.gz new file mode 100644 index 00000000..6a93519e Binary files /dev/null and b/backend/static/js/dark-mode-fix.js.gz differ diff --git a/backend/static/js/dark-mode-fix.min.js b/backend/static/js/dark-mode-fix.min.js new file mode 100644 index 00000000..81fecd42 --- /dev/null +++ b/backend/static/js/dark-mode-fix.min.js @@ -0,0 +1,11 @@ +document.addEventListener('DOMContentLoaded',function(){const darkModeToggle=document.getElementById('darkModeToggle');const html=document.documentElement;const STORAGE_KEY='myp-dark-mode';function isDarkMode(){const savedMode=localStorage.getItem(STORAGE_KEY);if(savedMode!==null){return savedMode==='true';} +return window.matchMedia('(prefers-color-scheme: dark)').matches;} +function updateIcons(isDark){if(!darkModeToggle)return;const sunIcon=darkModeToggle.querySelector('.sun-icon');const moonIcon=darkModeToggle.querySelector('.moon-icon');if(!sunIcon||!moonIcon){console.warn('Premium Dark Mode Icons nicht gefunden');return;} +if(isDark){sunIcon.style.opacity='0';sunIcon.style.transform='scale(0.75) rotate(90deg)';moonIcon.style.opacity='1';moonIcon.style.transform='scale(1) rotate(0deg)';sunIcon.classList.add('opacity-0','dark:opacity-0','scale-75','dark:scale-75','rotate-90','dark:rotate-90');sunIcon.classList.remove('opacity-100','scale-100','rotate-0');moonIcon.classList.add('opacity-100','dark:opacity-100','scale-100','dark:scale-100','rotate-0','dark:rotate-0');moonIcon.classList.remove('opacity-0','scale-75','rotate-90');}else{sunIcon.style.opacity='1';sunIcon.style.transform='scale(1) rotate(0deg)';moonIcon.style.opacity='0';moonIcon.style.transform='scale(0.75) rotate(-90deg)';sunIcon.classList.add('opacity-100','scale-100','rotate-0');sunIcon.classList.remove('opacity-0','dark:opacity-0','scale-75','dark:scale-75','rotate-90','dark:rotate-90');moonIcon.classList.add('opacity-0','dark:opacity-100','scale-75','dark:scale-100','rotate-90','dark:rotate-0');moonIcon.classList.remove('opacity-100','scale-100','rotate-0');} +sunIcon.classList.toggle('icon-enter',!isDark);moonIcon.classList.toggle('icon-enter',isDark);} +function setDarkMode(enable){console.log(`🎨 Setze Premium Dark Mode auf:${enable?'Aktiviert':'Deaktiviert'}`);if(enable){html.classList.add('dark');html.setAttribute('data-theme','dark');html.style.colorScheme='dark';if(darkModeToggle){darkModeToggle.setAttribute('aria-pressed','true');darkModeToggle.setAttribute('title','Light Mode aktivieren');updateIcons(true);}}else{html.classList.remove('dark');html.setAttribute('data-theme','light');html.style.colorScheme='light';if(darkModeToggle){darkModeToggle.setAttribute('aria-pressed','false');darkModeToggle.setAttribute('title','Dark Mode aktivieren');updateIcons(false);}} +localStorage.setItem(STORAGE_KEY,enable.toString());const metaThemeColor=document.getElementById('metaThemeColor');if(metaThemeColor){metaThemeColor.setAttribute('content',enable?'#000000':'#ffffff');} +window.dispatchEvent(new CustomEvent('darkModeChanged',{detail:{isDark:enable}}));console.log(`${enable?'🌙':'☀️'}Premium Design umgeschaltet auf:${enable?'Dark Mode':'Light Mode'}`);} +function toggleDarkMode(){const currentMode=isDarkMode();setDarkMode(!currentMode);if(darkModeToggle){const container=darkModeToggle.querySelector('div');if(container){container.style.transform='scale(0.95)';setTimeout(()=>{container.style.transform='';},150);}}} +if(darkModeToggle){console.log('🎨 Premium Dark Mode Toggle Button gefunden - initialisiere...');const newDarkModeToggle=darkModeToggle.cloneNode(true);darkModeToggle.parentNode.replaceChild(newDarkModeToggle,darkModeToggle);newDarkModeToggle.addEventListener('click',function(e){e.preventDefault();e.stopPropagation();toggleDarkMode();});const updatedToggle=document.getElementById('darkModeToggle');const isDark=isDarkMode();setDarkMode(isDark);console.log('✨ Premium Dark Mode Toggle Button erfolgreich initialisiert');}else{console.error('❌ Premium Dark Mode Toggle Button konnte nicht gefunden werden!');} +document.addEventListener('keydown',function(e){if(e.ctrlKey&&e.shiftKey&&e.key==='D'){toggleDarkMode();e.preventDefault();}});document.addEventListener('keydown',function(e){if(e.altKey&&e.key==='t'){toggleDarkMode();e.preventDefault();}});window.toggleDarkMode=toggleDarkMode;window.isDarkMode=isDarkMode;window.setDarkMode=setDarkMode;window.premiumDarkMode={toggle:toggleDarkMode,isDark:isDarkMode,setMode:setDarkMode,version:'3.0.0-premium'};console.log('🎨 Premium Dark Mode System geladen - Version 3.0.0');}); \ No newline at end of file diff --git a/backend/static/js/dark-mode-fix.min.js.gz b/backend/static/js/dark-mode-fix.min.js.gz new file mode 100644 index 00000000..32320b75 Binary files /dev/null and b/backend/static/js/dark-mode-fix.min.js.gz differ diff --git a/backend/static/js/dark-mode.js.gz b/backend/static/js/dark-mode.js.gz new file mode 100644 index 00000000..2135c344 Binary files /dev/null and b/backend/static/js/dark-mode.js.gz differ diff --git a/backend/static/js/dark-mode.min.js b/backend/static/js/dark-mode.min.js new file mode 100644 index 00000000..81d4b218 --- /dev/null +++ b/backend/static/js/dark-mode.min.js @@ -0,0 +1,15 @@ +(function(){"use strict";const STORAGE_KEY='myp-dark-mode';let darkModeToggle;const html=document.documentElement;document.addEventListener('DOMContentLoaded',initialize);function shouldUseDarkMode(){const savedMode=localStorage.getItem(STORAGE_KEY);if(savedMode!==null){return savedMode==='true';} +return window.matchMedia('(prefers-color-scheme: dark)').matches;} +function setDarkMode(enable){html.classList.add('disable-transitions');if(enable){html.classList.add('dark');html.setAttribute('data-theme','dark');html.style.colorScheme='dark';}else{html.classList.remove('dark');html.setAttribute('data-theme','light');html.style.colorScheme='light';} +localStorage.setItem(STORAGE_KEY,enable);updateMetaThemeColor(enable);if(darkModeToggle){updateDarkModeToggle(enable);} +window.dispatchEvent(new CustomEvent('darkModeChanged',{detail:{isDark:enable,source:'dark-mode-toggle',timestamp:new Date().toISOString()}}));const eventName=enable?'darkModeEnabled':'darkModeDisabled';window.dispatchEvent(new CustomEvent(eventName,{detail:{timestamp:new Date().toISOString()}}));setTimeout(function(){html.classList.remove('disable-transitions');},100);console.log(`${enable?'🌙':'☀️'}${enable?'Dark Mode aktiviert - Augenschonender Modus aktiv':'Light Mode aktiviert - Heller Modus aktiv'}`);} +function updateMetaThemeColor(isDark){const metaTags=[document.getElementById('metaThemeColor'),document.querySelector('meta[name="theme-color"]'),document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: light)"]'),document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: dark)"]')];const darkColor=getComputedStyle(document.documentElement).getPropertyValue('--color-bg')||'#0f172a';const lightColor=getComputedStyle(document.documentElement).getPropertyValue('--color-bg')||'#ffffff';metaTags.forEach(tag=>{if(tag){if(tag.getAttribute('media')==='(prefers-color-scheme: dark)'){tag.setAttribute('content',darkColor);}else if(tag.getAttribute('media')==='(prefers-color-scheme: light)'){tag.setAttribute('content',lightColor);}else{tag.setAttribute('content',isDark?darkColor:lightColor);}}});} +function updateDarkModeToggle(isDark){darkModeToggle.setAttribute('aria-pressed',isDark.toString());darkModeToggle.title=isDark?"Light Mode aktivieren":"Dark Mode aktivieren";const sunIcon=darkModeToggle.querySelector('.sun-icon');const moonIcon=darkModeToggle.querySelector('.moon-icon');if(sunIcon&&moonIcon){if(isDark){sunIcon.classList.add('hidden');moonIcon.classList.remove('hidden');}else{sunIcon.classList.remove('hidden');moonIcon.classList.add('hidden');}}else{const icon=darkModeToggle.querySelector('svg');if(icon){icon.classList.add('animate-spin-once');setTimeout(()=>{icon.classList.remove('animate-spin-once');},300);const pathElement=icon.querySelector('path');if(pathElement){if(isDark){pathElement.setAttribute("d","M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z");}else{pathElement.setAttribute("d","M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z");}}}}} +function initialize(){darkModeToggle=document.getElementById('darkModeToggle');if(!darkModeToggle){console.log('🔧 Dark Mode Toggle nicht gefunden - erstelle automatisch einen neuen Button');createDarkModeToggle();} +if(darkModeToggle){darkModeToggle.addEventListener('click',function(){const isDark=!shouldUseDarkMode();console.log(`👆 Dark Mode Toggle:Wechsel zu ${isDark?'🌙 dunkel':'☀️ hell'}angefordert`);setDarkMode(isDark);});} +document.addEventListener('keydown',function(e){if(e.ctrlKey&&e.shiftKey&&e.key==='D'){const isDark=!shouldUseDarkMode();console.log(`⌨️ Tastenkombination STRG+SHIFT+D erkannt:Wechsel zu ${isDark?'🌙 dunkel':'☀️ hell'}`);setDarkMode(isDark);e.preventDefault();}});const darkModeMediaQuery=window.matchMedia('(prefers-color-scheme: dark)');try{darkModeMediaQuery.addEventListener('change',function(e){if(localStorage.getItem(STORAGE_KEY)===null){console.log(`🖥️ Systemeinstellung geändert:${e.matches?'🌙 dunkel':'☀️ hell'}`);setDarkMode(e.matches);}});}catch(error){darkModeMediaQuery.addListener(function(e){if(localStorage.getItem(STORAGE_KEY)===null){console.log(`🖥️ Systemeinstellung geändert(Legacy-Browser):${e.matches?'🌙 dunkel':'☀️ hell'}`);setDarkMode(e.matches);}});} +const initialState=shouldUseDarkMode();console.log(`🔍 Ermittelter Ausgangszustand:${initialState?'🌙 Dark Mode':'☀️ Light Mode'}`);setDarkMode(initialState);const animClass=initialState?'dark-mode-transition':'light-mode-transition';document.body.classList.add(animClass);setTimeout(()=>{document.body.classList.remove(animClass);},300);console.log('🚀 Dark Mode Handler erfolgreich initialisiert');} +function createDarkModeToggle(){const header=document.querySelector('header');const nav=document.querySelector('nav');const container=document.querySelector('.dark-mode-container')||header||nav;if(!container){console.error('⚠️ Kein geeigneter Container für Dark Mode Toggle gefunden');return;} +darkModeToggle=document.createElement('button');darkModeToggle.id='darkModeToggle';darkModeToggle.className='dark-mode-toggle-new';darkModeToggle.setAttribute('aria-label','Dark Mode umschalten');darkModeToggle.setAttribute('title','Dark Mode aktivieren');darkModeToggle.setAttribute('data-action','toggle-dark-mode');const sunIcon=document.createElementNS("http://www.w3.org/2000/svg","svg");sunIcon.setAttribute("class","w-5 h-5 sm:w-5 sm:h-5 sun-icon");sunIcon.setAttribute("fill","none");sunIcon.setAttribute("stroke","currentColor");sunIcon.setAttribute("viewBox","0 0 24 24");sunIcon.setAttribute("aria-hidden","true");const sunPath=document.createElementNS("http://www.w3.org/2000/svg","path");sunPath.setAttribute("stroke-linecap","round");sunPath.setAttribute("stroke-linejoin","round");sunPath.setAttribute("stroke-width","2");sunPath.setAttribute("d","M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z");const moonIcon=document.createElementNS("http://www.w3.org/2000/svg","svg");moonIcon.setAttribute("class","w-5 h-5 sm:w-5 sm:h-5 moon-icon hidden");moonIcon.setAttribute("fill","none");moonIcon.setAttribute("stroke","currentColor");moonIcon.setAttribute("viewBox","0 0 24 24");moonIcon.setAttribute("aria-hidden","true");const moonPath=document.createElementNS("http://www.w3.org/2000/svg","path");moonPath.setAttribute("stroke-linecap","round");moonPath.setAttribute("stroke-linejoin","round");moonPath.setAttribute("stroke-width","2");moonPath.setAttribute("d","M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z");sunIcon.appendChild(sunPath);moonIcon.appendChild(moonPath);darkModeToggle.appendChild(sunIcon);darkModeToggle.appendChild(moonIcon);container.appendChild(darkModeToggle);console.log('✅ Dark Mode Toggle Button erfolgreich erstellt und zur Benutzeroberfläche hinzugefügt');} +const isDark=shouldUseDarkMode();console.log(`🏃‍♂️ Sofortige Anwendung:${isDark?'🌙 Dark Mode':'☀️ Light Mode'}(vor DOM-Ladung)`);setDarkMode(isDark);})();if(!document.querySelector('style#dark-mode-animations')){const styleTag=document.createElement('style');styleTag.id='dark-mode-animations';styleTag.textContent=`@keyframes spin-once{from{transform:rotate(0deg);} +to{transform:rotate(360deg);}}.animate-spin-once{animation:spin-once 0.3s ease-in-out;}`;document.head.appendChild(styleTag);console.log('💫 Animations-Styles für Dark Mode Toggle hinzugefügt');} \ No newline at end of file diff --git a/backend/static/js/dark-mode.min.js.gz b/backend/static/js/dark-mode.min.js.gz new file mode 100644 index 00000000..45035cd5 Binary files /dev/null and b/backend/static/js/dark-mode.min.js.gz differ diff --git a/backend/static/js/dashboard.js.gz b/backend/static/js/dashboard.js.gz new file mode 100644 index 00000000..ae16b517 Binary files /dev/null and b/backend/static/js/dashboard.js.gz differ diff --git a/backend/static/js/dashboard.min.js b/backend/static/js/dashboard.min.js new file mode 100644 index 00000000..378a2618 --- /dev/null +++ b/backend/static/js/dashboard.min.js @@ -0,0 +1,32 @@ +let dashboardData={};let updateInterval;const elements={activeJobs:null,scheduledJobs:null,availablePrinters:null,totalPrintTime:null,schedulerStatus:null,recentJobsList:null,recentActivitiesList:null,refreshBtn:null,schedulerToggleBtn:null};document.addEventListener('DOMContentLoaded',function(){initializeDashboard();});function initializeDashboard(){elements.activeJobs=document.getElementById('active-jobs');elements.scheduledJobs=document.getElementById('scheduled-jobs');elements.availablePrinters=document.getElementById('available-printers');elements.totalPrintTime=document.getElementById('total-print-time');elements.schedulerStatus=document.getElementById('scheduler-status');elements.recentJobsList=document.getElementById('recent-jobs-list');elements.recentActivitiesList=document.getElementById('recent-activities-list');elements.refreshBtn=document.getElementById('refresh-btn');elements.schedulerToggleBtn=document.getElementById('scheduler-toggle-btn');if(elements.refreshBtn){elements.refreshBtn.addEventListener('click',refreshDashboard);} +if(elements.schedulerToggleBtn){elements.schedulerToggleBtn.addEventListener('click',toggleScheduler);} +loadDashboardData();loadRecentJobs();loadRecentActivities();loadSchedulerStatus();updateInterval=setInterval(function(){loadDashboardData();loadRecentJobs();loadRecentActivities();loadSchedulerStatus();},30000);} +async function loadDashboardData(){try{const response=await fetch('/api/dashboard');if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +dashboardData=await response.json();updateDashboardUI();}catch(error){console.error('Fehler beim Laden der Dashboard-Daten:',error);showError('Fehler beim Laden der Dashboard-Daten');}} +function updateDashboardUI(){if(elements.activeJobs){elements.activeJobs.textContent=dashboardData.active_jobs||0;} +if(elements.scheduledJobs){elements.scheduledJobs.textContent=dashboardData.scheduled_jobs||0;} +if(elements.availablePrinters){elements.availablePrinters.textContent=dashboardData.available_printers||0;} +if(elements.totalPrintTime){const hours=Math.floor((dashboardData.total_print_time||0)/3600);const minutes=Math.floor(((dashboardData.total_print_time||0)%3600)/60);elements.totalPrintTime.textContent=`${hours}h ${minutes}m`;}} +async function loadRecentJobs(){try{const response=await fetch('/api/jobs/recent');if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const data=await response.json();updateRecentJobsList(data.jobs);}catch(error){console.error('Fehler beim Laden der aktuellen Jobs:',error);if(elements.recentJobsList){elements.recentJobsList.innerHTML='
  • Fehler beim Laden
  • ';}}} +function updateRecentJobsList(jobs){if(!elements.recentJobsList)return;if(!jobs||jobs.length===0){elements.recentJobsList.innerHTML='
  • Keine aktuellen Jobs
  • ';return;} +const jobsHtml=jobs.map(job=>{const statusClass=getStatusClass(job.status);const timeAgo=formatTimeAgo(job.created_at);return`
  • ${escapeHtml(job.name)}
    ${escapeHtml(job.printer_name)}• ${timeAgo}
    ${getStatusText(job.status)}
  • `;}).join('');elements.recentJobsList.innerHTML=jobsHtml;} +async function loadRecentActivities(){try{const response=await fetch('/api/activity/recent');if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const data=await response.json();updateRecentActivitiesList(data.activities);}catch(error){console.error('Fehler beim Laden der Aktivitäten:',error);if(elements.recentActivitiesList){elements.recentActivitiesList.innerHTML='
  • Fehler beim Laden
  • ';}}} +function updateRecentActivitiesList(activities){if(!elements.recentActivitiesList)return;if(!activities||activities.length===0){elements.recentActivitiesList.innerHTML='
  • Keine aktuellen Aktivitäten
  • ';return;} +const activitiesHtml=activities.map(activity=>{const timeAgo=formatTimeAgo(activity.timestamp);return`
  • ${escapeHtml(activity.description)}
    ${timeAgo}
  • `;}).join('');elements.recentActivitiesList.innerHTML=activitiesHtml;} +async function loadSchedulerStatus(){try{const response=await fetch('/api/scheduler/status');if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const data=await response.json();updateSchedulerStatus(data.running);}catch(error){console.error('Fehler beim Laden des Scheduler-Status:',error);if(elements.schedulerStatus){elements.schedulerStatus.innerHTML='Unbekannt';}}} +function updateSchedulerStatus(isRunning){if(!elements.schedulerStatus)return;const statusClass=isRunning?'bg-success':'bg-danger';const statusText=isRunning?'Aktiv':'Gestoppt';elements.schedulerStatus.innerHTML=`${statusText}`;if(elements.schedulerToggleBtn){elements.schedulerToggleBtn.textContent=isRunning?'Scheduler stoppen':'Scheduler starten';elements.schedulerToggleBtn.className=isRunning?'btn btn-danger btn-sm':'btn btn-success btn-sm';}} +async function toggleScheduler(){try{const isRunning=dashboardData.scheduler_running;const endpoint=isRunning?'/api/scheduler/stop':'/api/scheduler/start';const response=await fetch(endpoint,{method:'POST',headers:{'Content-Type':'application/json'}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const result=await response.json();if(result.success){showSuccess(result.message);setTimeout(loadSchedulerStatus,1000);}else{showError(result.error||'Unbekannter Fehler');}}catch(error){console.error('Fehler beim Umschalten des Schedulers:',error);showError('Fehler beim Umschalten des Schedulers');}} +function refreshDashboard(){if(elements.refreshBtn){elements.refreshBtn.disabled=true;elements.refreshBtn.innerHTML=' Aktualisiere...';} +Promise.all([loadDashboardData(),loadRecentJobs(),loadRecentActivities(),loadSchedulerStatus()]).finally(()=>{if(elements.refreshBtn){elements.refreshBtn.disabled=false;elements.refreshBtn.innerHTML=' Aktualisieren';}});} +function getStatusClass(status){const statusClasses={'pending':'bg-warning','printing':'bg-primary','completed':'bg-success','failed':'bg-danger','cancelled':'bg-secondary','scheduled':'bg-info'};return statusClasses[status]||'bg-secondary';} +function getStatusText(status){const statusTexts={'pending':'Wartend','printing':'Druckt','completed':'Abgeschlossen','failed':'Fehlgeschlagen','cancelled':'Abgebrochen','scheduled':'Geplant'};return statusTexts[status]||status;} +function formatTimeAgo(timestamp){const now=new Date();const time=new Date(timestamp);const diffMs=now-time;const diffMins=Math.floor(diffMs/60000);const diffHours=Math.floor(diffMins/60);const diffDays=Math.floor(diffHours/24);if(diffMins<1)return'Gerade eben';if(diffMins<60)return`vor ${diffMins}Min`;if(diffHours<24)return`vor ${diffHours}Std`;return`vor ${diffDays}Tag${diffDays>1?'en':''}`;} +function escapeHtml(text){const div=document.createElement('div');div.textContent=text;return div.innerHTML;} +function showSuccess(message){showNotification(message,'success');} +function showError(message){showNotification(message,'danger');} +function showNotification(message,type){const alertDiv=document.createElement('div');alertDiv.className=`alert alert-${type}alert-dismissible fade show position-fixed`;alertDiv.style.top='20px';alertDiv.style.right='20px';alertDiv.style.zIndex='9999';alertDiv.innerHTML=`${escapeHtml(message)}`;document.body.appendChild(alertDiv);setTimeout(()=>{if(alertDiv.parentNode){alertDiv.parentNode.removeChild(alertDiv);}},5000);} +window.addEventListener('beforeunload',function(){if(updateInterval){clearInterval(updateInterval);}}); \ No newline at end of file diff --git a/backend/static/js/dashboard.min.js.gz b/backend/static/js/dashboard.min.js.gz new file mode 100644 index 00000000..f6d9a36e Binary files /dev/null and b/backend/static/js/dashboard.min.js.gz differ diff --git a/backend/static/js/debug-fix.js.gz b/backend/static/js/debug-fix.js.gz new file mode 100644 index 00000000..0181b9a3 Binary files /dev/null and b/backend/static/js/debug-fix.js.gz differ diff --git a/backend/static/js/debug-fix.min.js b/backend/static/js/debug-fix.min.js new file mode 100644 index 00000000..0fe45c57 --- /dev/null +++ b/backend/static/js/debug-fix.min.js @@ -0,0 +1,10 @@ +(function(){'use strict';console.log('🔧 Debug Fix Script wird geladen...');window.MYP=window.MYP||{};window.MYP.UI=window.MYP.UI||{};window.MVP=window.MVP||{};window.MVP.UI=window.MVP.UI||{};window.MVP.UI.DarkModeManager=function(){console.log('⚠️ MVP.UI.DarkModeManager Konstruktor aufgerufen - verwende MYP.UI.darkMode stattdessen');if(window.MYP&&window.MYP.UI&&window.MYP.UI.darkMode){return window.MYP.UI.darkMode;} +return{init:function(){console.log('DarkModeManager Fallback init');},setDarkMode:function(){console.log('DarkModeManager Fallback setDarkMode');},isDarkMode:function(){return false;}};};document.addEventListener('DOMContentLoaded',function(){console.log('🚀 Debug Fix: DOM Content geladen');setTimeout(()=>{try{if(window.MYP&&window.MYP.UI&&window.MYP.UI.darkMode){window.MVP.UI.DarkModeManager=function(){console.log('⚠️ MVP.UI.DarkModeManager Konstruktor aufgerufen - verwende MYP.UI.darkMode stattdessen');return window.MYP.UI.darkMode;};console.log('✅ MVP.UI.DarkModeManager Alias aktualisiert');} +if(!window.jobManager&&window.JobManager){window.jobManager=new window.JobManager();console.log('✅ JobManager Instanz erstellt');} +if(window.jobManager&&!window.jobManager.setupFormHandlers){window.jobManager.setupFormHandlers=function(){console.log('✅ setupFormHandlers Fallback aufgerufen');};} +window.refreshJobs=function(){if(window.jobManager&&window.jobManager.loadJobs){return window.jobManager.loadJobs();}else{console.warn('⚠️ JobManager nicht verfügbar - Seite wird neu geladen');window.location.reload();}};window.startJob=function(jobId){if(window.jobManager&&window.jobManager.startJob){return window.jobManager.startJob(jobId);}};window.pauseJob=function(jobId){if(window.jobManager&&window.jobManager.pauseJob){return window.jobManager.pauseJob(jobId);}};window.resumeJob=function(jobId){if(window.jobManager&&window.jobManager.resumeJob){return window.jobManager.resumeJob(jobId);}};window.deleteJob=function(jobId){if(window.jobManager&&window.jobManager.deleteJob){return window.jobManager.deleteJob(jobId);}};console.log('✅ Debug Fix Script erfolgreich angewendet');}catch(error){console.error('❌ Debug Fix Fehler:',error);}},100);});window.addEventListener('error',function(e){const errorInfo={message:e.message||'Unbekannter Fehler',filename:e.filename||'Unbekannte Datei',lineno:e.lineno||0,colno:e.colno||0,stack:e.error?e.error.stack:'Stack nicht verfügbar',type:e.error?e.error.constructor.name:'Unbekannter Typ'};console.error('🐛 JavaScript Error abgefangen:',JSON.stringify(errorInfo,null,2));if(e.message.includes('MVP.UI.DarkModeManager is not a constructor')){console.log('🔧 DarkModeManager Fehler erkannt - verwende MYP.UI.darkMode');e.preventDefault();return false;} +if(e.message.includes('setupFormHandlers is not a function')){console.log('🔧 setupFormHandlers Fehler erkannt - verwende Fallback');e.preventDefault();return false;} +if(e.message.includes('refreshStats is not defined')){console.log('🔧 refreshStats Fehler erkannt - lade global-refresh-functions.js');const script=document.createElement('script');script.src='/static/js/global-refresh-functions.js';script.onload=function(){console.log('✅ global-refresh-functions.js nachgeladen');};document.head.appendChild(script);e.preventDefault();return false;} +if(e.message.includes('Cannot read properties of undefined')){console.log('🔧 Undefined Properties Fehler erkannt - ignoriert für Stabilität');e.preventDefault();return false;} +if(e.message.includes('jobManager')||e.message.includes('JobManager')){console.log('🔧 JobManager Fehler erkannt - verwende Fallback');e.preventDefault();return false;} +if(e.message.includes('showToast is not defined')){console.log('🔧 showToast Fehler erkannt - verwende Fallback');e.preventDefault();return false;}});window.addEventListener('unhandledrejection',function(e){console.error('🐛 Promise Rejection abgefangen:',e.reason);if(e.reason&&e.reason.message&&e.reason.message.includes('Jobs')){console.log('🔧 Jobs-bezogener Promise Fehler - ignoriert');e.preventDefault();}});console.log('✅ Debug Fix Script bereit');})(); \ No newline at end of file diff --git a/backend/static/js/debug-fix.min.js.gz b/backend/static/js/debug-fix.min.js.gz new file mode 100644 index 00000000..213a44a9 Binary files /dev/null and b/backend/static/js/debug-fix.min.js.gz differ diff --git a/backend/static/js/event-handlers.js.gz b/backend/static/js/event-handlers.js.gz new file mode 100644 index 00000000..a8fde9ac Binary files /dev/null and b/backend/static/js/event-handlers.js.gz differ diff --git a/backend/static/js/event-handlers.min.js b/backend/static/js/event-handlers.min.js new file mode 100644 index 00000000..491bb217 --- /dev/null +++ b/backend/static/js/event-handlers.min.js @@ -0,0 +1,32 @@ +class GlobalEventManager{constructor(){this.init();} +init(){document.addEventListener('click',this.handleClick.bind(this));document.addEventListener('DOMContentLoaded',this.setupEventListeners.bind(this));if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',this.setupEventListeners.bind(this));}else{this.setupEventListeners();}} +handleClick(event){const target=event.target.closest('[data-action]');if(!target)return;const action=target.getAttribute('data-action');const params=this.parseActionParams(target);event.preventDefault();this.executeAction(action,params,target);} +parseActionParams(element){const params={};for(const attr of element.attributes){if(attr.name.startsWith('data-action-')){const key=attr.name.replace('data-action-','');params[key]=attr.value;}} +return params;} +executeAction(action,params,element){console.log(`🎯 Führe Action aus:${action}`,params);switch(action){case'refresh-dashboard':if(typeof refreshDashboard==='function')refreshDashboard();break;case'logout':if(typeof handleLogout==='function')handleLogout();break;case'go-back':window.history.back();break;case'reload-page':window.location.reload();break;case'print-page':window.print();break;case'refresh-jobs':if(typeof refreshJobs==='function')refreshJobs();break;case'toggle-batch-mode':if(typeof toggleBatchMode==='function')toggleBatchMode();break;case'start-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.startJob(params.id);} +break;case'pause-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.pauseJob(params.id);} +break;case'resume-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.resumeJob(params.id);} +break;case'delete-job':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.deleteJob(params.id);} +break;case'open-job-details':if(typeof jobManager!=='undefined'&¶ms.id){jobManager.openJobDetails(params.id);} +break;case'refresh-printers':if(typeof refreshPrinters==='function')refreshPrinters();break;case'toggle-maintenance-mode':if(typeof toggleMaintenanceMode==='function')toggleMaintenanceMode();break;case'open-add-printer-modal':if(typeof openAddPrinterModal==='function')openAddPrinterModal();break;case'toggle-auto-refresh':if(typeof toggleAutoRefresh==='function')toggleAutoRefresh();break;case'clear-all-filters':if(typeof clearAllFilters==='function')clearAllFilters();break;case'test-printer-connection':if(typeof testPrinterConnection==='function')testPrinterConnection();break;case'delete-printer':if(typeof deletePrinter==='function')deletePrinter();break;case'edit-printer':if(typeof printerManager!=='undefined'&¶ms.id){printerManager.editPrinter(params.id);} +break;case'connect-printer':if(typeof printerManager!=='undefined'&¶ms.id){printerManager.connectPrinter(params.id);} +break;case'refresh-calendar':if(typeof refreshCalendar==='function')refreshCalendar();break;case'toggle-auto-optimization':if(typeof toggleAutoOptimization==='function')toggleAutoOptimization();break;case'export-calendar':if(typeof exportCalendar==='function')exportCalendar();break;case'open-create-event-modal':if(typeof openCreateEventModal==='function')openCreateEventModal();break;case'close-modal':this.closeModal(params.target||element.closest('.fixed'));break;case'close-printer-modal':if(typeof closePrinterModal==='function')closePrinterModal();break;case'close-job-modal':if(typeof closeJobModal==='function')closeJobModal();break;case'close-event-modal':if(typeof closeEventModal==='function')closeEventModal();break;case'reset-form':if(typeof resetForm==='function')resetForm();else this.resetNearestForm(element);break;case'clear-file':if(typeof clearFile==='function')clearFile();break;case'check-status':if(typeof checkStatus==='function')checkStatus();break;case'copy-code':if(typeof copyCode==='function')copyCode();break;case'refresh-status':if(typeof refreshStatus==='function')refreshStatus();break;case'show-status-check':if(typeof showStatusCheck==='function')showStatusCheck();break;case'perform-bulk-action':if(typeof performBulkAction==='function'&¶ms.type){performBulkAction(params.type);} +break;case'close-bulk-modal':if(typeof closeBulkModal==='function')closeBulkModal();break;case'clear-cache':if(typeof clearCache==='function')clearCache();break;case'optimize-database':if(typeof optimizeDatabase==='function')optimizeDatabase();break;case'create-backup':if(typeof createBackup==='function')createBackup();break;case'download-logs':if(typeof downloadLogs==='function')downloadLogs();break;case'run-maintenance':if(typeof runMaintenance==='function')runMaintenance();break;case'save-settings':if(typeof saveSettings==='function')saveSettings();break;case'toggle-edit-mode':if(typeof toggleEditMode==='function')toggleEditMode();break;case'trigger-avatar-upload':if(typeof triggerAvatarUpload==='function')triggerAvatarUpload();break;case'cancel-edit':if(typeof cancelEdit==='function')cancelEdit();break;case'download-user-data':if(typeof downloadUserData==='function')downloadUserData();break;case'refresh-stats':if(typeof refreshStats==='function')refreshStats();break;case'export-stats':if(typeof exportStats==='function')exportStats();break;case'remove-element':const targetElement=params.target?document.querySelector(params.target):element.closest(params.selector||'.removable');if(targetElement){targetElement.remove();} +break;case'toggle-element':const toggleTarget=params.target?document.querySelector(params.target):element.nextElementSibling;if(toggleTarget){toggleTarget.classList.toggle('hidden');} +break;case'show-element':const showTarget=document.querySelector(params.target);if(showTarget){showTarget.classList.remove('hidden');} +break;case'hide-element':const hideTarget=document.querySelector(params.target);if(hideTarget){hideTarget.classList.add('hidden');} +break;default:console.warn(`⚠️ Unbekannte Action:${action}`);if(typeof window[action]==='function'){window[action](params);} +break;}} +closeModal(modalElement){if(modalElement){modalElement.classList.add('hidden');modalElement.remove();}} +resetNearestForm(element){const form=element.closest('form');if(form){form.reset();}} +setupEventListeners(){this.setupAutoRefresh();this.setupKeyboardShortcuts();this.setupFormValidation();console.log('🔧 Globale Event-Handler initialisiert');} +setupAutoRefresh(){const currentPath=window.location.pathname;if(currentPath.includes('/dashboard')){setInterval(()=>{if(typeof refreshDashboard==='function'){refreshDashboard();}},30000);} +if(currentPath.includes('/jobs')){setInterval(()=>{if(typeof refreshJobs==='function'){refreshJobs();}},15000);}} +setupKeyboardShortcuts(){document.addEventListener('keydown',(event)=>{if(event.key==='Escape'){const openModal=document.querySelector('.fixed:not(.hidden)');if(openModal){this.closeModal(openModal);}} +if(event.ctrlKey&&event.key==='r'){event.preventDefault();const currentPath=window.location.pathname;if(currentPath.includes('/dashboard')&&typeof refreshDashboard==='function'){refreshDashboard();}else if(currentPath.includes('/jobs')&&typeof refreshJobs==='function'){refreshJobs();}else if(currentPath.includes('/printers')&&typeof refreshPrinters==='function'){refreshPrinters();}else{window.location.reload();}}});} +setupFormValidation(){const forms=document.querySelectorAll('form[data-validate]');forms.forEach(form=>{form.addEventListener('submit',this.validateForm.bind(this));});} +validateForm(event){const form=event.target;const requiredFields=form.querySelectorAll('[required]');let isValid=true;requiredFields.forEach(field=>{if(!field.value.trim()){isValid=false;field.classList.add('border-red-500');const errorId=`${field.id}-error`;let errorElement=document.getElementById(errorId);if(!errorElement){errorElement=document.createElement('div');errorElement.id=errorId;errorElement.className='text-red-500 text-sm mt-1';field.parentNode.appendChild(errorElement);} +errorElement.textContent=`${field.getAttribute('data-label')||'Dieses Feld'}ist erforderlich.`;}else{field.classList.remove('border-red-500');const errorElement=document.getElementById(`${field.id}-error`);if(errorElement){errorElement.remove();}}});if(!isValid){event.preventDefault();} +return isValid;}} +const globalEventManager=new GlobalEventManager();if(typeof module!=='undefined'&&module.exports){module.exports=GlobalEventManager;} +console.log('🌍 Globaler Event Manager geladen'); \ No newline at end of file diff --git a/backend/static/js/event-handlers.min.js.gz b/backend/static/js/event-handlers.min.js.gz new file mode 100644 index 00000000..c5f15073 Binary files /dev/null and b/backend/static/js/event-handlers.min.js.gz differ diff --git a/backend/static/js/fullcalendar/core.min.js.gz b/backend/static/js/fullcalendar/core.min.js.gz new file mode 100644 index 00000000..e0d4cc1e Binary files /dev/null and b/backend/static/js/fullcalendar/core.min.js.gz differ diff --git a/backend/static/js/fullcalendar/daygrid.min.js.gz b/backend/static/js/fullcalendar/daygrid.min.js.gz new file mode 100644 index 00000000..5cc81258 Binary files /dev/null and b/backend/static/js/fullcalendar/daygrid.min.js.gz differ diff --git a/backend/static/js/fullcalendar/interaction.min.js.gz b/backend/static/js/fullcalendar/interaction.min.js.gz new file mode 100644 index 00000000..e52c629f Binary files /dev/null and b/backend/static/js/fullcalendar/interaction.min.js.gz differ diff --git a/backend/static/js/fullcalendar/list.min.js.gz b/backend/static/js/fullcalendar/list.min.js.gz new file mode 100644 index 00000000..f5a79107 Binary files /dev/null and b/backend/static/js/fullcalendar/list.min.js.gz differ diff --git a/backend/static/js/fullcalendar/main.min.css.gz b/backend/static/js/fullcalendar/main.min.css.gz new file mode 100644 index 00000000..b0c586b6 Binary files /dev/null and b/backend/static/js/fullcalendar/main.min.css.gz differ diff --git a/backend/static/js/fullcalendar/timegrid.min.js.gz b/backend/static/js/fullcalendar/timegrid.min.js.gz new file mode 100644 index 00000000..4702ea40 Binary files /dev/null and b/backend/static/js/fullcalendar/timegrid.min.js.gz differ diff --git a/backend/static/js/glassmorphism-notifications.js.gz b/backend/static/js/glassmorphism-notifications.js.gz new file mode 100644 index 00000000..27d4bc50 Binary files /dev/null and b/backend/static/js/glassmorphism-notifications.js.gz differ diff --git a/backend/static/js/glassmorphism-notifications.min.js b/backend/static/js/glassmorphism-notifications.min.js new file mode 100644 index 00000000..0000458c --- /dev/null +++ b/backend/static/js/glassmorphism-notifications.min.js @@ -0,0 +1,80 @@ +class GlassmorphismNotificationSystem{constructor(){this.notifications=new Map();this.toastCounter=0;this.soundEnabled=localStorage.getItem('myp-notification-sound')!=='false';this.animationsEnabled=!window.matchMedia('(prefers-reduced-motion: reduce)').matches;this.actionCallbacks=new Map();this.callbackCounter=0;this.init();this.setupGlobalFunctions();this.injectStyles();} +init(){this.createToastContainer();this.setupEventListeners();console.log('🎨 Glassmorphism Notification System initialisiert');} +createToastContainer(){if(!document.getElementById('glassmorphism-toast-container')){const container=document.createElement('div');container.id='glassmorphism-toast-container';container.className='notifications-container';document.body.appendChild(container);}} +setupGlobalFunctions(){window.showFlashMessage=this.showToast.bind(this);window.showToast=this.showToast.bind(this);window.showNotification=this.showToast.bind(this);window.showSuccessMessage=(msg,duration)=>this.showToast(msg,'success',duration);window.showErrorMessage=(msg,duration)=>this.showToast(msg,'error',duration);window.showWarningMessage=(msg,duration)=>this.showToast(msg,'warning',duration);window.showInfoMessage=(msg,duration)=>this.showToast(msg,'info',duration);window.showSuccessNotification=(msg)=>this.showToast(msg,'success');window.showPersistentAlert=this.showPersistentAlert.bind(this);window.showConfirmationToast=this.showConfirmationToast.bind(this);window.showProgressToast=this.showProgressToast.bind(this);window.executeNotificationCallback=this.executeCallback.bind(this);} +setupEventListeners(){document.addEventListener('keydown',(e)=>{if(e.key==='Escape'){this.closeAllToasts();} +if((e.ctrlKey||e.metaKey)&&e.shiftKey&&e.key==='N'){this.showNotificationSettings();}});window.addEventListener('orientationchange',()=>{setTimeout(()=>this.repositionAllToasts(),200);});} +showToast(message,type='info',duration=5000,options={}){if(window.dndManager?.isEnabled){window.dndManager.suppressNotification(message,type);return null;} +const toastId=`glass-toast-${++this.toastCounter}`;const toast=this.createToastElement(toastId,message,type,duration,options);this.addToContainer(toast);this.animateIn(toast);this.scheduleRemoval(toastId,duration,options.persistent);if(this.soundEnabled&&options.playSound!==false){this.playNotificationSound(type);} +if(options.browserNotification&&'Notification'in window){this.showBrowserNotification(message,type,options);} +return toastId;} +createToastElement(toastId,message,type,duration,options){const toast=document.createElement('div');toast.id=toastId;toast.className=`glassmorphism-toast notification notification-${type}`;toast.setAttribute('role','alert');toast.setAttribute('aria-live','polite');toast.setAttribute('aria-atomic','true');const iconSvg=this.getIconSvg(type);const progressBar=duration>0&&!options.persistent?this.createProgressBar(duration):'';toast.innerHTML=`
    ${iconSvg}
    ${options.title?`
    ${options.title}
    `:''}
    ${message}
    ${options.actions?this.createActionButtons(options.actions,toastId):''}
    ${progressBar}
    `;if(duration>0&&!options.persistent){this.setupHoverEvents(toast,toastId);} +this.notifications.set(toastId,{element:toast,type,message,timestamp:Date.now(),options});return toast;} +createProgressBar(duration){return`
    `;} +createActionButtons(actions,toastId){return actions.map(action=>{let callbackId='';if(action.callback&&typeof action.callback==='function'){callbackId=`callback-${++this.callbackCounter}`;this.actionCallbacks.set(callbackId,action.callback);} +return``;}).join('');} +getIconSvg(type){const icons={success:``,error:``,warning:``,info:``,loading:``};return icons[type]||icons.info;} +addToContainer(toast){const container=document.getElementById('glassmorphism-toast-container');if(container){container.appendChild(toast);this.repositionAllToasts();}} +animateIn(toast){if(!this.animationsEnabled){toast.classList.add('show');return;} +toast.style.transform='translateX(120%) scale(0.8) rotateY(15deg)';toast.style.opacity='0';toast.style.filter='blur(8px)';requestAnimationFrame(()=>{toast.style.transition='all 0.8s cubic-bezier(0.34, 1.56, 0.64, 1)';toast.style.transform='translateX(0) scale(1) rotateY(0deg)';toast.style.opacity='1';toast.style.filter='blur(0px)';toast.classList.add('show');});} +setupHoverEvents(toast,toastId){let timeoutId;let isPaused=false;const notification=this.notifications.get(toastId);if(!notification)return;const pauseTimer=()=>{isPaused=true;clearTimeout(timeoutId);const progressBar=toast.querySelector('.toast-progress-bar');if(progressBar){progressBar.style.animationPlayState='paused';} +toast.style.transform='translateY(-4px) scale(1.03)';toast.style.filter='brightness(1.1) saturate(1.1)';};const resumeTimer=()=>{isPaused=false;const progressBar=toast.querySelector('.toast-progress-bar');if(progressBar){progressBar.style.animationPlayState='running';} +toast.style.transform='translateY(0) scale(1)';toast.style.filter='brightness(1) saturate(1)';};toast.addEventListener('mouseenter',pauseTimer);toast.addEventListener('mouseleave',resumeTimer);toast.addEventListener('focus',pauseTimer);toast.addEventListener('blur',resumeTimer);} +scheduleRemoval(toastId,duration,persistent=false){if(persistent||duration<=0)return;setTimeout(()=>{this.closeToast(toastId);},duration);} +closeToast(toastId){const notification=this.notifications.get(toastId);if(!notification)return;const toast=notification.element;if(this.animationsEnabled){toast.style.transition='all 0.6s cubic-bezier(0.4, 0, 1, 1)';toast.style.transform='translateX(120%) scale(0.8) rotateY(-15deg)';toast.style.opacity='0';toast.style.filter='blur(4px)';setTimeout(()=>{this.removeToast(toastId);},600);}else{this.removeToast(toastId);}} +removeToast(toastId){const notification=this.notifications.get(toastId);if(!notification)return;if(notification.element.parentNode){notification.element.parentNode.removeChild(notification.element);} +this.notifications.delete(toastId);this.actionCallbacks.forEach((callback,callbackId)=>{if(callbackId.includes(toastId)){this.actionCallbacks.delete(callbackId);}});this.repositionAllToasts();} +repositionAllToasts(){const container=document.getElementById('glassmorphism-toast-container');if(!container)return;const toasts=Array.from(container.children);toasts.forEach((toast,index)=>{toast.style.top=`${1+index*3.75}rem`;toast.style.zIndex=1000-index;if(index>0){toast.style.transform=`scale(${1-index*0.015})`;toast.style.opacity=`${1-index*0.08}`;}});} +closeAllToasts(){const toastIds=Array.from(this.notifications.keys());toastIds.forEach((id,index)=>{setTimeout(()=>this.closeToast(id),index*100);});} +showConfirmationToast(message,onConfirm,onCancel=null,options={}){const confirmCallback=()=>{if(typeof onConfirm==='function'){try{onConfirm();}catch(error){console.error('Fehler beim Ausführen der Bestätigungslogik:',error);}}};const cancelCallback=()=>{if(typeof onCancel==='function'){try{onCancel();}catch(error){console.error('Fehler beim Ausführen der Abbruchlogik:',error);}}};const actions=[{text:options.confirmText||'Bestätigen',type:'primary',callback:confirmCallback,closeAfter:true}];if(onCancel||options.cancelText){actions.push({text:options.cancelText||'Abbrechen',type:'secondary',callback:cancelCallback,closeAfter:true});} +return this.showToast(message,'warning',0,{persistent:true,title:options.title||'Bestätigung erforderlich',actions:actions,...options});} +showProgressToast(message,type='info',options={}){const toastId=this.showToast(message,'loading',0,{persistent:true,title:options.title||'Verarbeitung...',...options});return{id:toastId,updateProgress:(percent)=>this.updateProgressToast(toastId,percent),updateMessage:(newMessage)=>this.updateToastMessage(toastId,newMessage),complete:(finalMessage,finalType='success')=>{this.closeToast(toastId);if(finalMessage){this.showToast(finalMessage,finalType,3000);}}};} +updateProgressToast(toastId,percent){const notification=this.notifications.get(toastId);if(!notification)return;let progressBar=notification.element.querySelector('.toast-progress-bar');if(!progressBar){const progressContainer=document.createElement('div');progressContainer.className='toast-progress';progressContainer.innerHTML=`
    `;notification.element.querySelector('.toast-content').appendChild(progressContainer);progressBar=progressContainer.querySelector('.toast-progress-bar');} +progressBar.style.width=`${Math.min(100,Math.max(0,percent))}%`;} +updateToastMessage(toastId,newMessage){const notification=this.notifications.get(toastId);if(!notification)return;const messageEl=notification.element.querySelector('.toast-message');if(messageEl){messageEl.textContent=newMessage;}} +showPersistentAlert(message,type='warning',options={}){return this.showToast(message,type,0,{persistent:true,title:options.title||'Wichtiger Hinweis',actions:[{text:'Verstanden',type:'primary',onClick:''}],...options});} +async showBrowserNotification(message,type,options={}){if(!('Notification'in window))return null;if(Notification.permission==='granted'){return new Notification(options.title||'MYP Platform',{body:message,icon:'/static/icons/notification-icon.png',badge:'/static/icons/badge-icon.png',tag:`myp-${type}`,...options.browserOptions});}else if(Notification.permission==='default'){const permission=await Notification.requestPermission();if(permission==='granted'){return this.showBrowserNotification(message,type,options);}} +return null;} +playNotificationSound(type){if(!this.soundEnabled)return;try{const audioContext=new(window.AudioContext||window.webkitAudioContext)();const frequencies={success:[523.25,659.25,783.99,880],error:[440,370,311],warning:[493.88,587.33,659.25],info:[523.25,659.25],loading:[392,440,493.88,523.25]};const freq=frequencies[type]||frequencies.info;freq.forEach((f,i)=>{setTimeout(()=>{const oscillator=audioContext.createOscillator();const gainNode=audioContext.createGain();const filterNode=audioContext.createBiquadFilter();oscillator.connect(filterNode);filterNode.connect(gainNode);gainNode.connect(audioContext.destination);filterNode.type='lowpass';filterNode.frequency.setValueAtTime(2000,audioContext.currentTime);filterNode.Q.setValueAtTime(1,audioContext.currentTime);oscillator.frequency.setValueAtTime(f,audioContext.currentTime);oscillator.type=type==='error'?'triangle':'sine';const baseVolume=type==='error'?0.06:0.08;gainNode.gain.setValueAtTime(0,audioContext.currentTime);gainNode.gain.linearRampToValueAtTime(baseVolume,audioContext.currentTime+0.1);gainNode.gain.exponentialRampToValueAtTime(0.001,audioContext.currentTime+0.3);oscillator.start(audioContext.currentTime);oscillator.stop(audioContext.currentTime+0.3);},i*120);});if(type==='success'){setTimeout(()=>{const reverb=audioContext.createConvolver();const impulse=audioContext.createBuffer(2,audioContext.sampleRate*0.5,audioContext.sampleRate);for(let channel=0;channel

    Benachrichtigungseinstellungen

    `;this.showToast(settingsHTML,'info',0,{persistent:true,title:'Einstellungen',actions:[{text:'Schließen',type:'secondary',onClick:''}]});} +toggleAnimations(){this.animationsEnabled=!this.animationsEnabled;this.showToast(`Animationen ${this.animationsEnabled?'aktiviert':'deaktiviert'}`,'info',2000);} +injectStyles(){if(document.getElementById('glassmorphism-notification-styles'))return;const styles=document.createElement('style');styles.id='glassmorphism-notification-styles';styles.textContent=`.glassmorphism-toast{margin-bottom:0.625rem;transform:translateX(100%);opacity:0;transition:all 0.7s cubic-bezier(0.34,1.56,0.64,1);will-change:transform,opacity,filter;backdrop-filter:blur(50px)saturate(200%)brightness(120%)contrast(110%);-webkit-backdrop-filter:blur(50px)saturate(200%)brightness(120%)contrast(110%);box-shadow:0 32px 64px rgba(0,0,0,0.1),0 16px 32px rgba(0,0,0,0.06),0 6px 12px rgba(0,0,0,0.05),inset 0 2px 0 rgba(255,255,255,0.4),inset 0 1px 2px rgba(255,255,255,0.7),0 0 0 1px rgba(255,255,255,0.18);border-radius:1.5rem;overflow:hidden;position:relative;background:linear-gradient(145deg,rgba(255,255,255,0.12)0%,rgba(255,255,255,0.06)25%,rgba(255,255,255,0.1)50%,rgba(255,255,255,0.05)75%,rgba(255,255,255,0.08)100%);} +.glassmorphism-toast::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at 25%25%,rgba(255,255,255,0.25)0%,transparent 35%),radial-gradient(circle at 75%75%,rgba(255,255,255,0.15)0%,transparent 35%),radial-gradient(circle at 50%10%,rgba(255,255,255,0.1)0%,transparent 40%),linear-gradient(135deg,rgba(255,255,255,0.15)0%,transparent 60%),conic-gradient(from 180deg at 50%50%,rgba(255,255,255,0.05)0deg,rgba(255,255,255,0.1)45deg,rgba(255,255,255,0.05)90deg,rgba(255,255,255,0.08)135deg,rgba(255,255,255,0.05)180deg,rgba(255,255,255,0.12)225deg,rgba(255,255,255,0.05)270deg,rgba(255,255,255,0.08)315deg,rgba(255,255,255,0.05)360deg);pointer-events:none;opacity:0;transition:opacity 0.4s cubic-bezier(0.4,0,0.2,1);animation:subtle-shimmer 8s ease-in-out infinite;} +@keyframes subtle-shimmer{0%,100%{opacity:0;transform:scale(1)rotate(0deg);} +25%{opacity:0.3;transform:scale(1.01)rotate(1deg);} +50%{opacity:0.6;transform:scale(1.02)rotate(0deg);} +75%{opacity:0.3;transform:scale(1.01)rotate(-1deg);}}.glassmorphism-toast:hover::before{opacity:1;animation-play-state:paused;} +.glassmorphism-toast::after{content:'';position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(circle at 20%20%,rgba(255,255,255,0.3)1px,transparent 1px),radial-gradient(circle at 60%80%,rgba(255,255,255,0.2)1px,transparent 1px),radial-gradient(circle at 80%30%,rgba(255,255,255,0.25)1px,transparent 1px),radial-gradient(circle at 30%70%,rgba(255,255,255,0.15)1px,transparent 1px);opacity:0;transition:opacity 0.6s ease;animation:floating-particles 12s linear infinite;pointer-events:none;}@keyframes floating-particles{0%{transform:translate(0,0)rotate(0deg);} +25%{transform:translate(-10px,-10px)rotate(90deg);} +50%{transform:translate(0,-20px)rotate(180deg);} +75%{transform:translate(10px,-10px)rotate(270deg);} +100%{transform:translate(0,0)rotate(360deg);}}.glassmorphism-toast:hover::after{opacity:1;}.glassmorphism-toast.show{transform:translateX(0);opacity:1;} +.glassmorphism-toast.notification-success{background:linear-gradient(145deg,rgba(34,197,94,0.18)0%,rgba(134,239,172,0.12)20%,rgba(16,185,129,0.15)40%,rgba(34,197,94,0.08)60%,rgba(134,239,172,0.1)80%,rgba(16,185,129,0.06)100%);border:1px solid rgba(34,197,94,0.3);box-shadow:0 40px 80px rgba(34,197,94,0.15),0 20px 40px rgba(34,197,94,0.08),0 8px 16px rgba(16,185,129,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(134,239,172,0.4),0 0 0 1px rgba(34,197,94,0.2);color:rgba(5,95,70,0.95);}.dark.glassmorphism-toast.notification-success{color:rgba(134,239,172,0.95);background:linear-gradient(145deg,rgba(34,197,94,0.25)0%,rgba(134,239,172,0.15)30%,rgba(34,197,94,0.12)70%,rgba(16,185,129,0.08)100%);}.glassmorphism-toast.notification-error{background:linear-gradient(145deg,rgba(239,68,68,0.18)0%,rgba(252,165,165,0.12)20%,rgba(220,38,38,0.15)40%,rgba(239,68,68,0.08)60%,rgba(252,165,165,0.1)80%,rgba(220,38,38,0.06)100%);border:1px solid rgba(239,68,68,0.3);box-shadow:0 40px 80px rgba(239,68,68,0.15),0 20px 40px rgba(239,68,68,0.08),0 8px 16px rgba(220,38,38,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(252,165,165,0.4),0 0 0 1px rgba(239,68,68,0.2);color:rgba(153,27,27,0.95);}.dark.glassmorphism-toast.notification-error{color:rgba(252,165,165,0.95);background:linear-gradient(145deg,rgba(239,68,68,0.25)0%,rgba(252,165,165,0.15)30%,rgba(239,68,68,0.12)70%,rgba(220,38,38,0.08)100%);}.glassmorphism-toast.notification-warning{background:linear-gradient(145deg,rgba(245,158,11,0.18)0%,rgba(252,211,77,0.12)20%,rgba(217,119,6,0.15)40%,rgba(245,158,11,0.08)60%,rgba(252,211,77,0.1)80%,rgba(217,119,6,0.06)100%);border:1px solid rgba(245,158,11,0.3);box-shadow:0 40px 80px rgba(245,158,11,0.15),0 20px 40px rgba(245,158,11,0.08),0 8px 16px rgba(217,119,6,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(252,211,77,0.4),0 0 0 1px rgba(245,158,11,0.2);color:rgba(146,64,14,0.95);}.dark.glassmorphism-toast.notification-warning{color:rgba(252,211,77,0.95);background:linear-gradient(145deg,rgba(245,158,11,0.25)0%,rgba(252,211,77,0.15)30%,rgba(245,158,11,0.12)70%,rgba(217,119,6,0.08)100%);}.glassmorphism-toast.notification-info{background:linear-gradient(145deg,rgba(59,130,246,0.18)0%,rgba(147,197,253,0.12)20%,rgba(37,99,235,0.15)40%,rgba(59,130,246,0.08)60%,rgba(147,197,253,0.1)80%,rgba(37,99,235,0.06)100%);border:1px solid rgba(59,130,246,0.3);box-shadow:0 40px 80px rgba(59,130,246,0.15),0 20px 40px rgba(59,130,246,0.08),0 8px 16px rgba(37,99,235,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(147,197,253,0.4),0 0 0 1px rgba(59,130,246,0.2);color:rgba(30,64,175,0.95);}.dark.glassmorphism-toast.notification-info{color:rgba(147,197,253,0.95);background:linear-gradient(145deg,rgba(59,130,246,0.25)0%,rgba(147,197,253,0.15)30%,rgba(59,130,246,0.12)70%,rgba(37,99,235,0.08)100%);}.glassmorphism-toast.notification-loading{background:linear-gradient(145deg,rgba(99,102,241,0.18)0%,rgba(165,180,252,0.12)20%,rgba(79,70,229,0.15)40%,rgba(99,102,241,0.08)60%,rgba(165,180,252,0.1)80%,rgba(79,70,229,0.06)100%);border:1px solid rgba(99,102,241,0.3);box-shadow:0 40px 80px rgba(99,102,241,0.15),0 20px 40px rgba(99,102,241,0.08),0 8px 16px rgba(79,70,229,0.1),inset 0 3px 0 rgba(255,255,255,0.6),inset 0 1px 2px rgba(165,180,252,0.4),0 0 0 1px rgba(99,102,241,0.2);color:rgba(55,48,163,0.95);}.dark.glassmorphism-toast.notification-loading{color:rgba(165,180,252,0.95);background:linear-gradient(145deg,rgba(99,102,241,0.25)0%,rgba(165,180,252,0.15)30%,rgba(99,102,241,0.12)70%,rgba(79,70,229,0.08)100%);} +.dark.glassmorphism-toast{backdrop-filter:blur(80px)saturate(200%)brightness(115%)contrast(125%);-webkit-backdrop-filter:blur(80px)saturate(200%)brightness(115%)contrast(125%);box-shadow:0 40px 80px rgba(0,0,0,0.4),0 20px 40px rgba(0,0,0,0.3),0 8px 16px rgba(0,0,0,0.2),inset 0 3px 0 rgba(255,255,255,0.15),inset 0 1px 2px rgba(255,255,255,0.25),0 0 0 1px rgba(255,255,255,0.08);background:linear-gradient(145deg,rgba(0,0,0,0.25)0%,rgba(15,15,15,0.18)25%,rgba(0,0,0,0.22)50%,rgba(10,10,10,0.15)75%,rgba(0,0,0,0.2)100%);}.dark.glassmorphism-toast::before{background:radial-gradient(circle at 25%25%,rgba(255,255,255,0.12)0%,transparent 35%),radial-gradient(circle at 75%75%,rgba(255,255,255,0.08)0%,transparent 35%),radial-gradient(circle at 50%10%,rgba(255,255,255,0.06)0%,transparent 40%),linear-gradient(135deg,rgba(255,255,255,0.08)0%,transparent 60%),conic-gradient(from 180deg at 50%50%,rgba(255,255,255,0.03)0deg,rgba(255,255,255,0.06)45deg,rgba(255,255,255,0.03)90deg,rgba(255,255,255,0.05)135deg,rgba(255,255,255,0.03)180deg,rgba(255,255,255,0.07)225deg,rgba(255,255,255,0.03)270deg,rgba(255,255,255,0.05)315deg,rgba(255,255,255,0.03)360deg);}.dark.glassmorphism-toast::after{background:radial-gradient(circle at 20%20%,rgba(255,255,255,0.15)1px,transparent 1px),radial-gradient(circle at 60%80%,rgba(255,255,255,0.1)1px,transparent 1px),radial-gradient(circle at 80%30%,rgba(255,255,255,0.12)1px,transparent 1px),radial-gradient(circle at 30%70%,rgba(255,255,255,0.08)1px,transparent 1px);}.toast-content{position:relative;overflow:hidden;padding:1rem;border-radius:inherit;}.toast-header{display:flex;align-items:flex-start;gap:0.875rem;}.toast-icon{flex-shrink:0;width:2.25rem;height:2.25rem;display:flex;align-items:center;justify-content:center;border-radius:50%;background:linear-gradient(145deg,rgba(255,255,255,0.35)0%,rgba(255,255,255,0.2)50%,rgba(255,255,255,0.3)100%);backdrop-filter:blur(16px)saturate(140%);-webkit-backdrop-filter:blur(16px)saturate(140%);border:1px solid rgba(255,255,255,0.4);box-shadow:0 8px 16px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.6),inset 0-1px 0 rgba(0,0,0,0.03);transition:all 0.3s cubic-bezier(0.4,0,0.2,1);position:relative;overflow:hidden;} +.toast-icon::before{content:'';position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(circle,rgba(255,255,255,0.25)0%,transparent 70%);opacity:0;transition:opacity 0.3s ease;animation:icon-pulse 3s ease-in-out infinite;}@keyframes icon-pulse{0%,100%{opacity:0;transform:scale(0.8);} +50%{opacity:0.4;transform:scale(1.1);}}.toast-icon:hover::before{opacity:1;animation-play-state:paused;}.toast-icon:hover{transform:scale(1.08)rotate(8deg);box-shadow:0 12px 24px rgba(0,0,0,0.08),0 4px 8px rgba(0,0,0,0.06),inset 0 1px 0 rgba(255,255,255,0.7),inset 0-1px 0 rgba(0,0,0,0.05);}.dark.toast-icon{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.3)100%);border:1px solid rgba(255,255,255,0.12);box-shadow:0 8px 16px rgba(0,0,0,0.25),0 2px 4px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.12),inset 0-1px 0 rgba(255,255,255,0.03);}.dark.toast-icon::before{background:radial-gradient(circle,rgba(255,255,255,0.12)0%,transparent 70%);}.toast-body{flex:1;min-width:0;}.toast-title{font-weight:600;font-size:0.875rem;margin-bottom:0.375rem;line-height:1.3;letter-spacing:0.01em;text-shadow:0 1px 2px rgba(0,0,0,0.06);background:linear-gradient(135deg,currentColor 0%,currentColor 100%);-webkit-background-clip:text;background-clip:text;}.toast-message{font-size:0.8125rem;line-height:1.4;opacity:0.9;font-weight:450;text-shadow:0 1px 2px rgba(0,0,0,0.04);}.toast-actions{display:flex;gap:0.5rem;align-items:flex-start;flex-shrink:0;}.toast-action-btn{padding:0.5rem 0.875rem;border-radius:0.75rem;font-size:0.75rem;font-weight:500;border:none;cursor:pointer;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);display:flex;align-items:center;gap:0.375rem;background:linear-gradient(145deg,rgba(255,255,255,0.25)0%,rgba(255,255,255,0.12)50%,rgba(255,255,255,0.2)100%);color:inherit;backdrop-filter:blur(16px)saturate(130%);-webkit-backdrop-filter:blur(16px)saturate(130%);border:1px solid rgba(255,255,255,0.3);box-shadow:0 4px 8px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.5);text-shadow:0 1px 2px rgba(0,0,0,0.06);position:relative;overflow:hidden;} +.toast-action-btn::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.3)50%,transparent 100%);transition:left 0.4s cubic-bezier(0.4,0,0.2,1);}.toast-action-btn:hover::before{left:100%;}.toast-action-btn:hover{background:linear-gradient(145deg,rgba(255,255,255,0.35)0%,rgba(255,255,255,0.2)50%,rgba(255,255,255,0.3)100%);transform:translateY(-2px)scale(1.04);box-shadow:0 8px 16px rgba(0,0,0,0.08),0 2px 4px rgba(0,0,0,0.06),inset 0 1px 0 rgba(255,255,255,0.6);border-color:rgba(255,255,255,0.5);}.toast-action-btn:active{transform:translateY(-1px)scale(1.02);transition:transform 0.1s ease;}.toast-action-primary{background:linear-gradient(145deg,rgba(59,130,246,0.8)0%,rgba(37,99,235,0.85)50%,rgba(59,130,246,0.75)100%);color:white;border-color:rgba(59,130,246,0.6);box-shadow:0 4px 12px rgba(59,130,246,0.2),0 1px 3px rgba(59,130,246,0.12),inset 0 1px 0 rgba(255,255,255,0.25);text-shadow:0 1px 2px rgba(0,0,0,0.15);}.toast-action-primary::before{background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.25)50%,transparent 100%);}.toast-action-primary:hover{background:linear-gradient(145deg,rgba(59,130,246,0.9)0%,rgba(37,99,235,0.95)50%,rgba(59,130,246,0.85)100%);box-shadow:0 8px 20px rgba(59,130,246,0.25),0 2px 6px rgba(59,130,246,0.18),inset 0 1px 0 rgba(255,255,255,0.3);}.dark.toast-action-btn{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.3)100%);border:1px solid rgba(255,255,255,0.12);box-shadow:0 4px 8px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.08);}.dark.toast-action-btn:hover{background:linear-gradient(145deg,rgba(0,0,0,0.45)0%,rgba(15,15,15,0.35)50%,rgba(0,0,0,0.4)100%);box-shadow:0 8px 16px rgba(0,0,0,0.2),0 2px 4px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.12);}.toast-close{padding:0.375rem;border-radius:0.625rem;border:none;background:linear-gradient(145deg,rgba(255,255,255,0.18)0%,rgba(255,255,255,0.08)50%,rgba(255,255,255,0.12)100%);color:inherit;cursor:pointer;opacity:0.75;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(16px)saturate(110%);-webkit-backdrop-filter:blur(16px)saturate(110%);border:1px solid rgba(255,255,255,0.2);box-shadow:0 2px 4px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.3);position:relative;overflow:hidden;} +.toast-close::after{content:'';position:absolute;top:50%;left:50%;width:0;height:0;border-radius:50%;background:rgba(255,255,255,0.3);transform:translate(-50%,-50%);transition:width 0.25s ease,height 0.25s ease;}.toast-close:hover::after{width:100%;height:100%;}.toast-close:hover{opacity:1;background:linear-gradient(145deg,rgba(255,255,255,0.28)0%,rgba(255,255,255,0.15)50%,rgba(255,255,255,0.22)100%);transform:scale(1.1)rotate(90deg);box-shadow:0 4px 8px rgba(0,0,0,0.08),0 1px 2px rgba(0,0,0,0.06),inset 0 1px 0 rgba(255,255,255,0.5);}.toast-close:active{transform:scale(1.05)rotate(90deg);transition:transform 0.1s ease;}.dark.toast-close{background:linear-gradient(145deg,rgba(0,0,0,0.25)0%,rgba(15,15,15,0.15)50%,rgba(0,0,0,0.2)100%);border:1px solid rgba(255,255,255,0.08);box-shadow:0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.06);}.dark.toast-close::after{background:rgba(255,255,255,0.15);}.dark.toast-close:hover{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.4)100%);box-shadow:0 4px 8px rgba(0,0,0,0.25),0 1px 2px rgba(0,0,0,0.18),inset 0 1px 0 rgba(255,255,255,0.1);}.toast-progress{position:absolute;bottom:0;left:0;right:0;height:3px;background:linear-gradient(90deg,rgba(255,255,255,0.08)0%,rgba(255,255,255,0.04)50%,rgba(255,255,255,0.08)100%);overflow:hidden;border-radius:0 0 1.75rem 1.75rem;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);}.toast-progress-bar{height:100%;background:linear-gradient(90deg,rgba(255,255,255,0.6)0%,rgba(255,255,255,0.8)25%,rgba(255,255,255,0.9)50%,rgba(255,255,255,0.8)75%,rgba(255,255,255,0.6)100%);width:0%;transition:width 0.25s ease;position:relative;border-radius:inherit;box-shadow:0 0 8px rgba(255,255,255,0.4),0 0 4px rgba(255,255,255,0.3),inset 0 1px 0 rgba(255,255,255,0.7);overflow:hidden;} +.toast-progress-bar::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.4)25%,rgba(255,255,255,0.6)50%,rgba(255,255,255,0.4)75%,transparent 100%);animation:progress-shimmer 2s ease-in-out infinite;}.toast-progress-bar::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(45deg,transparent 25%,rgba(255,255,255,0.2)25%,rgba(255,255,255,0.2)50%,transparent 50%,transparent 75%,rgba(255,255,255,0.2)75%);background-size:12px 12px;animation:progress-stripes 0.8s linear infinite;}@keyframes progress-shimmer{0%{transform:translateX(-100%);} +100%{transform:translateX(100%);}}@keyframes progress-stripes{0%{background-position:0 0;} +100%{background-position:12px 0;}}@keyframes toast-progress{from{width:100%;} +to{width:0%;}}.notification-settings{max-width:320px;padding:0.875rem;background:linear-gradient(145deg,rgba(255,255,255,0.12)0%,rgba(255,255,255,0.06)50%,rgba(255,255,255,0.1)100%);border-radius:1.25rem;backdrop-filter:blur(24px)saturate(140%);-webkit-backdrop-filter:blur(24px)saturate(140%);border:1px solid rgba(255,255,255,0.25);box-shadow:0 16px 32px rgba(0,0,0,0.08),0 6px 12px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.4);}.dark.notification-settings{background:linear-gradient(145deg,rgba(0,0,0,0.35)0%,rgba(15,15,15,0.25)50%,rgba(0,0,0,0.3)100%);border:1px solid rgba(255,255,255,0.08);box-shadow:0 16px 32px rgba(0,0,0,0.25),0 6px 12px rgba(0,0,0,0.15),inset 0 1px 0 rgba(255,255,255,0.06);}.notification-settings h3{margin-bottom:1rem;font-size:1rem;font-weight:600;color:inherit;text-align:center;background:linear-gradient(135deg,currentColor 0%,currentColor 100%);-webkit-background-clip:text;background-clip:text;text-shadow:0 1px 2px rgba(0,0,0,0.08);}.setting-item{display:flex;align-items:center;gap:0.75rem;margin:1rem 0;cursor:pointer;font-size:0.875rem;font-weight:450;padding:0.75rem;border-radius:0.875rem;transition:all 0.25s cubic-bezier(0.4,0,0.2,1);background:linear-gradient(145deg,rgba(255,255,255,0.08)0%,rgba(255,255,255,0.04)100%);border:1px solid rgba(255,255,255,0.12);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);position:relative;overflow:hidden;} +.setting-item::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.08)50%,transparent 100%);transition:left 0.3s ease;}.setting-item:hover::before{left:100%;}.setting-item:hover{background:linear-gradient(145deg,rgba(255,255,255,0.16)0%,rgba(255,255,255,0.08)100%);transform:translateX(6px)scale(1.01);box-shadow:0 6px 12px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.25);}.dark.setting-item{background:linear-gradient(145deg,rgba(0,0,0,0.18)0%,rgba(15,15,15,0.12)100%);border:1px solid rgba(255,255,255,0.06);}.dark.setting-item:hover{background:linear-gradient(145deg,rgba(0,0,0,0.25)0%,rgba(15,15,15,0.18)100%);box-shadow:0 6px 12px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.12),inset 0 1px 0 rgba(255,255,255,0.08);}.setting-item input[type="checkbox"]{margin:0;width:1.25rem;height:1.25rem;accent-color:currentColor;cursor:pointer;border-radius:0.3rem;transition:all 0.2s ease;}.setting-item input[type="checkbox"]:checked{transform:scale(1.05);box-shadow:0 0 6px rgba(59,130,246,0.3);} +@media(max-width:640px){.notifications-container{left:0.5rem;right:0.5rem;top:0.5rem;}.glassmorphism-toast{margin-bottom:0.5rem;border-radius:1.25rem;}.toast-content{padding:0.875rem;}.toast-header{gap:0.75rem;}.toast-icon{width:2rem;height:2rem;}.toast-title{font-size:0.8125rem;}.toast-message{font-size:0.75rem;}.toast-action-btn{padding:0.4rem 0.7rem;font-size:0.7rem;}.toast-close{padding:0.3rem;}} +@media(prefers-contrast:high){.glassmorphism-toast{border:2px solid currentColor;backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(255,255,255,0.95);}.dark.glassmorphism-toast{background:rgba(0,0,0,0.95);}} +@media(prefers-reduced-motion:reduce){.glassmorphism-toast,.toast-icon,.toast-action-btn,.toast-close{transition:none!important;animation:none!important;}.glassmorphism-toast:hover{transform:none!important;}}`;document.head.appendChild(styles);} +handleActionClick(callbackId,toastId,shouldClose=true){if(callbackId&&this.actionCallbacks.has(callbackId)){const callback=this.actionCallbacks.get(callbackId);try{callback();}catch(error){console.error('Fehler beim Ausführen des Action-Callbacks:',error);} +this.actionCallbacks.delete(callbackId);} +if(shouldClose){this.closeToast(toastId);}} +executeCallback(callbackId){if(this.actionCallbacks.has(callbackId)){const callback=this.actionCallbacks.get(callbackId);try{callback();}catch(error){console.error('Fehler beim Ausführen des Callbacks:',error);} +this.actionCallbacks.delete(callbackId);}}} +const glassNotificationSystem=new GlassmorphismNotificationSystem();if(typeof window!=='undefined'){window.glassNotificationSystem=glassNotificationSystem;window.GlassmorphismNotificationSystem=GlassmorphismNotificationSystem;} +document.addEventListener('DOMContentLoaded',()=>{console.log('🎨 Glassmorphism Notification System bereit');}); \ No newline at end of file diff --git a/backend/static/js/glassmorphism-notifications.min.js.gz b/backend/static/js/glassmorphism-notifications.min.js.gz new file mode 100644 index 00000000..e3d4eb3e Binary files /dev/null and b/backend/static/js/glassmorphism-notifications.min.js.gz differ diff --git a/backend/static/js/global-refresh-functions.js.gz b/backend/static/js/global-refresh-functions.js.gz new file mode 100644 index 00000000..c445f091 Binary files /dev/null and b/backend/static/js/global-refresh-functions.js.gz differ diff --git a/backend/static/js/global-refresh-functions.min.js b/backend/static/js/global-refresh-functions.min.js new file mode 100644 index 00000000..13335ae3 --- /dev/null +++ b/backend/static/js/global-refresh-functions.min.js @@ -0,0 +1,47 @@ +function safeUpdateElement(elementId,value,options={}){const{fallbackValue='-',logWarning=true,attribute='textContent',transform=null}=options;const element=document.getElementById(elementId);if(!element){if(logWarning){console.warn(`🔍 Element mit ID'${elementId}'nicht gefunden`);} +return false;} +try{const finalValue=value!==undefined&&value!==null?value:fallbackValue;const displayValue=transform?transform(finalValue):finalValue;element[attribute]=displayValue;return true;}catch(error){console.error(`❌ Fehler beim Aktualisieren von Element'${elementId}':`,error);return false;}} +function safeBatchUpdate(updates,options={}){const results={};Object.entries(updates).forEach(([elementId,value])=>{results[elementId]=safeUpdateElement(elementId,value,options);});const successful=Object.values(results).filter(Boolean).length;const total=Object.keys(updates).length;console.log(`📊 Batch-Update:${successful}/${total}Elemente erfolgreich aktualisiert`);return results;} +function elementExists(elementId){return document.getElementById(elementId)!==null;} +window.refreshDashboard=async function(){const refreshButton=document.getElementById('refreshDashboard');if(refreshButton){refreshButton.disabled=true;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.add('animate-spin');}} +try{const response=await fetch('/api/dashboard/refresh',{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':getCSRFToken()}});const data=await response.json();if(data.success){updateDashboardStats(data.stats);showToast('✅ Dashboard erfolgreich aktualisiert','success');setTimeout(()=>{window.location.reload();},1000);}else{showToast('❌ Fehler beim Aktualisieren des Dashboards','error');}}catch(error){console.error('Dashboard-Refresh Fehler:',error);showToast('❌ Netzwerkfehler beim Dashboard-Refresh','error');}finally{if(refreshButton){refreshButton.disabled=false;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.remove('animate-spin');}}}};window.refreshStats=async function(){const refreshButton=document.querySelector('button[onclick="refreshStats()"]');if(refreshButton){refreshButton.disabled=true;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.add('animate-spin');}} +try{if(typeof loadBasicStats==='function'){await loadBasicStats();}else{const response=await fetch('/api/stats');const data=await response.json();if(response.ok){updateStatsCounter('total-jobs-count',data.total_jobs);updateStatsCounter('completed-jobs-count',data.completed_jobs);updateStatsCounter('online-printers-count',data.online_printers);updateStatsCounter('success-rate-percent',data.success_rate+'%');updateStatsCounter('active-jobs-count',data.active_jobs);updateStatsCounter('failed-jobs-count',data.failed_jobs);updateStatsCounter('total-users-count',data.total_users);}else{throw new Error(data.error||'Fehler beim Laden der Statistiken');}} +if(window.refreshAllCharts){window.refreshAllCharts();} +showToast('✅ Statistiken erfolgreich aktualisiert','success');}catch(error){console.error('Stats-Refresh Fehler:',error);showToast('❌ Fehler beim Aktualisieren der Statistiken','error');}finally{if(refreshButton){refreshButton.disabled=false;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.remove('animate-spin');}}}};window.refreshJobs=async function(){const refreshButton=document.getElementById('refresh-button');if(refreshButton){refreshButton.disabled=true;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.add('animate-spin');}} +try{console.log('🔄 Starte Jobs-Refresh...');let refreshSuccess=false;if(typeof window.jobManager!=='undefined'&&window.jobManager&&typeof window.jobManager.loadJobs==='function'){console.log('📝 Verwende window.jobManager.loadJobs()');await window.jobManager.loadJobs();refreshSuccess=true;}else if(typeof jobManager!=='undefined'&&jobManager&&typeof jobManager.loadJobs==='function'){console.log('📝 Verwende lokalen jobManager.loadJobs()');await jobManager.loadJobs();refreshSuccess=true;}else{console.log('📝 JobManager nicht verfügbar - verwende direkten API-Aufruf');const response=await fetch('/api/jobs',{headers:{'Content-Type':'application/json','X-CSRFToken':getCSRFToken()}});if(!response.ok){throw new Error(`API-Fehler:${response.status}${response.statusText}`);} +const data=await response.json();console.log('📝 API-Response erhalten:',data);if(!data||typeof data!=='object'){throw new Error('Ungültige API-Response: Keine Daten erhalten');} +let jobs=[];if(Array.isArray(data.jobs)){jobs=data.jobs;}else if(Array.isArray(data)){jobs=data;}else if(data.success&&Array.isArray(data.data)){jobs=data.data;}else{console.warn('⚠️ Keine Jobs-Array in API-Response gefunden:',data);jobs=[];} +console.log(`📝 ${jobs.length}Jobs aus API extrahiert:`,jobs);const jobsContainers=['.jobs-container','#jobs-container','.job-grid','#jobs-list','#jobs-grid'];let containerFound=false;for(const selector of jobsContainers){const container=document.querySelector(selector);if(container){containerFound=true;console.log(`📝 Container gefunden:${selector}`);if(jobs.length===0){container.innerHTML=`
    📭

    Keine Jobs vorhanden

    Es wurden noch keine Druckaufträge erstellt.

    `;}else{const jobCards=jobs.map(job=>{if(!job||typeof job!=='object'){console.warn('⚠️ Ungültiges Job-Objekt übersprungen:',job);return'';} +return`

    ${job.filename||job.title||job.name||'Unbekannter Job'}

    ID:${job.id||'N/A'}

    Status:${job.status||'Unbekannt'}

    ${job.printer_name?`

    Drucker:${job.printer_name}

    `:''} +${job.created_at?`

    Erstellt:${new Date(job.created_at).toLocaleDateString('de-DE')}

    `:''}
    `;}).filter(card=>card!=='').join('');container.innerHTML=jobCards||`

    Keine gültigen Jobs zum Anzeigen

    `;} +break;}} +if(!containerFound){console.warn('⚠️ Kein Jobs-Container gefunden. Verfügbare Container:',jobsContainers);} +refreshSuccess=true;} +if(refreshSuccess){showToast('✅ Druckaufträge erfolgreich aktualisiert','success');}}catch(error){console.error('❌ Jobs-Refresh Fehler:',error);let errorMessage;if(error.message.includes('undefined')){errorMessage='Jobs-Daten nicht verfügbar';}else if(error.message.includes('fetch')){errorMessage='Netzwerkfehler beim Laden der Jobs';}else if(error.message.includes('API')){errorMessage='Server-Fehler beim Laden der Jobs';}else{errorMessage=error.message||'Unbekannter Fehler beim Laden der Jobs';} +showToast(`❌ Fehler:${errorMessage}`,'error');const container=document.querySelector('.jobs-container, #jobs-container, .job-grid, #jobs-list');if(container){container.innerHTML=`
    ⚠️

    Fehler beim Laden

    ${errorMessage}

    `;}}finally{if(refreshButton){refreshButton.disabled=false;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.remove('animate-spin');}}}};window.refreshCalendar=async function(){const refreshButton=document.getElementById('refresh-button');if(refreshButton){refreshButton.disabled=true;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.add('animate-spin');}} +try{if(typeof calendar!=='undefined'&&calendar.refetchEvents){calendar.refetchEvents();showToast('✅ Kalender erfolgreich aktualisiert','success');}else{window.location.reload();}}catch(error){console.error('Calendar-Refresh Fehler:',error);showToast('❌ Fehler beim Aktualisieren des Kalenders','error');}finally{if(refreshButton){refreshButton.disabled=false;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.remove('animate-spin');}}}};window.refreshPrinters=async function(){const refreshButton=document.getElementById('refresh-button');if(refreshButton){refreshButton.disabled=true;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.add('animate-spin');}} +try{if(typeof printerManager!=='undefined'&&printerManager.loadPrinters){await printerManager.loadPrinters();}else{const response=await fetch('/api/printers/status/live',{headers:{'X-CSRFToken':getCSRFToken()}});if(response.ok){window.location.reload();}else{throw new Error('Drucker-Status konnte nicht abgerufen werden');}} +showToast('✅ Drucker erfolgreich aktualisiert','success');}catch(error){console.error('Printer-Refresh Fehler:',error);showToast('❌ Fehler beim Aktualisieren der Drucker','error');}finally{if(refreshButton){refreshButton.disabled=false;const icon=refreshButton.querySelector('svg');if(icon){icon.classList.remove('animate-spin');}}}};function updateDashboardStats(stats){const activeJobsEl=document.querySelector('[data-stat="active-jobs"]');if(activeJobsEl){activeJobsEl.textContent=stats.active_jobs||0;} +const availablePrintersEl=document.querySelector('[data-stat="available-printers"]');if(availablePrintersEl){availablePrintersEl.textContent=stats.available_printers||0;} +const totalJobsEl=document.querySelector('[data-stat="total-jobs"]');if(totalJobsEl){totalJobsEl.textContent=stats.total_jobs||0;} +const successRateEl=document.querySelector('[data-stat="success-rate"]');if(successRateEl){successRateEl.textContent=(stats.success_rate||0)+'%';} +console.log('📊 Dashboard-Statistiken aktualisiert:',stats);} +function updateStatsCounter(elementId,value,animate=true){const element=document.getElementById(elementId);if(!element){console.warn(`Element mit ID'${elementId}'nicht gefunden`);return;} +if(value===null||value===undefined){console.warn(`Ungültiger Wert für Element'${elementId}':`,value);value=0;} +if(animate){const currentValue=parseInt(element.textContent.replace(/[^\d]/g,''))||0;const targetValue=parseInt(value.toString().replace(/[^\d]/g,''))||0;if(currentValue!==targetValue){const finalTextValue=value!==null&&value!==undefined?value.toString():'0';animateCounter(element,currentValue,targetValue,finalTextValue);}}else{element.textContent=value!==null&&value!==undefined?value.toString():'0';}} +function animateCounter(element,start,end,finalText){if(!element){console.warn('animateCounter: Kein gültiges Element übergeben');return;} +if(typeof finalText!=='string'){if(finalText===null||finalText===undefined||(typeof finalText==='object'&&finalText!==null)){console.warn('animateCounter: Problematischer finalText-Wert:',finalText);} +finalText=finalText!==null&&finalText!==undefined?String(finalText):'0';} +start=parseInt(start)||0;end=parseInt(end)||0;const duration=1000;const startTime=performance.now();function updateCounter(currentTime){const elapsed=currentTime-startTime;const progress=Math.min(elapsed/duration,1);const easeOut=1-Math.pow(1-progress,3);const currentValue=Math.round(start+(end-start)*easeOut);try{if(typeof finalText==='string'&&finalText.includes('%')){element.textContent=currentValue+'%';}else{element.textContent=currentValue;}}catch(error){console.warn('animateCounter: Fehler bei finalText.includes:',error,'finalText:',finalText);element.textContent=currentValue;} +if(progress<1){requestAnimationFrame(updateCounter);}else{try{element.textContent=finalText;}catch(error){console.warn('animateCounter: Fehler bei finaler Zuweisung:',error);element.textContent=String(end);}}} +requestAnimationFrame(updateCounter);} +function getCSRFToken(){const token=document.querySelector('meta[name="csrf-token"]');return token?token.getAttribute('content'):'';} +function showToast(message,type='info'){if(typeof optimizationManager!=='undefined'&&optimizationManager.showToast){optimizationManager.showToast(message,type);return;} +const emoji={success:'✅',error:'❌',warning:'⚠️',info:'ℹ️'};console.log(`${emoji[type]||'ℹ️'}${message}`);try{const toast=document.createElement('div');toast.className=`fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transition-all duration-300 transform translate-x-full`;const colors={success:'bg-green-500 text-white',error:'bg-red-500 text-white',warning:'bg-yellow-500 text-black',info:'bg-blue-500 text-white'};toast.className+=`${colors[type]}`;toast.textContent=message;document.body.appendChild(toast);setTimeout(()=>{toast.classList.remove('translate-x-full');},100);setTimeout(()=>{toast.classList.add('translate-x-full');setTimeout(()=>{toast.remove();},300);},3000);}catch(error){console.warn('Toast-Erstellung fehlgeschlagen:',error);}} +window.universalRefresh=function(){const currentPath=window.location.pathname;if(currentPath.includes('/dashboard')){refreshDashboard();}else if(currentPath.includes('/jobs')){refreshJobs();}else if(currentPath.includes('/calendar')||currentPath.includes('/schichtplan')){refreshCalendar();}else if(currentPath.includes('/printers')||currentPath.includes('/drucker')){refreshPrinters();}else{window.location.reload();}};class AutoRefreshManager{constructor(){this.isEnabled=false;this.interval=null;this.intervalTime=30000;} +start(){if(this.isEnabled)return;this.isEnabled=true;this.interval=setInterval(()=>{if(!document.hidden){universalRefresh();}},this.intervalTime);console.log('🔄 Auto-Refresh aktiviert (alle 30 Sekunden)');} +stop(){if(!this.isEnabled)return;this.isEnabled=false;if(this.interval){clearInterval(this.interval);this.interval=null;} +console.log('⏸️ Auto-Refresh deaktiviert');} +toggle(){if(this.isEnabled){this.stop();}else{this.start();}}} +window.autoRefreshManager=new AutoRefreshManager();document.addEventListener('keydown',function(e){if(e.key==='F5'||(e.ctrlKey&&e.key==='r')){e.preventDefault();universalRefresh();} +if(e.ctrlKey&&e.shiftKey&&e.key==='R'){e.preventDefault();autoRefreshManager.toggle();showToast(autoRefreshManager.isEnabled?'🔄 Auto-Refresh aktiviert':'⏸️ Auto-Refresh deaktiviert','info');}});document.addEventListener('visibilitychange',function(){if(!document.hidden&&autoRefreshManager.isEnabled){setTimeout(universalRefresh,1000);}});console.log('🔄 Globale Refresh-Funktionen geladen'); \ No newline at end of file diff --git a/backend/static/js/global-refresh-functions.min.js.gz b/backend/static/js/global-refresh-functions.min.js.gz new file mode 100644 index 00000000..cf7f098b Binary files /dev/null and b/backend/static/js/global-refresh-functions.min.js.gz differ diff --git a/backend/static/js/job-manager.js.gz b/backend/static/js/job-manager.js.gz new file mode 100644 index 00000000..f2701c49 Binary files /dev/null and b/backend/static/js/job-manager.js.gz differ diff --git a/backend/static/js/job-manager.min.js b/backend/static/js/job-manager.min.js new file mode 100644 index 00000000..ec689bdd --- /dev/null +++ b/backend/static/js/job-manager.min.js @@ -0,0 +1,67 @@ +(function(){'use strict';class JobManager{constructor(){this.jobs=[];this.currentPage=1;this.totalPages=1;this.isLoading=false;this.refreshInterval=null;this.autoRefreshEnabled=false;console.log('🔧 JobManager wird initialisiert...');} +async init(){try{console.log('🚀 JobManager-Initialisierung gestartet');this.setupEventListeners();this.setupFormHandlers();await this.loadJobs();if(this.autoRefreshEnabled){this.startAutoRefresh();} +console.log('✅ JobManager erfolgreich initialisiert');}catch(error){console.error('❌ Fehler bei JobManager-Initialisierung:',error);this.showToast('Fehler beim Initialisieren des Job-Managers','error');}} +setupEventListeners(){console.log('📡 Event-Listener werden eingerichtet...');document.addEventListener('click',(e)=>{const target=e.target.closest('[data-job-action]');if(!target)return;const action=target.getAttribute('data-job-action');const jobId=target.getAttribute('data-job-id');if(!jobId){console.warn('⚠️ Job-ID fehlt für Aktion:',action);return;} +switch(action){case'start':this.startJob(jobId);break;case'pause':this.pauseJob(jobId);break;case'resume':this.resumeJob(jobId);break;case'stop':this.stopJob(jobId);break;case'delete':this.deleteJob(jobId);break;case'details':this.openJobDetails(jobId);break;default:console.warn('⚠️ Unbekannte Job-Aktion:',action);}});const refreshBtn=document.getElementById('refresh-jobs');if(refreshBtn){refreshBtn.addEventListener('click',()=>this.loadJobs());} +const autoRefreshToggle=document.getElementById('auto-refresh-toggle');if(autoRefreshToggle){autoRefreshToggle.addEventListener('change',(e)=>{this.autoRefreshEnabled=e.target.checked;if(this.autoRefreshEnabled){this.startAutoRefresh();}else{this.stopAutoRefresh();}});} +console.log('✅ Event-Listener erfolgreich eingerichtet');} +setupFormHandlers(){console.log('📝 Formular-Handler werden eingerichtet...');const newJobForm=document.getElementById('new-job-form');if(newJobForm){newJobForm.addEventListener('submit',async(e)=>{e.preventDefault();await this.createNewJob(new FormData(newJobForm));});} +const editJobForm=document.getElementById('edit-job-form');if(editJobForm){editJobForm.addEventListener('submit',async(e)=>{e.preventDefault();const jobId=editJobForm.getAttribute('data-job-id');await this.updateJob(jobId,new FormData(editJobForm));});} +console.log('✅ Formular-Handler erfolgreich eingerichtet');} +async loadJobs(page=1){if(this.isLoading){console.log('⚠️ Jobs werden bereits geladen...');return;} +this.isLoading=true;this.showLoadingState(true);try{console.log(`📥 Lade Jobs(Seite ${page})...`);const response=await fetch(`/api/jobs?page=${page}`,{headers:{'X-CSRFToken':this.getCSRFToken(),'Content-Type':'application/json'}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const data=await response.json();if(data&&typeof data==='object'){this.jobs=Array.isArray(data.jobs)?data.jobs:[];this.currentPage=Number(data.current_page)||1;this.totalPages=Number(data.total_pages)||1;console.log(`✅ ${this.jobs.length}Jobs erfolgreich geladen`,this.jobs);}else{console.warn('⚠️ Unerwartete API-Response-Struktur:',data);this.jobs=[];this.currentPage=1;this.totalPages=1;} +this.renderJobs();this.updatePagination();console.log(`✅ ${this.jobs.length}Jobs erfolgreich geladen und gerendert`);}catch(error){console.error('❌ Fehler beim Laden der Jobs:',error);this.showToast('Fehler beim Laden der Jobs','error');this.jobs=[];this.currentPage=1;this.totalPages=1;this.renderJobs();}finally{this.isLoading=false;this.showLoadingState(false);}} +async startJob(jobId){try{console.log(`▶️ Starte Job ${jobId}...`);const response=await fetch(`/api/jobs/${jobId}/start`,{method:'POST',headers:{'X-CSRFToken':this.getCSRFToken(),'Content-Type':'application/json'}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +this.showToast('Job erfolgreich gestartet','success');await this.loadJobs();}catch(error){console.error('❌ Fehler beim Starten des Jobs:',error);this.showToast('Fehler beim Starten des Jobs','error');}} +async pauseJob(jobId){try{console.log(`⏸️ Pausiere Job ${jobId}...`);const response=await fetch(`/api/jobs/${jobId}/pause`,{method:'POST',headers:{'X-CSRFToken':this.getCSRFToken(),'Content-Type':'application/json'}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +this.showToast('Job erfolgreich pausiert','success');await this.loadJobs();}catch(error){console.error('❌ Fehler beim Pausieren des Jobs:',error);this.showToast('Fehler beim Pausieren des Jobs','error');}} +async resumeJob(jobId){try{console.log(`▶️ Setze Job ${jobId}fort...`);const response=await fetch(`/api/jobs/${jobId}/resume`,{method:'POST',headers:{'X-CSRFToken':this.getCSRFToken(),'Content-Type':'application/json'}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +this.showToast('Job erfolgreich fortgesetzt','success');await this.loadJobs();}catch(error){console.error('❌ Fehler beim Fortsetzen des Jobs:',error);this.showToast('Fehler beim Fortsetzen des Jobs','error');}} +async stopJob(jobId){if(!confirm('Möchten Sie diesen Job wirklich stoppen?')){return;} +try{console.log(`⏹️ Stoppe Job ${jobId}...`);const response=await fetch(`/api/jobs/${jobId}/stop`,{method:'POST',headers:{'X-CSRFToken':this.getCSRFToken(),'Content-Type':'application/json'}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +this.showToast('Job erfolgreich gestoppt','success');await this.loadJobs();}catch(error){console.error('❌ Fehler beim Stoppen des Jobs:',error);this.showToast('Fehler beim Stoppen des Jobs','error');}} +async deleteJob(jobId){if(!confirm('Möchten Sie diesen Job wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')){return;} +try{console.log(`🗑️ Lösche Job ${jobId}...`);const response=await fetch(`/api/jobs/${jobId}`,{method:'DELETE',headers:{'X-CSRFToken':this.getCSRFToken(),'Content-Type':'application/json'}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +this.showToast('Job erfolgreich gelöscht','success');await this.loadJobs();}catch(error){console.error('❌ Fehler beim Löschen des Jobs:',error);this.showToast('Fehler beim Löschen des Jobs','error');}} +openJobDetails(jobId){console.log(`📄 Öffne Details für Job ${jobId}...`);const detailsUrl=`/jobs/${jobId}`;const detailsModal=document.getElementById(`job-details-${jobId}`);if(detailsModal&&typeof window.MYP!=='undefined'&&window.MYP.UI&&window.MYP.UI.modal){window.MYP.UI.modal.open(`job-details-${jobId}`);}else{window.location.href=detailsUrl;}} +renderJobs(){const jobsList=document.getElementById('jobs-list');if(!jobsList){console.warn('⚠️ Jobs-Liste Element nicht gefunden');return;} +if(!Array.isArray(this.jobs)){console.warn('⚠️ this.jobs ist kein Array:',this.jobs);this.jobs=[];} +if(this.jobs.length===0){jobsList.innerHTML=`
    📭

    Keine Jobs vorhanden

    Es wurden noch keine Druckaufträge erstellt.

    `;return;} +try{const jobsHTML=this.jobs.map(job=>{if(!job||typeof job!=='object'){console.warn('⚠️ Ungültiges Job-Objekt:',job);return'';} +return this.renderJobCard(job);}).filter(html=>html!=='').join('');jobsList.innerHTML=jobsHTML;console.log(`📋 ${this.jobs.length}Jobs gerendert`);}catch(error){console.error('❌ Fehler beim Rendern der Jobs:',error);jobsList.innerHTML=`
    ⚠️

    Fehler beim Laden

    Es gab einen Fehler beim Darstellen der Jobs.

    `;}} +renderJobCard(job){const statusClass=this.getJobStatusClass(job.status);const statusText=this.getJobStatusText(job.status);return`

    ${job.name||'Unbenannter Job'}

    ID:${job.id}Drucker:${job.printer_name||'Unbekannt'}Erstellt:${new Date(job.created_at).toLocaleDateString('de-DE')}
    ${statusText}${job.progress?`${job.progress}%`:''}
    ${this.renderJobActions(job)}
    `;} +renderJobActions(job){const actions=[];actions.push(``);switch(job.status){case'pending':case'ready':actions.push(``);break;case'running':case'printing':actions.push(``);actions.push(``);break;case'paused':actions.push(``);actions.push(``);break;case'completed':case'failed':case'cancelled':actions.push(``);break;} +return actions.join('');} +getJobStatusClass(status){const statusClasses={'pending':'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300','ready':'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300','running':'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300','printing':'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300','paused':'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300','completed':'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300','failed':'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300','cancelled':'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'};return statusClasses[status]||statusClasses['pending'];} +getJobStatusText(status){const statusTexts={'pending':'Wartend','ready':'Bereit','running':'Läuft','printing':'Druckt','paused':'Pausiert','completed':'Abgeschlossen','failed':'Fehlgeschlagen','cancelled':'Abgebrochen'};return statusTexts[status]||'Unbekannt';} +showLoadingState(show){const loadingEl=document.getElementById('jobs-loading');const jobsList=document.getElementById('jobs-list');if(loadingEl){loadingEl.style.display=show?'block':'none';} +if(jobsList){jobsList.style.opacity=show?'0.5':'1';jobsList.style.pointerEvents=show?'none':'auto';}} +getCSRFToken(){const token=document.querySelector('meta[name="csrf-token"]');return token?token.getAttribute('content'):'';} +showToast(message,type='info'){if(typeof window.showToast==='function'){window.showToast(message,type);}else{console.log(`${type.toUpperCase()}:${message}`);}} +startAutoRefresh(){this.stopAutoRefresh();this.refreshInterval=setInterval(()=>{if(!this.isLoading){this.loadJobs(this.currentPage);}},30000);console.log('🔄 Auto-Refresh gestartet (30s Intervall)');} +stopAutoRefresh(){if(this.refreshInterval){clearInterval(this.refreshInterval);this.refreshInterval=null;console.log('⏹️ Auto-Refresh gestoppt');}} +updatePagination(){const paginationEl=document.getElementById('jobs-pagination');if(!paginationEl)return;if(this.totalPages<=1){paginationEl.style.display='none';return;} +paginationEl.style.display='flex';let paginationHTML='';if(this.currentPage>1){paginationHTML+=``;} +for(let i=1;i<=this.totalPages;i++){const isActive=i===this.currentPage;paginationHTML+=``;} +if(this.currentPageWeiter`;} +paginationEl.innerHTML=paginationHTML;} +async createNewJob(formData){try{console.log('📝 Erstelle neuen Job...');const response=await fetch('/api/jobs',{method:'POST',headers:{'X-CSRFToken':this.getCSRFToken()},body:formData});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const result=await response.json();this.showToast('Job erfolgreich erstellt','success');await this.loadJobs();const form=document.getElementById('new-job-form');if(form){form.reset();} +return result;}catch(error){console.error('❌ Fehler beim Erstellen des Jobs:',error);this.showToast('Fehler beim Erstellen des Jobs','error');throw error;}} +async updateJob(jobId,formData){try{console.log(`📝 Aktualisiere Job ${jobId}...`);const response=await fetch(`/api/jobs/${jobId}`,{method:'PUT',headers:{'X-CSRFToken':this.getCSRFToken()},body:formData});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const result=await response.json();this.showToast('Job erfolgreich aktualisiert','success');await this.loadJobs();return result;}catch(error){console.error('❌ Fehler beim Aktualisieren des Jobs:',error);this.showToast('Fehler beim Aktualisieren des Jobs','error');throw error;}}} +window.JobManager=JobManager;document.addEventListener('DOMContentLoaded',function(){if(typeof window.jobManager==='undefined'){window.jobManager=new JobManager();if(document.getElementById('jobs-list')||document.querySelector('[data-job-action]')){window.jobManager.init();}}});console.log('✅ JobManager-Modul geladen');})(); \ No newline at end of file diff --git a/backend/static/js/job-manager.min.js.gz b/backend/static/js/job-manager.min.js.gz new file mode 100644 index 00000000..f751e7a4 Binary files /dev/null and b/backend/static/js/job-manager.min.js.gz differ diff --git a/backend/static/js/jobs-safety-fix.js.gz b/backend/static/js/jobs-safety-fix.js.gz new file mode 100644 index 00000000..818b5d90 Binary files /dev/null and b/backend/static/js/jobs-safety-fix.js.gz differ diff --git a/backend/static/js/jobs-safety-fix.min.js b/backend/static/js/jobs-safety-fix.min.js new file mode 100644 index 00000000..0de2efcb --- /dev/null +++ b/backend/static/js/jobs-safety-fix.min.js @@ -0,0 +1,21 @@ +(function(){'use strict';console.log('🛡️ Jobs Safety Fix wird geladen...');if(typeof window.jobsData==='undefined'){window.jobsData=[];} +if(typeof window.filteredJobs==='undefined'){window.filteredJobs=[];} +window.safeJobsOperations={getJobs:function(){if(window.jobManager&&Array.isArray(window.jobManager.jobs)){return window.jobManager.jobs;} +if(Array.isArray(window.jobsData)){return window.jobsData;} +return[];},getFilteredJobs:function(){if(Array.isArray(window.filteredJobs)){return window.filteredJobs;} +return this.getJobs();},setJobs:function(jobs){if(!Array.isArray(jobs)){console.warn('⚠️ Jobs ist kein Array, konvertiere zu leerem Array');jobs=[];} +if(window.jobManager){window.jobManager.jobs=jobs;} +window.jobsData=jobs;console.log(`✅ ${jobs.length}Jobs sicher gesetzt`);},findJob:function(jobId){const jobs=this.getJobs();return jobs.find(job=>job&&job.id&&job.id.toString()===jobId.toString())||null;},filterJobs:function(filterFn){const jobs=this.getJobs();if(typeof filterFn!=='function'){return jobs;} +try{return jobs.filter(job=>{if(!job||typeof job!=='object'){return false;} +return filterFn(job);});}catch(error){console.error('❌ Fehler beim Filtern der Jobs:',error);return jobs;}}};function ensureJobManagerSafety(){if(typeof window.jobManager!=='undefined'&&window.jobManager){if(!Array.isArray(window.jobManager.jobs)){console.warn('⚠️ JobManager.jobs ist kein Array, korrigiere...');window.jobManager.jobs=[];} +if(window.jobManager.loadJobs&&typeof window.jobManager.loadJobs==='function'){const originalLoadJobs=window.jobManager.loadJobs;window.jobManager.loadJobs=async function(...args){try{await originalLoadJobs.apply(this,args);if(!Array.isArray(this.jobs)){this.jobs=[];}}catch(error){console.error('❌ Fehler beim sicheren Laden der Jobs:',error);this.jobs=[];throw error;}};} +if(window.jobManager.renderJobs&&typeof window.jobManager.renderJobs==='function'){const originalRenderJobs=window.jobManager.renderJobs;window.jobManager.renderJobs=function(...args){try{if(!Array.isArray(this.jobs)){console.warn('⚠️ Jobs ist kein Array vor dem Rendern, korrigiere...');this.jobs=[];} +return originalRenderJobs.apply(this,args);}catch(error){console.error('❌ Fehler beim sicheren Rendern der Jobs:',error);const jobsList=document.getElementById('jobs-list');if(jobsList){jobsList.innerHTML=`
    ⚠️

    Renderingfehler

    Jobs konnten nicht angezeigt werden

    `;}}};}}} +function setupGlobalVariableWatching(){let _jobsData=[];Object.defineProperty(window,'jobsData',{get:function(){return _jobsData;},set:function(value){if(!Array.isArray(value)){console.warn('⚠️ Versuche jobsData mit Non-Array zu setzen:',value);_jobsData=[];}else{_jobsData=value;}},enumerable:true,configurable:true});let _filteredJobs=[];Object.defineProperty(window,'filteredJobs',{get:function(){return _filteredJobs;},set:function(value){if(!Array.isArray(value)){console.warn('⚠️ Versuche filteredJobs mit Non-Array zu setzen:',value);_filteredJobs=[];}else{_filteredJobs=value;}},enumerable:true,configurable:true});} +window.validateJobsResponse=function(data){if(!data||typeof data!=='object'){console.warn('⚠️ Ungültige API-Response:',data);return{jobs:[],total:0};} +let jobs=[];if(Array.isArray(data.jobs)){jobs=data.jobs;}else if(Array.isArray(data.data)){jobs=data.data;}else if(Array.isArray(data)){jobs=data;}else if(data.success&&Array.isArray(data.jobs)){jobs=data.jobs;} +jobs=jobs.filter(job=>{if(!job||typeof job!=='object'){console.warn('⚠️ Ungültiges Job-Objekt gefiltert:',job);return false;} +if(!job.id){console.warn('⚠️ Job ohne ID gefiltert:',job);return false;} +return true;});console.log(`✅ ${jobs.length}Jobs validiert`);return{jobs:jobs,total:jobs.length,current_page:data.current_page||1,total_pages:data.total_pages||1};};window.addEventListener('error',function(e){if(e.message&&e.message.includes('jobs')&&e.message.includes('undefined')){console.log('🛡️ Jobs undefined Fehler abgefangen:',e.message);window.safeJobsOperations.setJobs([]);e.preventDefault();return false;}});window.addEventListener('unhandledrejection',function(e){if(e.reason&&e.reason.message&&e.reason.message.includes('jobs')){console.log('🛡️ Jobs Promise rejection abgefangen:',e.reason);window.safeJobsOperations.setJobs([]);e.preventDefault();}});if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',function(){setupGlobalVariableWatching();ensureJobManagerSafety();});}else{setupGlobalVariableWatching();ensureJobManagerSafety();} +setInterval(function(){ensureJobManagerSafety();if(!Array.isArray(window.jobsData)){console.warn('⚠️ jobsData ist kein Array mehr, repariere...');window.jobsData=[];} +if(!Array.isArray(window.filteredJobs)){console.warn('⚠️ filteredJobs ist kein Array mehr, repariere...');window.filteredJobs=[];}},10000);console.log('✅ Jobs Safety Fix erfolgreich geladen');})(); \ No newline at end of file diff --git a/backend/static/js/jobs-safety-fix.min.js.gz b/backend/static/js/jobs-safety-fix.min.js.gz new file mode 100644 index 00000000..5645a26c Binary files /dev/null and b/backend/static/js/jobs-safety-fix.min.js.gz differ diff --git a/backend/static/js/notifications.js.gz b/backend/static/js/notifications.js.gz new file mode 100644 index 00000000..eab77f59 Binary files /dev/null and b/backend/static/js/notifications.js.gz differ diff --git a/backend/static/js/notifications.min.js b/backend/static/js/notifications.min.js new file mode 100644 index 00000000..a6e45ea1 --- /dev/null +++ b/backend/static/js/notifications.min.js @@ -0,0 +1,40 @@ +class ModernNotificationManager{constructor(){this.notificationToggle=document.getElementById('notificationToggle');this.notificationDropdown=document.getElementById('notificationDropdown');this.notificationBadge=document.getElementById('notificationBadge');this.notificationList=document.getElementById('notificationList');this.markAllReadBtn=document.getElementById('markAllRead');this.isOpen=false;this.notifications=[];this.activeToasts=new Map();this.toastCounter=0;this.csrfToken=this.getCSRFToken();this.init();this.setupGlobalNotificationSystem();} +getCSRFToken(){const metaTag=document.querySelector('meta[name="csrf-token"]');return metaTag?metaTag.getAttribute('content'):'';} +getAPIHeaders(){const headers={'Content-Type':'application/json',};if(this.csrfToken){headers['X-CSRFToken']=this.csrfToken;} +return headers;} +init(){if(!this.notificationToggle)return;this.notificationToggle.addEventListener('click',(e)=>{e.stopPropagation();this.toggleDropdown();});if(this.markAllReadBtn){this.markAllReadBtn.addEventListener('click',()=>{this.markAllAsRead();});} +document.addEventListener('click',(e)=>{if(!this.notificationDropdown.contains(e.target)&&!this.notificationToggle.contains(e.target)){this.closeDropdown();}});this.loadNotifications();setInterval(()=>{this.loadNotifications();},30000);} +toggleDropdown(){if(this.isOpen){this.closeDropdown();}else{this.openDropdown();}} +openDropdown(){this.notificationDropdown.classList.remove('hidden');this.notificationToggle.setAttribute('aria-expanded','true');this.isOpen=true;this.notificationDropdown.style.opacity='0';this.notificationDropdown.style.transform='translateY(-10px)';requestAnimationFrame(()=>{this.notificationDropdown.style.transition='opacity 0.2s ease, transform 0.2s ease';this.notificationDropdown.style.opacity='1';this.notificationDropdown.style.transform='translateY(0)';});} +closeDropdown(){this.notificationDropdown.style.transition='opacity 0.2s ease, transform 0.2s ease';this.notificationDropdown.style.opacity='0';this.notificationDropdown.style.transform='translateY(-10px)';setTimeout(()=>{this.notificationDropdown.classList.add('hidden');this.notificationToggle.setAttribute('aria-expanded','false');this.isOpen=false;},200);} +async loadNotifications(){try{const response=await fetch('/api/notifications');if(response.ok){const data=await response.json();this.notifications=data.notifications||[];this.updateUI();}}catch(error){console.error('Fehler beim Laden der Benachrichtigungen:',error);}} +updateUI(){this.updateBadge();this.updateNotificationList();} +updateBadge(){const unreadCount=this.notifications.filter(n=>!n.read).length;if(unreadCount>0){this.notificationBadge.textContent=unreadCount>99?'99+':unreadCount.toString();this.notificationBadge.classList.remove('hidden');}else{this.notificationBadge.classList.add('hidden');}} +updateNotificationList(){if(this.notifications.length===0){this.notificationList.innerHTML=`
    Keine neuen Benachrichtigungen
    `;return;} +const notificationHTML=this.notifications.map(notification=>{const isUnread=!notification.read;const timeAgo=this.formatTimeAgo(new Date(notification.created_at));return`
    ${this.getNotificationIcon(notification.type)}

    ${this.getNotificationTitle(notification.type)}

    ${isUnread?'
    ':''}

    ${this.getNotificationMessage(notification)}

    ${timeAgo}

    `;}).join('');this.notificationList.innerHTML=notificationHTML;this.notificationList.querySelectorAll('.notification-item').forEach(item=>{item.addEventListener('click',(e)=>{const notificationId=item.dataset.notificationId;this.markAsRead(notificationId);});});} +getNotificationIcon(type){const icons={'guest_request':`
    `,'job_completed':`
    `,'system':`
    `};return icons[type]||icons['system'];} +getNotificationTitle(type){const titles={'guest_request':'Neue Gastanfrage','job_completed':'Druckauftrag abgeschlossen','system':'System-Benachrichtigung'};return titles[type]||'Benachrichtigung';} +getNotificationMessage(notification){try{const payload=JSON.parse(notification.payload||'{}');switch(notification.type){case'guest_request':return`${payload.guest_name||'Ein Gast'}hat eine neue Druckanfrage gestellt.`;case'job_completed':return`Der Druckauftrag"${payload.job_name || 'Unbekannt'}"wurde erfolgreich abgeschlossen.`;default:return payload.message||'Neue Benachrichtigung erhalten.';}}catch(error){return'Neue Benachrichtigung erhalten.';}} +formatTimeAgo(date){const now=new Date();const diffInSeconds=Math.floor((now-date)/1000);if(diffInSeconds<60){return'Gerade eben';}else if(diffInSeconds<3600){const minutes=Math.floor(diffInSeconds/60);return`vor ${minutes}Minute${minutes!==1?'n':''}`;}else if(diffInSeconds<86400){const hours=Math.floor(diffInSeconds/3600);return`vor ${hours}Stunde${hours!==1?'n':''}`;}else{const days=Math.floor(diffInSeconds/86400);return`vor ${days}Tag${days!==1?'en':''}`;}} +async markAsRead(notificationId){try{const response=await fetch(`/api/notifications/${notificationId}/read`,{method:'POST',headers:this.getAPIHeaders()});if(response.ok){const notification=this.notifications.find(n=>n.id==notificationId);if(notification){notification.read=true;this.updateUI();}}else{console.error('Fehler beim Markieren als gelesen:',response.status,response.statusText);}}catch(error){console.error('Fehler beim Markieren als gelesen:',error);}} +async markAllAsRead(){try{const response=await fetch('/api/notifications/mark-all-read',{method:'POST',headers:this.getAPIHeaders()});if(response.ok){this.notifications.forEach(notification=>{notification.read=true;});this.updateUI();}else{console.error('Fehler beim Markieren aller als gelesen:',response.status,response.statusText);}}catch(error){console.error('Fehler beim Markieren aller als gelesen:',error);}} +setupGlobalNotificationSystem(){window.showFlashMessage=this.showGlassToast.bind(this);window.showToast=this.showGlassToast.bind(this);window.showNotification=this.showGlassToast.bind(this);window.showSuccessMessage=(message)=>this.showGlassToast(message,'success');window.showErrorMessage=(message)=>this.showGlassToast(message,'error');window.showWarningMessage=(message)=>this.showGlassToast(message,'warning');window.showInfoMessage=(message)=>this.showGlassToast(message,'info');} +showGlassToast(message,type='info',duration=5000,options={}){if(window.dndManager&&window.dndManager.isEnabled){window.dndManager.suppressNotification(message,type);return;} +const toastId=`toast-${++this.toastCounter}`;const toast=document.createElement('div');toast.id=toastId;toast.className=`glass-toast notification notification-${type}show`;const iconMap={success:``,error:``,warning:``,info:``};toast.innerHTML=`
    ${iconMap[type]||iconMap.info}
    ${options.title?`
    ${options.title}
    `:''}
    ${message}
    `;let container=document.getElementById('toast-container');if(!container){container=document.createElement('div');container.id='toast-container';container.className='notifications-container';document.body.appendChild(container);} +const existingToasts=container.children.length;toast.style.top=`${1+existingToasts*5}rem`;container.appendChild(toast);this.activeToasts.set(toastId,toast);let isPaused=false;let timeoutId;const startTimer=()=>{if(!isPaused&&duration>0){timeoutId=setTimeout(()=>{this.closeToast(toastId);},duration);}};toast.addEventListener('mouseenter',()=>{isPaused=true;clearTimeout(timeoutId);toast.style.transform='translateY(-2px) scale(1.02)';});toast.addEventListener('mouseleave',()=>{isPaused=false;toast.style.transform='translateY(0) scale(1)';startTimer();});setTimeout(startTimer,100);if(options.playSound!==false){this.playNotificationSound(type);} +return toastId;} +closeToast(toastId){const toast=this.activeToasts.get(toastId);if(!toast)return;toast.classList.add('hiding');setTimeout(()=>{if(toast.parentNode){toast.parentNode.removeChild(toast);} +this.activeToasts.delete(toastId);this.repositionToasts();},400);} +repositionToasts(){let index=0;this.activeToasts.forEach((toast)=>{if(toast.parentNode){toast.style.top=`${1+index*5}rem`;index++;}});} +playNotificationSound(type){try{if(typeof AudioContext!=='undefined'||typeof webkitAudioContext!=='undefined'){const audioContext=new(window.AudioContext||window.webkitAudioContext)();const oscillator=audioContext.createOscillator();const gainNode=audioContext.createGain();oscillator.connect(gainNode);gainNode.connect(audioContext.destination);const frequencies={success:[523.25,659.25,783.99],error:[440,415.3],warning:[493.88,523.25],info:[523.25]};const freq=frequencies[type]||frequencies.info;freq.forEach((f,i)=>{setTimeout(()=>{const osc=audioContext.createOscillator();const gain=audioContext.createGain();osc.connect(gain);gain.connect(audioContext.destination);osc.frequency.setValueAtTime(f,audioContext.currentTime);gain.gain.setValueAtTime(0.1,audioContext.currentTime);gain.gain.exponentialRampToValueAtTime(0.01,audioContext.currentTime+0.1);osc.start(audioContext.currentTime);osc.stop(audioContext.currentTime+0.1);},i*100);});}}catch(error){}} +showBrowserNotification(title,message,options={}){if('Notification'in window){if(Notification.permission==='granted'){const notification=new Notification(title,{body:message,icon:options.icon||'/static/icons/static/icons/notification-icon.png',badge:options.badge||'/static/icons/static/icons/badge-icon.png',tag:options.tag||'myp-notification',requireInteraction:options.requireInteraction||false,...options});notification.onclick=options.onClick||(()=>{window.focus();notification.close();});return notification;}else if(Notification.permission==='default'){Notification.requestPermission().then(permission=>{if(permission==='granted'){this.showBrowserNotification(title,message,options);}});}} +return null;} +showAlert(message,type='info',options={}){const alertId=`alert-${Date.now()}`;const alert=document.createElement('div');alert.id=alertId;alert.className=`alert alert-${type}`;alert.innerHTML=`
    ${this.getIconForType(type)}
    ${options.title?`

    ${options.title}

    `:''}

    ${message}

    ${options.dismissible!==false?``:''}
    `;const container=options.container||document.querySelector('.flash-messages')||document.body;container.appendChild(alert);if(options.autoDismiss!==false){setTimeout(()=>{if(alert.parentNode){alert.style.opacity='0';alert.style.transform='translateY(-20px)';setTimeout(()=>alert.remove(),300);}},options.duration||7000);} +return alertId;} +getIconForType(type){const icons={success:``,error:``,warning:``,info:``};return icons[type]||icons.info;}} +class NotificationManager extends ModernNotificationManager{constructor(){super();console.warn('NotificationManager ist deprecated. Verwenden Sie ModernNotificationManager.');}} +const modernNotificationManager=new ModernNotificationManager();if(typeof window!=='undefined'){window.notificationManager=modernNotificationManager;window.modernNotificationManager=modernNotificationManager;} +const toastStyles=``;if(typeof document!=='undefined'&&!document.getElementById('toast-styles')){const styleElement=document.createElement('div');styleElement.id='toast-styles';styleElement.innerHTML=toastStyles;document.head.appendChild(styleElement);} \ No newline at end of file diff --git a/backend/static/js/notifications.min.js.gz b/backend/static/js/notifications.min.js.gz new file mode 100644 index 00000000..434233dc Binary files /dev/null and b/backend/static/js/notifications.min.js.gz differ diff --git a/backend/static/js/offline-app.js.gz b/backend/static/js/offline-app.js.gz new file mode 100644 index 00000000..75e7f995 Binary files /dev/null and b/backend/static/js/offline-app.js.gz differ diff --git a/backend/static/js/offline-app.min.js b/backend/static/js/offline-app.min.js new file mode 100644 index 00000000..5239cc2e --- /dev/null +++ b/backend/static/js/offline-app.min.js @@ -0,0 +1,28 @@ +class MYPApp{constructor(){this.isOffline=!navigator.onLine;this.registerTime=new Date().toISOString();this.darkMode=document.documentElement.classList.contains('dark');this.setupOfflineDetection();this.setupServiceWorker();this.setupLocalStorage();this.setupUI();this.setupThemeListeners();console.log(`MYP App initialisiert um ${this.registerTime}`);console.log(`Initiale Netzwerkverbindung:${navigator.onLine?'Online':'Offline'}`);console.log(`Aktueller Modus:${this.darkMode?'Dark Mode':'Light Mode'}`);} +setupOfflineDetection(){window.addEventListener('online',()=>{this.isOffline=false;document.body.classList.remove('offline-mode');console.log('Netzwerkverbindung wiederhergestellt!');this.syncOfflineData();window.dispatchEvent(new CustomEvent('networkStatusChange',{detail:{isOffline:false}}));});window.addEventListener('offline',()=>{this.isOffline=true;document.body.classList.add('offline-mode');console.log('Netzwerkverbindung verloren!');window.dispatchEvent(new CustomEvent('networkStatusChange',{detail:{isOffline:true}}));});if(this.isOffline){document.body.classList.add('offline-mode');}} +setupServiceWorker(){if('serviceWorker'in navigator){const swPath='/sw.js';navigator.serviceWorker.register(swPath,{scope:'/'}).then(registration=>{console.log('Service Worker erfolgreich registriert mit Scope:',registration.scope);if(registration.installing){console.log('Service Worker wird installiert');}else if(registration.waiting){console.log('Service Worker wartet auf Aktivierung');}else if(registration.active){console.log('Service Worker ist aktiv');} +registration.addEventListener('updatefound',()=>{const newWorker=registration.installing;newWorker.addEventListener('statechange',()=>{console.log(`Service Worker Status:${newWorker.state}`);if(newWorker.state==='activated'){this.fetchAndCacheAppData();}});});registration.update();}).catch(error=>{console.error('Service Worker Registrierung fehlgeschlagen:',error);this.setupLocalCache();});navigator.serviceWorker.addEventListener('controllerchange',()=>{console.log('Service Worker Controller hat gewechselt');});}else{console.warn('Service Worker werden von diesem Browser nicht unterstützt');this.setupLocalCache();}} +setupLocalCache(){console.log('Verwende lokalen Cache als Fallback');this.fetchAndCacheAppData();setInterval(()=>{if(navigator.onLine){this.fetchAndCacheAppData();}},15*60*1000);} +fetchAndCacheAppData(){if(!navigator.onLine)return;const endpoints=['/api/printers','/api/jobs','/api/schedule','/api/status'];for(const endpoint of endpoints){fetch(endpoint).then(response=>response.json()).then(data=>{localStorage.setItem(`cache_${endpoint}`,JSON.stringify({timestamp:new Date().getTime(),data:data}));console.log(`Daten für ${endpoint}gecached`);}).catch(error=>{console.error(`Fehler beim Cachen von ${endpoint}:`,error);});}} +setupLocalStorage(){if(!localStorage.getItem('offlineChanges')){localStorage.setItem('offlineChanges',JSON.stringify([]));} +if(!localStorage.getItem('appConfig')){const defaultConfig={theme:'system',notifications:true,dataSync:true,lastSync:null};localStorage.setItem('appConfig',JSON.stringify(defaultConfig));} +this.cleanupOldCache(7);} +cleanupOldCache(daysOld){const now=new Date().getTime();const maxAge=daysOld*24*60*60*1000;for(let i=0;imaxAge)){localStorage.removeItem(key);console.log(`Alter Cache-Eintrag entfernt:${key}`);}}catch(e){console.error(`Fehler beim Verarbeiten von Cache-Eintrag ${key}:`,e);}}}} +syncOfflineData(){if(!navigator.onLine)return;const offlineChanges=JSON.parse(localStorage.getItem('offlineChanges')||'[]');if(offlineChanges.length===0){console.log('Keine Offline-Änderungen zu synchronisieren');return;} +console.log(`${offlineChanges.length}Offline-Änderungen werden synchronisiert...`);document.body.classList.add('syncing');const syncPromises=offlineChanges.map(change=>{return fetch(change.url,{method:change.method,headers:{'Content-Type':'application/json','X-Offline-Change':'true'},body:JSON.stringify(change.data)}).then(response=>{if(!response.ok){throw new Error(`Fehler ${response.status}:${response.statusText}`);} +return response.json();}).then(data=>{console.log(`Änderung erfolgreich synchronisiert:${change.url}`);return{success:true,change};}).catch(error=>{console.error(`Synchronisierung fehlgeschlagen für ${change.url}:`,error);return{success:false,change,error};});});Promise.all(syncPromises).then(results=>{const failedChanges=results.filter(result=>!result.success).map(result=>result.change);localStorage.setItem('offlineChanges',JSON.stringify(failedChanges));const appConfig=JSON.parse(localStorage.getItem('appConfig')||'{}');appConfig.lastSync=new Date().toISOString();localStorage.setItem('appConfig',JSON.stringify(appConfig));document.body.classList.remove('syncing');const syncEvent=new CustomEvent('offlineDataSynced',{detail:{total:offlineChanges.length,succeeded:offlineChanges.length-failedChanges.length,failed:failedChanges.length}});window.dispatchEvent(syncEvent);console.log(`Synchronisierung abgeschlossen:${offlineChanges.length-failedChanges.length}erfolgreich,${failedChanges.length}fehlgeschlagen`);});} +addOfflineChange(url,method,data){const offlineChanges=JSON.parse(localStorage.getItem('offlineChanges')||'[]');offlineChanges.push({id:Date.now().toString(36)+Math.random().toString(36).substr(2,5),url,method,data,timestamp:new Date().toISOString()});localStorage.setItem('offlineChanges',JSON.stringify(offlineChanges));console.log(`Offline-Änderung gespeichert:${method}${url}`);} +setupThemeListeners(){window.addEventListener('darkModeChanged',(e)=>{this.darkMode=e.detail.isDark;this.updateAppTheme();const appConfig=JSON.parse(localStorage.getItem('appConfig')||'{}');appConfig.theme=this.darkMode?'dark':'light';localStorage.setItem('appConfig',JSON.stringify(appConfig));console.log(`Theme geändert:${this.darkMode?'Dark Mode':'Light Mode'}`);});window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',(e)=>{const appConfig=JSON.parse(localStorage.getItem('appConfig')||'{}');if(appConfig.theme==='system'){this.darkMode=e.matches;this.updateAppTheme();console.log(`Systemthema geändert:${this.darkMode?'Dark Mode':'Light Mode'}`);}});} +updateAppTheme(){const metaThemeColor=document.getElementById('metaThemeColor')||document.querySelector('meta[name="theme-color"]');if(metaThemeColor){metaThemeColor.setAttribute('content',this.darkMode?'#0f172a':'#ffffff');} +this.updateUIForTheme();} +updateUIForTheme(){const offlineBanner=document.getElementById('offline-banner');const syncIndicator=document.getElementById('sync-indicator');if(offlineBanner){if(this.darkMode){offlineBanner.classList.add('dark-theme');offlineBanner.classList.remove('light-theme');}else{offlineBanner.classList.add('light-theme');offlineBanner.classList.remove('dark-theme');}} +if(syncIndicator){if(this.darkMode){syncIndicator.classList.add('dark-theme');syncIndicator.classList.remove('light-theme');}else{syncIndicator.classList.add('light-theme');syncIndicator.classList.remove('dark-theme');}} +} +setupUI(){if(!document.getElementById('offline-banner')){const banner=document.createElement('div');banner.id='offline-banner';banner.className=`hidden fixed top-0 left-0 right-0 bg-amber-500 dark:bg-amber-600 text-white text-center py-2 px-4 z-50 ${this.darkMode?'dark-theme':'light-theme'}`;banner.textContent='Sie sind offline. Einige Funktionen sind eingeschränkt.';document.body.prepend(banner);if(this.isOffline){banner.classList.remove('hidden');} +window.addEventListener('online',()=>banner.classList.add('hidden'));window.addEventListener('offline',()=>banner.classList.remove('hidden'));} +if(!document.getElementById('sync-indicator')){const indicator=document.createElement('div');indicator.id='sync-indicator';indicator.className=`hidden fixed bottom-4 right-4 bg-indigo-600 dark:bg-indigo-700 text-white text-sm rounded-full py-1 px-3 z-50 flex items-center ${this.darkMode?'dark-theme':'light-theme'}`;const spinnerSvg=document.createElementNS('http://www.w3.org/2000/svg','svg');spinnerSvg.setAttribute('class','animate-spin -ml-1 mr-2 h-4 w-4 text-white');spinnerSvg.setAttribute('fill','none');spinnerSvg.setAttribute('viewBox','0 0 24 24');const circle=document.createElementNS('http://www.w3.org/2000/svg','circle');circle.setAttribute('class','opacity-25');circle.setAttribute('cx','12');circle.setAttribute('cy','12');circle.setAttribute('r','10');circle.setAttribute('stroke','currentColor');circle.setAttribute('stroke-width','4');const path=document.createElementNS('http://www.w3.org/2000/svg','path');path.setAttribute('class','opacity-75');path.setAttribute('fill','currentColor');path.setAttribute('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');spinnerSvg.appendChild(circle);spinnerSvg.appendChild(path);const text=document.createElement('span');text.textContent='Synchronisiere...';indicator.appendChild(spinnerSvg);indicator.appendChild(text);document.body.appendChild(indicator);const observer=new MutationObserver((mutations)=>{mutations.forEach((mutation)=>{if(mutation.type==='attributes'&&mutation.attributeName==='class'){if(document.body.classList.contains('syncing')){indicator.classList.remove('hidden');}else{indicator.classList.add('hidden');}}});});observer.observe(document.body,{attributes:true});} +this.setupMobileOptimizations();} +setupMobileOptimizations(){if('serviceWorker'in navigator&&navigator.serviceWorker.controller){this.setupTouchFeedback();this.fixMobileViewportHeight();if('scrollRestoration'in history){history.scrollRestoration='manual';}}} +setupTouchFeedback(){document.addEventListener('touchstart',function(e){if(e.target.tagName==='BUTTON'||e.target.tagName==='A'||e.target.closest('button')||e.target.closest('a')){const element=e.target.tagName==='BUTTON'||e.target.tagName==='A'?e.target:(e.target.closest('button')||e.target.closest('a'));element.classList.add('touch-active');}},{passive:true});document.addEventListener('touchend',function(){const activeElements=document.querySelectorAll('.touch-active');activeElements.forEach(el=>el.classList.remove('touch-active'));},{passive:true});} +fixMobileViewportHeight(){const setViewportHeight=()=>{const vh=window.innerHeight*0.01;document.documentElement.style.setProperty('--vh',`${vh}px`);};setViewportHeight();window.addEventListener('resize',setViewportHeight);window.addEventListener('orientationchange',()=>{setTimeout(setViewportHeight,100);});}} +document.addEventListener('DOMContentLoaded',()=>{window.myp=new MYPApp();}); \ No newline at end of file diff --git a/backend/static/js/offline-app.min.js.gz b/backend/static/js/offline-app.min.js.gz new file mode 100644 index 00000000..ebcfaa5d Binary files /dev/null and b/backend/static/js/offline-app.min.js.gz differ diff --git a/backend/static/js/optimization-features.js.gz b/backend/static/js/optimization-features.js.gz new file mode 100644 index 00000000..67c2d235 Binary files /dev/null and b/backend/static/js/optimization-features.js.gz differ diff --git a/backend/static/js/optimization-features.min.js b/backend/static/js/optimization-features.min.js new file mode 100644 index 00000000..6dd596da --- /dev/null +++ b/backend/static/js/optimization-features.min.js @@ -0,0 +1,71 @@ +class OptimizationManager{constructor(){this.isAutoOptimizationEnabled=false;this.isBatchModeEnabled=false;this.selectedJobs=new Set();this.optimizationSettings={algorithm:'round_robin',considerDistance:true,minimizeChangeover:true,maxBatchSize:10,timeWindow:24};this.init();} +init(){this.setupEventListeners();this.loadSavedSettings();this.updateUI();} +setupEventListeners(){document.addEventListener('keydown',(e)=>{if(e.ctrlKey&&e.altKey&&e.key==='O'){this.toggleAutoOptimization();e.preventDefault();} +if(e.ctrlKey&&e.altKey&&e.key==='B'){this.toggleBatchMode();e.preventDefault();}});} +toggleAutoOptimization(){this.isAutoOptimizationEnabled=!this.isAutoOptimizationEnabled;const button=document.getElementById('auto-opt-toggle');if(button){this.updateAutoOptimizationButton(button);} +localStorage.setItem('myp-auto-optimization',this.isAutoOptimizationEnabled);this.showOptimizationNotification(this.isAutoOptimizationEnabled?'aktiviert':'deaktiviert','auto-optimization');if(this.isAutoOptimizationEnabled){this.performAutoOptimization();} +this.updateUI();} +updateAutoOptimizationButton(button){const span=button.querySelector('span');const icon=button.querySelector('svg');if(this.isAutoOptimizationEnabled){button.classList.remove('btn-secondary');button.classList.add('btn-primary');span.textContent='Auto-Optimierung AN';button.style.transform='scale(1.05)';setTimeout(()=>{button.style.transform='';},200);icon.style.animation='spin 1s ease-in-out';setTimeout(()=>{icon.style.animation='';},1000);}else{button.classList.remove('btn-primary');button.classList.add('btn-secondary');span.textContent='Auto-Optimierung';}} +toggleBatchMode(){this.isBatchModeEnabled=!this.isBatchModeEnabled;const button=document.getElementById('batch-toggle');if(button){this.updateBatchModeButton(button);} +this.toggleBatchSelection();localStorage.setItem('myp-batch-mode',this.isBatchModeEnabled);this.showOptimizationNotification(this.isBatchModeEnabled?'aktiviert':'deaktiviert','batch-mode');this.updateUI();} +updateBatchModeButton(button){const span=button.querySelector('span');if(this.isBatchModeEnabled){button.classList.remove('btn-secondary');button.classList.add('btn-warning');span.textContent=`Batch-Modus(${this.selectedJobs.size})`;button.style.transform='scale(1.05)';setTimeout(()=>{button.style.transform='';},200);}else{button.classList.remove('btn-warning');button.classList.add('btn-secondary');span.textContent='Mehrfachauswahl';this.selectedJobs.clear();}} +toggleBatchSelection(){const jobCards=document.querySelectorAll('.job-card, [data-job-id]');jobCards.forEach(card=>{if(this.isBatchModeEnabled){this.enableBatchSelection(card);}else{this.disableBatchSelection(card);}});} +enableBatchSelection(card){let checkbox=card.querySelector('.batch-checkbox');if(!checkbox){checkbox=document.createElement('input');checkbox.type='checkbox';checkbox.className='batch-checkbox absolute top-3 left-3 w-5 h-5 rounded border-2 border-gray-300 text-blue-600 focus:ring-blue-500';checkbox.style.zIndex='10';checkbox.addEventListener('change',(e)=>{const jobId=card.dataset.jobId;if(e.target.checked){this.selectedJobs.add(jobId);card.classList.add('selected-for-batch');}else{this.selectedJobs.delete(jobId);card.classList.remove('selected-for-batch');} +this.updateBatchCounter();});card.style.position='relative';card.appendChild(checkbox);} +checkbox.style.display='block';card.classList.add('batch-selectable');} +disableBatchSelection(card){const checkbox=card.querySelector('.batch-checkbox');if(checkbox){checkbox.style.display='none';} +card.classList.remove('batch-selectable','selected-for-batch');} +updateBatchCounter(){const button=document.getElementById('batch-toggle');if(button&&this.isBatchModeEnabled){const span=button.querySelector('span');span.textContent=`Batch-Modus(${this.selectedJobs.size})`;}} +async performAutoOptimization(){try{this.showOptimizationLoading();const response=await fetch('/api/optimization/auto-optimize',{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()},body:JSON.stringify({settings:this.optimizationSettings,enabled:this.isAutoOptimizationEnabled})});const data=await response.json();this.hideOptimizationLoading();if(data.success){this.showRewardModal(data);this.refreshCurrentView();}else{this.showErrorMessage(`Optimierung fehlgeschlagen:${data.error}`);}}catch(error){this.hideOptimizationLoading();console.error('Auto-Optimierung Fehler:',error);this.showErrorMessage('Netzwerkfehler bei der Auto-Optimierung');}} +showRewardModal(data){const existingModal=document.getElementById('optimization-reward-modal');if(existingModal){existingModal.remove();} +const modal=document.createElement('div');modal.id='optimization-reward-modal';modal.className='fixed inset-0 bg-black/70 backdrop-blur-md z-50 flex items-center justify-center p-4 animate-fade-in';const optimizedCount=data.optimized_jobs||0;let celebration='🎉';let message='Optimierung erfolgreich!';if(optimizedCount===0){celebration='✅';message='System bereits optimal!';}else if(optimizedCount<=3){celebration='🚀';message='Kleine Verbesserungen durchgeführt!';}else if(optimizedCount<=10){celebration='⚡';message='Deutliche Optimierung erreicht!';}else{celebration='💎';message='Exzellente Optimierung!';} +modal.innerHTML=`
    ${this.generateConfetti()}
    ${celebration}

    ${message}

    ${optimizedCount}
    Jobs optimiert
    ${data.algorithm?.replace('_',' ')||'Standard'}
    Algorithmus
    Effizienz-Boost erreicht!
    `;document.body.appendChild(modal);this.playSuccessSound();setTimeout(()=>{if(modal&&modal.parentNode){modal.style.opacity='0';modal.style.transform='scale(0.95)';setTimeout(()=>modal.remove(),300);}},20000);} +generateConfetti(){const colors=['#FFD700','#FF6B6B','#4ECDC4','#45B7D1','#96CEB4','#FFEAA7'];let confetti='';for(let i=0;i<50;i++){const color=colors[Math.floor(Math.random()*colors.length)];const delay=Math.random()*5;const duration=4+Math.random()*3;const left=Math.random()*100;confetti+=`
    `;} +return confetti;} +showOptimizationLoading(){const loader=document.createElement('div');loader.id='optimization-loader';loader.className='fixed inset-0 bg-black/50 backdrop-blur-sm z-40 flex items-center justify-center';loader.innerHTML=`

    Optimierung läuft...

    Jobs werden intelligent verteilt

    `;document.body.appendChild(loader);} +hideOptimizationLoading(){const loader=document.getElementById('optimization-loader');if(loader){loader.style.opacity='0';setTimeout(()=>loader.remove(),200);}} +playSuccessSound(){try{const audioContext=new(window.AudioContext||window.webkitAudioContext)();const oscillator=audioContext.createOscillator();const gainNode=audioContext.createGain();oscillator.connect(gainNode);gainNode.connect(audioContext.destination);oscillator.frequency.setValueAtTime(523.25,audioContext.currentTime);oscillator.frequency.setValueAtTime(659.25,audioContext.currentTime+0.1);oscillator.frequency.setValueAtTime(783.99,audioContext.currentTime+0.2);gainNode.gain.setValueAtTime(0.1,audioContext.currentTime);gainNode.gain.exponentialRampToValueAtTime(0.01,audioContext.currentTime+0.5);oscillator.start(audioContext.currentTime);oscillator.stop(audioContext.currentTime+0.5);}catch(e){console.log('Audio-API nicht verfügbar');}} +async performBatchOperation(operation){if(this.selectedJobs.size===0){this.showWarningMessage('Keine Jobs für Batch-Operation ausgewählt');return;} +const jobIds=Array.from(this.selectedJobs);try{const response=await fetch('/api/jobs/batch-operation',{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':this.getCSRFToken()},body:JSON.stringify({job_ids:jobIds,operation:operation})});const data=await response.json();if(data.success){this.showSuccessMessage(`Batch-Operation"${operation}"erfolgreich auf ${jobIds.length}Jobs angewendet`);this.selectedJobs.clear();this.updateBatchCounter();this.refreshCurrentView();}else{this.showErrorMessage(`Batch-Operation fehlgeschlagen:${data.error}`);}}catch(error){console.error('Batch-Operation Fehler:',error);this.showErrorMessage('Netzwerkfehler bei der Batch-Operation');}} +showOptimizationSettings(){this.createOptimizationModal();} +createOptimizationModal(){const modal=document.createElement('div');modal.id='optimization-settings-modal';modal.className='fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4';modal.innerHTML=`

    Optimierungs-Einstellungen

    Konfigurieren Sie die automatische Optimierung für maximale Effizienz

    `;document.body.appendChild(modal);this.loadOptimizationSettingsInModal();} +loadOptimizationSettingsInModal(){document.getElementById('optimization-algorithm').value=this.optimizationSettings.algorithm;document.getElementById('consider-distance').checked=this.optimizationSettings.considerDistance;document.getElementById('minimize-changeover').checked=this.optimizationSettings.minimizeChangeover;document.getElementById('max-batch-size').value=this.optimizationSettings.maxBatchSize;document.getElementById('time-window').value=this.optimizationSettings.timeWindow;} +saveOptimizationSettings(){this.optimizationSettings.algorithm=document.getElementById('optimization-algorithm').value;this.optimizationSettings.considerDistance=document.getElementById('consider-distance').checked;this.optimizationSettings.minimizeChangeover=document.getElementById('minimize-changeover').checked;this.optimizationSettings.maxBatchSize=parseInt(document.getElementById('max-batch-size').value);this.optimizationSettings.timeWindow=parseInt(document.getElementById('time-window').value);localStorage.setItem('myp-optimization-settings',JSON.stringify(this.optimizationSettings));document.getElementById('optimization-settings-modal').remove();this.showSuccessMessage('Optimierungs-Einstellungen gespeichert');if(this.isAutoOptimizationEnabled){this.performAutoOptimization();}} +loadSavedSettings(){const savedAutoOpt=localStorage.getItem('myp-auto-optimization');if(savedAutoOpt!==null){this.isAutoOptimizationEnabled=savedAutoOpt==='true';} +const savedBatchMode=localStorage.getItem('myp-batch-mode');if(savedBatchMode!==null){this.isBatchModeEnabled=savedBatchMode==='true';} +const savedSettings=localStorage.getItem('myp-optimization-settings');if(savedSettings){try{this.optimizationSettings={...this.optimizationSettings,...JSON.parse(savedSettings)};}catch(error){console.error('Fehler beim Laden der Optimierungs-Einstellungen:',error);}}} +updateUI(){const autoOptButton=document.getElementById('auto-opt-toggle');if(autoOptButton){this.updateAutoOptimizationButton(autoOptButton);} +const batchButton=document.getElementById('batch-toggle');if(batchButton){this.updateBatchModeButton(batchButton);} +if(this.isBatchModeEnabled){this.toggleBatchSelection();}} +getCSRFToken(){const token=document.querySelector('meta[name="csrf-token"]');return token?token.getAttribute('content'):'';} +refreshCurrentView(){if(typeof refreshJobs==='function'){refreshJobs();}else if(typeof refreshCalendar==='function'){refreshCalendar();}else if(typeof refreshDashboard==='function'){refreshDashboard();}} +showOptimizationNotification(status,type){const messages={'auto-optimization':{'aktiviert':'🚀 Auto-Optimierung aktiviert - Jobs werden automatisch optimiert','deaktiviert':'⏸️ Auto-Optimierung deaktiviert'},'batch-mode':{'aktiviert':'📦 Batch-Modus aktiviert - Wählen Sie Jobs für Batch-Operationen aus','deaktiviert':'✅ Batch-Modus deaktiviert'}};const message=messages[type]?.[status]||`${type}${status}`;this.showSuccessMessage(message);} +showSuccessMessage(message){if(typeof showFlashMessage==='function'){showFlashMessage(message,'success');}else{console.log('Success:',message);}} +showErrorMessage(message){if(typeof showFlashMessage==='function'){showFlashMessage(message,'error');}else{console.error('Error:',message);}} +showWarningMessage(message){if(typeof showFlashMessage==='function'){showFlashMessage(message,'warning');}else{console.warn('Warning:',message);}} +showToast(message,type='info'){if(typeof showFlashMessage==='function'){showFlashMessage(message,type);}else{console.log(`${type.toUpperCase()}:${message}`);}}} +let optimizationManager;window.toggleAutoOptimization=function(){if(!optimizationManager){optimizationManager=new OptimizationManager();} +optimizationManager.toggleAutoOptimization();};window.toggleBatchMode=function(){if(!optimizationManager){optimizationManager=new OptimizationManager();} +optimizationManager.toggleBatchMode();};window.openBatchPlanningModal=function(){if(!optimizationManager){optimizationManager=new OptimizationManager();} +optimizationManager.showOptimizationSettings();};window.showOptimizationSettings=function(){if(!optimizationManager){optimizationManager=new OptimizationManager();} +optimizationManager.showOptimizationSettings();};document.addEventListener('DOMContentLoaded',function(){optimizationManager=new OptimizationManager();console.log('🎯 Optimierungs-Manager initialisiert');}); \ No newline at end of file diff --git a/backend/static/js/optimization-features.min.js.gz b/backend/static/js/optimization-features.min.js.gz new file mode 100644 index 00000000..1da72ed0 Binary files /dev/null and b/backend/static/js/optimization-features.min.js.gz differ diff --git a/backend/static/js/performance-service-worker.js.gz b/backend/static/js/performance-service-worker.js.gz new file mode 100644 index 00000000..2816cef3 Binary files /dev/null and b/backend/static/js/performance-service-worker.js.gz differ diff --git a/backend/static/js/performance-service-worker.min.js b/backend/static/js/performance-service-worker.min.js new file mode 100644 index 00000000..bdf189ec --- /dev/null +++ b/backend/static/js/performance-service-worker.min.js @@ -0,0 +1,25 @@ +const CACHE_VERSION='v2.0';const STATIC_CACHE=`myp-static-${CACHE_VERSION}`;const DYNAMIC_CACHE=`myp-dynamic-${CACHE_VERSION}`;const FONTS_CACHE=`myp-fonts-${CACHE_VERSION}`;const IMAGES_CACHE=`myp-images-${CACHE_VERSION}`;const CRITICAL_ASSETS=['/','/static/css/critical-inline.css','/static/css/app-bundle.min.css','/static/js/performance-service-worker.js','/static/js/app-bundle.min.js'];const CACHE_STRATEGIES={css:{cache:STATIC_CACHE,strategy:'cache-first',maxAge:86400000},js:{cache:STATIC_CACHE,strategy:'cache-first',maxAge:86400000},fonts:{cache:FONTS_CACHE,strategy:'cache-first',maxAge:2592000000},images:{cache:IMAGES_CACHE,strategy:'cache-first',maxAge:604800000},html:{cache:DYNAMIC_CACHE,strategy:'network-first',maxAge:3600000},api:{cache:DYNAMIC_CACHE,strategy:'network-first',maxAge:300000}};self.addEventListener('install',event=>{console.log('[SW] Installing Performance Service Worker...');event.waitUntil(Promise.all([caches.open(STATIC_CACHE).then(cache=>{console.log('[SW] Pre-caching critical assets...');return cache.addAll(CRITICAL_ASSETS);}),self.skipWaiting()]));});self.addEventListener('activate',event=>{console.log('[SW] Activating Performance Service Worker...');event.waitUntil(Promise.all([caches.keys().then(cacheNames=>{return Promise.all(cacheNames.map(cacheName=>{if(!cacheName.includes(CACHE_VERSION)){console.log('[SW] Deleting old cache:',cacheName);return caches.delete(cacheName);}}));}),self.clients.claim()]));});self.addEventListener('fetch',event=>{if(event.request.method!=='GET')return;const url=new URL(event.request.url);const strategy=getResourceStrategy(url);event.respondWith(handleRequest(event.request,strategy));});function getResourceStrategy(url){const pathname=url.pathname;if(pathname.endsWith('.css'))return CACHE_STRATEGIES.css;if(pathname.endsWith('.js'))return CACHE_STRATEGIES.js;if(pathname.match(/\.(woff2?|ttf|eot)$/))return CACHE_STRATEGIES.fonts;if(pathname.match(/\.(jpg|jpeg|png|gif|webp|svg|ico)$/))return CACHE_STRATEGIES.images;if(pathname.startsWith('/api/'))return CACHE_STRATEGIES.api;return CACHE_STRATEGIES.html;} +async function handleRequest(request,strategy){const cacheName=strategy.cache;switch(strategy.strategy){case'cache-first':return cacheFirst(request,cacheName,strategy.maxAge);case'network-first':return networkFirst(request,cacheName,strategy.maxAge);default:return fetch(request);}} +async function cacheFirst(request,cacheName,maxAge){try{const cache=await caches.open(cacheName);const cachedResponse=await cache.match(request);if(cachedResponse&&!isExpired(cachedResponse,maxAge)){if(isCriticalResource(request.url)){updateInBackground(request,cache);} +return cachedResponse;} +const networkResponse=await fetchWithTimeout(request,5000);if(networkResponse&&networkResponse.ok){await cache.put(request,networkResponse.clone());return networkResponse;} +return cachedResponse||createFallbackResponse(request);}catch(error){console.warn('[SW] Cache-first failed:',error);const cache=await caches.open(cacheName);const cachedResponse=await cache.match(request);return cachedResponse||createFallbackResponse(request);}} +async function networkFirst(request,cacheName,maxAge){try{const networkResponse=await fetchWithTimeout(request,3000);if(networkResponse&&networkResponse.ok){const cache=await caches.open(cacheName);await cache.put(request,networkResponse.clone());return networkResponse;} +throw new Error('Network response not ok');}catch(error){console.warn('[SW] Network-first fallback to cache:',error);const cache=await caches.open(cacheName);const cachedResponse=await cache.match(request);if(cachedResponse&&!isExpired(cachedResponse,maxAge)){return cachedResponse;} +return createFallbackResponse(request);}} +function fetchWithTimeout(request,timeout){return new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error('Timeout')),timeout);fetch(request).then(response=>{clearTimeout(timer);resolve(response);}).catch(error=>{clearTimeout(timer);reject(error);});});} +function isExpired(response,maxAge){const date=response.headers.get('date');if(!date)return false;const responseTime=new Date(date).getTime();const now=Date.now();return(now-responseTime)>maxAge;} +function isCriticalResource(url){return CRITICAL_ASSETS.some(asset=>url.includes(asset));} +async function updateInBackground(request,cache){try{const response=await fetch(request);if(response&&response.ok){await cache.put(request,response);console.log('[SW] Background update completed for:',request.url);}}catch(error){console.warn('[SW] Background update failed:',error);}} +function createFallbackResponse(request){const url=new URL(request.url);if(url.pathname.endsWith('.css')){return new Response(getFallbackCSS(),{headers:{'Content-Type':'text/css'}});} +if(url.pathname.endsWith('.js')){return new Response('console.log("Fallback JS loaded");',{headers:{'Content-Type':'application/javascript'}});} +if(url.pathname.match(/\.(jpg|jpeg|png|gif|webp)$/)){return new Response(getFallbackImage(),{headers:{'Content-Type':'image/svg+xml'}});} +return new Response('Offline',{status:503});} +function getFallbackCSS(){return`body{font-family:system-ui,sans-serif;margin:0;padding:20px;background:#f8fafc;color:#1f2937}.offline{background:#fef3c7;border:1px solid#f59e0b;border-radius:8px;padding:16px;text-align:center;margin:20px 0}.btn{background:#0073ce;color:white;border:none;padding:8px 16px;border-radius:4px;cursor:pointer}.card{background:white;border:1px solid#e5e7eb;border-radius:8px;padding:16px;margin:16px 0}`;} +function getFallbackImage(){return`Bild offline`;} +self.addEventListener('message',event=>{const{type,data}=event.data;switch(type){case'SKIP_WAITING':self.skipWaiting();break;case'CLEAR_ALL_CACHES':clearAllCaches().then(()=>{event.ports[0].postMessage({success:true});});break;case'PREFETCH_URLS':prefetchUrls(data.urls).then(()=>{event.ports[0].postMessage({success:true});});break;case'GET_CACHE_INFO':getCacheInfo().then(info=>{event.ports[0].postMessage(info);});break;}});async function clearAllCaches(){const cacheNames=await caches.keys();await Promise.all(cacheNames.map(name=>caches.delete(name)));console.log('[SW] All caches cleared');} +async function prefetchUrls(urls){for(const url of urls){try{const response=await fetch(url);if(response.ok){const strategy=getResourceStrategy(new URL(url));const cache=await caches.open(strategy.cache);await cache.put(url,response);console.log('[SW] Prefetched:',url);}}catch(error){console.warn('[SW] Prefetch failed for:',url,error);}}} +async function getCacheInfo(){const cacheNames=await caches.keys();const info={};for(const name of cacheNames){const cache=await caches.open(name);const keys=await cache.keys();info[name]={entries:keys.length,urls:keys.map(req=>req.url)};} +return info;} +let performanceData={cacheHits:0,cacheMisses:0,networkRequests:0,backgroundUpdates:0};self.addEventListener('sync',event=>{if(event.tag==='background-sync'){console.log('[SW] Background sync triggered');event.waitUntil(doBackgroundSync());}});async function doBackgroundSync(){console.log('[SW] Background sync completed');} +console.log('[SW] Performance Service Worker loaded and ready');setInterval(async()=>{console.log('[SW] Starting automatic cache cleanup...');await cleanupOldCaches();},24*60*60*1000);async function cleanupOldCaches(){const cacheNames=await caches.keys();const oldCaches=cacheNames.filter(name=>!name.includes(CACHE_VERSION));await Promise.all(oldCaches.map(name=>{console.log('[SW] Cleaning up old cache:',name);return caches.delete(name);}));} \ No newline at end of file diff --git a/backend/static/js/performance-service-worker.min.js.gz b/backend/static/js/performance-service-worker.min.js.gz new file mode 100644 index 00000000..171cec40 Binary files /dev/null and b/backend/static/js/performance-service-worker.min.js.gz differ diff --git a/backend/static/js/printer_monitor.js.gz b/backend/static/js/printer_monitor.js.gz new file mode 100644 index 00000000..ace48c2a Binary files /dev/null and b/backend/static/js/printer_monitor.js.gz differ diff --git a/backend/static/js/printer_monitor.min.js b/backend/static/js/printer_monitor.min.js new file mode 100644 index 00000000..ff0bd8e1 --- /dev/null +++ b/backend/static/js/printer_monitor.min.js @@ -0,0 +1,34 @@ +class PrinterMonitor{constructor(){this.refreshInterval=30000;this.fastRefreshInterval=5000;this.currentInterval=null;this.printers=new Map();this.callbacks=new Set();this.isActive=false;this.useCache=true;this.lastUpdate=null;this.errorCount=0;this.maxErrors=3;this.statusCategories={'online':{label:'Online',color:'success',icon:'🟢'},'offline':{label:'Offline',color:'danger',icon:'🔴'},'standby':{label:'Standby',color:'warning',icon:'🟡'},'unreachable':{label:'Nicht erreichbar',color:'secondary',icon:'⚫'},'unconfigured':{label:'Nicht konfiguriert',color:'info',icon:'🔵'}};console.log('🖨️ PrinterMonitor initialisiert');} +start(){if(this.isActive){console.log('⚠️ PrinterMonitor läuft bereits');return;} +this.isActive=true;this.errorCount=0;console.log('🚀 Starte PrinterMonitor');this.updatePrinterStatus();this.startInterval();document.addEventListener('visibilitychange',this.handleVisibilityChange.bind(this));} +stop(){if(!this.isActive){return;} +this.isActive=false;if(this.currentInterval){clearInterval(this.currentInterval);this.currentInterval=null;} +document.removeEventListener('visibilitychange',this.handleVisibilityChange.bind(this));console.log('⏹️ PrinterMonitor gestoppt');} +startInterval(){if(this.currentInterval){clearInterval(this.currentInterval);} +this.currentInterval=setInterval(()=>{this.updatePrinterStatus();},this.refreshInterval);} +handleVisibilityChange(){if(document.hidden){this.refreshInterval=60000;}else{this.refreshInterval=30000;this.updatePrinterStatus();} +if(this.isActive){this.startInterval();}} +async updatePrinterStatus(){if(!this.isActive){return;} +try{const response=await fetch(`/api/printers/monitor/live-status?use_cache=${this.useCache}`,{method:'GET',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'}});if(!response.ok){throw new Error(`HTTP ${response.status}:${response.statusText}`);} +const data=await response.json();if(data.success){this.processPrinterData(data);this.errorCount=0;}else{throw new Error(data.error||'Unbekannter Fehler');}}catch(error){this.errorCount++;console.error('❌ Fehler beim Abrufen des Drucker-Status:',error);if(this.errorCount>=this.maxErrors){this.refreshInterval=Math.min(this.refreshInterval*2,300000);this.startInterval();this.notifyCallbacks({type:'error',message:`Drucker-Status nicht verfügbar(${this.errorCount}Fehler)`,timestamp:new Date().toISOString()});}}} +processPrinterData(data){const previousPrinters=new Map(this.printers);this.printers.clear();let printersData=null;if(data&&data.printers&&typeof data.printers==='object'){printersData=data.printers;}else if(data&&data.status&&typeof data.status==='object'){printersData=data.status;}else if(data&&typeof data==='object'&&!data.success&&!data.error){printersData=data;} +if(printersData&&typeof printersData==='object'){Object.values(printersData).forEach(printer=>{if(printer&&typeof printer==='object'&&printer.id){this.printers.set(printer.id,{...printer,statusInfo:this.statusCategories[printer.status]||this.statusCategories['offline']});}else{console.warn('⚠️ Ungültiges Drucker-Objekt übersprungen:',printer);}});console.log(`✅ ${this.printers.size}Drucker erfolgreich verarbeitet`);}else{console.warn('⚠️ Keine gültigen Drucker-Daten in Response-Struktur gefunden');console.debug('Response-Struktur:',data);this.notifyCallbacks({type:'warning',message:'Keine Drucker-Daten verfügbar',data:data});return;} +this.lastUpdate=new Date(data.timestamp||Date.now());const changes=this.detectChanges(previousPrinters,this.printers);this.notifyCallbacks({type:'update',printers:this.printers,summary:data.summary,changes:changes,timestamp:this.lastUpdate,cacheUsed:data.cache_used});console.log(`🔄 Drucker-Status aktualisiert:${this.printers.size}Drucker`);} +detectChanges(oldPrinters,newPrinters){const changes=[];newPrinters.forEach((newPrinter,id)=>{const oldPrinter=oldPrinters.get(id);if(!oldPrinter){changes.push({type:'added',printer:newPrinter});}else if(oldPrinter.status!==newPrinter.status){changes.push({type:'status_change',printer:newPrinter,oldStatus:oldPrinter.status,newStatus:newPrinter.status});}});oldPrinters.forEach((oldPrinter,id)=>{if(!newPrinters.has(id)){changes.push({type:'removed',printer:oldPrinter});}});return changes;} +notifyCallbacks(data){this.callbacks.forEach(callback=>{try{callback(data);}catch(error){console.error('❌ Fehler in PrinterMonitor Callback:',error);}});} +onUpdate(callback){if(typeof callback==='function'){this.callbacks.add(callback);}} +offUpdate(callback){this.callbacks.delete(callback);} +async forceUpdate(){const oldUseCache=this.useCache;this.useCache=false;try{await this.updatePrinterStatus();}finally{this.useCache=oldUseCache;}} +async clearCache(){try{const response=await fetch('/api/printers/monitor/clear-cache',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'}});if(response.ok){console.log('🧹 Drucker-Cache geleert');await this.forceUpdate();}else{throw new Error(`HTTP ${response.status}`);}}catch(error){console.error('❌ Fehler beim Leeren des Caches:',error);}} +async getSummary(){try{const response=await fetch('/api/printers/monitor/summary',{method:'GET',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'}});if(response.ok){const data=await response.json();return data.success?data.summary:null;}}catch(error){console.error('❌ Fehler beim Abrufen der Zusammenfassung:',error);} +return null;} +getPrinter(id){return this.printers.get(id);} +getAllPrinters(){return Array.from(this.printers.values());} +getPrintersByStatus(status){return this.getAllPrinters().filter(printer=>printer.status===status);} +getStatusSummary(){const summary={total:this.printers.size,online:0,offline:0,printing:0,standby:0,unreachable:0,unconfigured:0,error:0};this.printers.forEach(printer=>{const status=printer.status;if(summary.hasOwnProperty(status)){summary[status]++;}else{summary.offline++;}});return summary;} +enableFastUpdates(){this.refreshInterval=this.fastRefreshInterval;if(this.isActive){this.startInterval();} +console.log('⚡ Schnelle Updates aktiviert');} +disableFastUpdates(){this.refreshInterval=30000;if(this.isActive){this.startInterval();} +console.log('🐌 Normale Updates aktiviert');} +async initializeAllOutlets(){try{const response=await fetch('/api/printers/monitor/initialize-outlets',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'}});if(response.ok){const data=await response.json();if(data.success){console.log('🔌 Steckdosen-Initialisierung erfolgreich:',data.statistics);this.notifyCallbacks({type:'initialization',results:data.results,statistics:data.statistics,message:data.message});await this.forceUpdate();return data;}else{throw new Error(data.error||'Initialisierung fehlgeschlagen');}}else{throw new Error(`HTTP ${response.status}`);}}catch(error){console.error('❌ Fehler bei Steckdosen-Initialisierung:',error);throw error;}}} +window.printerMonitor=new PrinterMonitor();document.addEventListener('DOMContentLoaded',()=>{const relevantPages=['/printers','/dashboard','/admin','/admin-dashboard'];const currentPath=window.location.pathname;if(relevantPages.some(page=>currentPath.includes(page))){console.log('🖨️ Auto-Start PrinterMonitor für Seite:',currentPath);window.printerMonitor.start();}});window.addEventListener('beforeunload',()=>{if(window.printerMonitor){window.printerMonitor.stop();}});if(typeof module!=='undefined'&&module.exports){module.exports=PrinterMonitor;} \ No newline at end of file diff --git a/backend/static/js/printer_monitor.min.js.gz b/backend/static/js/printer_monitor.min.js.gz new file mode 100644 index 00000000..cbb50f65 Binary files /dev/null and b/backend/static/js/printer_monitor.min.js.gz differ diff --git a/backend/static/js/service-worker.js.gz b/backend/static/js/service-worker.js.gz new file mode 100644 index 00000000..7fa1d60d Binary files /dev/null and b/backend/static/js/service-worker.js.gz differ diff --git a/backend/static/js/service-worker.min.js b/backend/static/js/service-worker.min.js new file mode 100644 index 00000000..afa1cfe5 --- /dev/null +++ b/backend/static/js/service-worker.min.js @@ -0,0 +1,3 @@ +const CACHE_NAME='myp-platform-backup-v1';const ASSETS_TO_CACHE=['/','/dashboard','/static/css/tailwind.min.css','/static/css/tailwind-dark.min.css','/static/js/ui-components.js','/static/js/offline-app.js','/static/favicon.ico'];self.addEventListener('install',(event)=>{console.log('Backup SW: Installing...');event.waitUntil(caches.open(CACHE_NAME).then((cache)=>{console.log('Backup SW: Caching assets');return cache.addAll(ASSETS_TO_CACHE);}).then(()=>{console.log('Backup SW: Assets cached');return self.skipWaiting();}));});self.addEventListener('activate',(event)=>{console.log('Backup SW: Activating...');event.waitUntil(caches.keys().then((cacheNames)=>{return Promise.all(cacheNames.map((cacheName)=>{if(cacheName!==CACHE_NAME){console.log('Backup SW: Deleting old cache',cacheName);return caches.delete(cacheName);}}));}).then(()=>{console.log('Backup SW: Activated');return self.clients.claim();}));});self.addEventListener('fetch',(event)=>{event.respondWith(caches.match(event.request).then((response)=>{if(response){return response;} +return fetch(event.request).then((fetchResponse)=>{if(!fetchResponse||fetchResponse.status!==200||fetchResponse.type!=='basic'){return fetchResponse;} +const responseToCache=fetchResponse.clone();caches.open(CACHE_NAME).then((cache)=>{cache.put(event.request,responseToCache);});return fetchResponse;});}));}); \ No newline at end of file diff --git a/backend/static/js/service-worker.min.js.gz b/backend/static/js/service-worker.min.js.gz new file mode 100644 index 00000000..b3031be4 Binary files /dev/null and b/backend/static/js/service-worker.min.js.gz differ diff --git a/backend/static/js/session-manager.js.gz b/backend/static/js/session-manager.js.gz new file mode 100644 index 00000000..bfaaa0c3 Binary files /dev/null and b/backend/static/js/session-manager.js.gz differ diff --git a/backend/static/js/session-manager.min.js b/backend/static/js/session-manager.min.js new file mode 100644 index 00000000..98fd72aa --- /dev/null +++ b/backend/static/js/session-manager.min.js @@ -0,0 +1,29 @@ +class SessionManager{constructor(){this.isAuthenticated=false;this.maxInactiveMinutes=30;this.heartbeatInterval=5*60*1000;this.warningTime=5*60*1000;this.checkInterval=30*1000;this.heartbeatTimer=null;this.statusCheckTimer=null;this.warningShown=false;this.sessionWarningModal=null;this.init();} +async init(){try{await this.checkAuthenticationStatus();if(this.isAuthenticated){this.startSessionMonitoring();this.createWarningModal();console.log('🔐 Session Manager gestartet');console.log(`📊 Max Inaktivität:${this.maxInactiveMinutes}Minuten`);console.log(`💓 Heartbeat Intervall:${this.heartbeatInterval/1000/60}Minuten`);}else{console.log('👤 Benutzer nicht angemeldet - Session Manager inaktiv');}}catch(error){console.error('❌ Session Manager Initialisierung fehlgeschlagen:',error);}} +async checkAuthenticationStatus(){try{const response=await fetch('/api/session/status',{method:'GET',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'}});if(response.ok){const data=await response.json();if(data.success){this.isAuthenticated=true;this.maxInactiveMinutes=data.session.max_inactive_minutes;console.log('✅ Session Status:',{user:data.user.email,timeLeft:Math.floor(data.session.time_left_seconds/60)+' Minuten',lastActivity:new Date(data.session.last_activity).toLocaleString('de-DE')});return data;}}else if(response.status===401){this.isAuthenticated=false;this.handleSessionExpired('Authentication check failed');}}catch(error){console.error('❌ Fehler beim Prüfen des Session-Status:',error);this.isAuthenticated=false;} +return null;} +startSessionMonitoring(){this.heartbeatTimer=setInterval(()=>{this.sendHeartbeat();},this.heartbeatInterval);this.statusCheckTimer=setInterval(()=>{this.checkSessionStatus();},this.checkInterval);setTimeout(()=>this.sendHeartbeat(),1000);} +async sendHeartbeat(){try{const csrfToken=document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');const headers={'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'};if(csrfToken){headers['X-CSRFToken']=csrfToken;} +const response=await fetch('/api/session/heartbeat',{method:'POST',headers:headers,body:JSON.stringify({timestamp:new Date().toISOString(),page:window.location.pathname,csrf_token:csrfToken})});if(response.ok){const data=await response.json();if(data.success){console.log('💓 Heartbeat gesendet - Session aktiv:',Math.floor(data.time_left_seconds/60)+' Minuten verbleibend');}else{console.warn('⚠️ Heartbeat fehlgeschlagen:',data);}}else if(response.status===401){this.handleSessionExpired('Heartbeat failed - unauthorized');}else if(response.status===400){console.warn('⚠️ CSRF-Token Problem beim Heartbeat - versuche Seite neu zu laden');setTimeout(()=>location.reload(),5000);}}catch(error){console.error('❌ Heartbeat-Fehler:',error);}} +async checkSessionStatus(){try{const sessionData=await this.checkAuthenticationStatus();if(sessionData&&sessionData.session){const timeLeftSeconds=sessionData.session.time_left_seconds;const timeLeftMinutes=Math.floor(timeLeftSeconds/60);if(timeLeftSeconds<=this.warningTime/1000&&timeLeftSeconds>0){if(!this.warningShown){this.showSessionWarning(timeLeftMinutes);this.warningShown=true;}}else if(timeLeftSeconds<=0){this.handleSessionExpired('Session time expired');}else{this.warningShown=false;this.hideSessionWarning();} +this.updateSessionStatusDisplay(sessionData);}}catch(error){console.error('❌ Session-Status-Check fehlgeschlagen:',error);}} +showSessionWarning(minutesLeft){this.hideSessionWarning();this.showToast('Session läuft ab',`Ihre Session läuft in ${minutesLeft}Minuten ab.Möchten Sie verlängern?`,'warning',10000,[{text:'Verlängern',action:()=>this.extendSession()},{text:'Abmelden',action:()=>this.logout()}]);if(this.sessionWarningModal){this.sessionWarningModal.show();this.updateWarningModal(minutesLeft);} +console.log(`⚠️ Session-Warnung:${minutesLeft}Minuten verbleibend`);} +hideSessionWarning(){if(this.sessionWarningModal){this.sessionWarningModal.hide();}} +createWarningModal(){const modalHTML=``;document.body.insertAdjacentHTML('beforeend',modalHTML);document.getElementById('extendSessionBtn').addEventListener('click',()=>{this.extendSession();this.hideSessionWarning();});document.getElementById('logoutBtn').addEventListener('click',()=>{this.logout();});this.sessionWarningModal={element:document.getElementById('sessionWarningModal'),show:()=>{document.getElementById('sessionWarningModal').classList.remove('hidden');},hide:()=>{document.getElementById('sessionWarningModal').classList.add('hidden');}};} +updateWarningModal(minutesLeft){const timeElement=document.getElementById('timeRemaining');if(timeElement){timeElement.textContent=minutesLeft;}} +async extendSession(extendMinutes=30){try{const response=await fetch('/api/session/extend',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({extend_minutes:extendMinutes})});if(response.ok){const data=await response.json();if(data.success){this.warningShown=false;this.showToast('Session verlängert',`Ihre Session wurde um ${data.extended_minutes}Minuten verlängert`,'success',5000);console.log('✅ Session verlängert:',data);}else{this.showToast('Fehler','Session konnte nicht verlängert werden','error');}}else if(response.status===401){this.handleSessionExpired('Extend session failed - unauthorized');}}catch(error){console.error('❌ Session-Verlängerung fehlgeschlagen:',error);this.showToast('Fehler','Session-Verlängerung fehlgeschlagen','error');}} +async logout(){try{this.stopSessionMonitoring();const response=await fetch('/auth/logout',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'}});if(response.ok){window.location.href='/auth/login';}else{window.location.href='/auth/login';}}catch(error){console.error('❌ Logout-Fehler:',error);window.location.href='/auth/login';}} +handleSessionExpired(reason){console.log('🕒 Session abgelaufen:',reason);this.stopSessionMonitoring();this.isAuthenticated=false;this.showToast('Session abgelaufen','Sie wurden automatisch abgemeldet. Bitte melden Sie sich erneut an.','warning',8000);setTimeout(()=>{window.location.href='/auth/login?reason=session_expired';},2000);} +stopSessionMonitoring(){if(this.heartbeatTimer){clearInterval(this.heartbeatTimer);this.heartbeatTimer=null;} +if(this.statusCheckTimer){clearInterval(this.statusCheckTimer);this.statusCheckTimer=null;} +console.log('🛑 Session-Monitoring gestoppt');} +updateSessionStatusDisplay(sessionData){const statusElement=document.getElementById('sessionStatus');if(statusElement){const timeLeftMinutes=Math.floor(sessionData.session.time_left_seconds/60);statusElement.textContent=`Session:${timeLeftMinutes}min`;if(timeLeftMinutes<=5){statusElement.className='text-red-600 font-medium';}else if(timeLeftMinutes<=10){statusElement.className='text-yellow-600 font-medium';}else{statusElement.className='text-green-600 font-medium';}}} +showToast(title,message,type='info',duration=5000,actions=[]){if(window.showToast){window.showToast(message,type,duration);return;} +if(type==='error'||type==='warning'){alert(`${title}:${message}`);}else{console.log(`${title}:${message}`);}} +isLoggedIn(){return this.isAuthenticated;} +start(){if(!this.heartbeatTimer&&this.isAuthenticated){this.startSessionMonitoring();}} +stop(){this.stopSessionMonitoring();} +async extend(minutes=30){return await this.extendSession(minutes);} +async logoutUser(){return await this.logout();}} +document.addEventListener('DOMContentLoaded',()=>{if(!window.location.pathname.includes('/auth/login')){window.sessionManager=new SessionManager();window.addEventListener('beforeunload',()=>{if(window.sessionManager){window.sessionManager.stop();}});document.addEventListener('visibilitychange',()=>{if(window.sessionManager&&window.sessionManager.isLoggedIn()){if(document.hidden){console.log('🙈 Tab versteckt - Session-Monitoring reduziert');}else{console.log('👁️ Tab sichtbar - Session-Check');setTimeout(()=>window.sessionManager.checkSessionStatus(),1000);}}});}});window.SessionManager=SessionManager; \ No newline at end of file diff --git a/backend/static/js/session-manager.min.js.gz b/backend/static/js/session-manager.min.js.gz new file mode 100644 index 00000000..7623c70a Binary files /dev/null and b/backend/static/js/session-manager.min.js.gz differ diff --git a/backend/static/js/sw.js.gz b/backend/static/js/sw.js.gz new file mode 100644 index 00000000..3ec4f02e Binary files /dev/null and b/backend/static/js/sw.js.gz differ diff --git a/backend/static/js/sw.min.js b/backend/static/js/sw.min.js new file mode 100644 index 00000000..a6da7a12 --- /dev/null +++ b/backend/static/js/sw.min.js @@ -0,0 +1,25 @@ +const CACHE_NAME='myp-platform-cache-v1';const STATIC_CACHE='myp-static-v1';const DYNAMIC_CACHE='myp-dynamic-v1';const ASSETS_TO_CACHE=['/','/dashboard','/static/css/tailwind.min.css','/static/css/tailwind-dark.min.css','/static/js/ui-components.js','/static/js/offline-app.js','/static/icons/mercedes-logo.svg','/static/icons/icon-144x144.png','/static/favicon.ico'];const STATIC_PATTERNS=[/\.css$/,/\.js$/,/\.svg$/,/\.png$/,/\.ico$/,/\.woff2?$/];const API_PATTERNS=[/^\/api\//,/^\/auth\//,/^\/api\/jobs/,/^\/api\/printers/,/^\/api\/stats/];self.addEventListener('install',(event)=>{console.log('Service Worker: Installing...');event.waitUntil(caches.open(STATIC_CACHE).then((cache)=>{console.log('Service Worker: Caching static files');return cache.addAll(ASSETS_TO_CACHE);}).then(()=>{console.log('Service Worker: Static files cached');return self.skipWaiting();}).catch((error)=>{console.error('Service Worker: Error caching static files',error);}));});self.addEventListener('activate',(event)=>{console.log('Service Worker: Activating...');event.waitUntil(caches.keys().then((cacheNames)=>{return Promise.all(cacheNames.map((cacheName)=>{if(cacheName!==STATIC_CACHE&&cacheName!==DYNAMIC_CACHE){console.log('Service Worker: Deleting old cache',cacheName);return caches.delete(cacheName);}}));}).then(()=>{console.log('Service Worker: Activated');return self.clients.claim();}));});self.addEventListener('fetch',(event)=>{const{request}=event;const url=new URL(request.url);if(request.method!=='GET'||(url.protocol!=='http:'&&url.protocol!=='https:')){return;} +event.respondWith(fetch(request).then((response)=>{if(response&&response.status===200&&isStaticFile(url.pathname)&&(url.protocol==='http:'||url.protocol==='https:')){const responseClone=response.clone();caches.open(STATIC_CACHE).then((cache)=>{cache.put(request,responseClone);}).catch(err=>{console.warn('Failed to cache response:',err);});} +return response;}).catch(()=>{return caches.match(request);}));});function isStaticFile(pathname){return STATIC_PATTERNS.some(pattern=>pattern.test(pathname));} +function isAPIRequest(pathname){return API_PATTERNS.some(pattern=>pattern.test(pathname));} +function isPageRequest(request){return request.mode==='navigate';} +const STATIC_FILES=['/','/static/css/tailwind.min.css','/static/css/tailwind-dark.min.css','/static/js/ui-components.js','/static/js/offline-app.js','/login','/dashboard'];const API_CACHE_PATTERNS=[/^\/api\/dashboard/,/^\/api\/printers/,/^\/api\/jobs/,/^\/api\/stats/];async function handleStaticFile(request){try{const cachedResponse=await caches.match(request);if(cachedResponse){return cachedResponse;} +const networkResponse=await fetch(request);if(networkResponse.ok){const cache=await caches.open(STATIC_CACHE);cache.put(request,networkResponse.clone());} +return networkResponse;}catch(error){console.error('Service Worker: Error handling static file',error);const cachedResponse=await caches.match(request);if(cachedResponse){return cachedResponse;} +return new Response('Offline - Datei nicht verfügbar',{status:503,statusText:'Service Unavailable'});}} +async function handleAPIRequest(request){const url=new URL(request.url);if(url.protocol==='chrome-extension:'){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'}});}} +try{const networkResponse=await fetch(request);if(networkResponse.ok){if(request.method==='GET'&&shouldCacheAPIResponse(url.pathname)){const cache=await caches.open(DYNAMIC_CACHE);cache.put(request,networkResponse.clone());} +return networkResponse;} +throw new Error(`HTTP ${networkResponse.status}`);}catch(error){console.log('Service Worker: Network failed for API request, trying cache');const cachedResponse=await caches.match(request);if(cachedResponse){const response=cachedResponse.clone();response.headers.set('X-Served-By','ServiceWorker-Cache');return response;} +return new Response(JSON.stringify({error:'Offline - Daten nicht verfügbar',offline:true,timestamp:new Date().toISOString()}),{status:503,statusText:'Service Unavailable',headers:{'Content-Type':'application/json','X-Served-By':'ServiceWorker-Offline'}});}} +async function handlePageRequest(request){try{const networkResponse=await fetch(request);if(networkResponse.ok){const cache=await caches.open(DYNAMIC_CACHE);cache.put(request,networkResponse.clone());return networkResponse;} +throw new Error(`HTTP ${networkResponse.status}`);}catch(error){console.log('Service Worker: Network failed for page request, trying cache');const cachedResponse=await caches.match(request);if(cachedResponse){return cachedResponse;} +return caches.match('/offline.html')||new Response('

    Offline

    Sie sind momentan offline. Bitte überprüfen Sie Ihre Internetverbindung.

    ',{status:200,headers:{'Content-Type':'text/html'}});}} +function shouldCacheAPIResponse(pathname){return API_CACHE_PATTERNS.some(pattern=>pattern.test(pathname));} +self.addEventListener('sync',(event)=>{console.log('Service Worker: Background sync triggered',event.tag);if(event.tag==='background-sync'){event.waitUntil(doBackgroundSync());}});async function doBackgroundSync(){try{const pendingRequests=await getPendingRequests();for(const request of pendingRequests){try{await fetch(request.url,request.options);await removePendingRequest(request.id);console.log('Service Worker: Synced request',request.url);}catch(error){console.error('Service Worker: Failed to sync request',request.url,error);}}}catch(error){console.error('Service Worker: Background sync failed',error);}} +async function getPendingRequests(){return[];} +async function removePendingRequest(id){console.log('Service Worker: Removing pending request',id);} +self.addEventListener('push',(event)=>{console.log('Service Worker: Push notification received');const options={body:'Sie haben neue Benachrichtigungen',icon:'/static/icons/icon-192x192.png',badge:'/static/icons/badge-72x72.png',vibrate:[100,50,100],data:{dateOfArrival:Date.now(),primaryKey:1},actions:[{action:'explore',title:'Anzeigen',icon:'/static/icons/checkmark.png'},{action:'close',title:'Schließen',icon:'/static/icons/xmark.png'}]};event.waitUntil(self.registration.showNotification('MYP Platform',options));});self.addEventListener('notificationclick',(event)=>{console.log('Service Worker: Notification clicked');event.notification.close();if(event.action==='explore'){event.waitUntil(clients.openWindow('/dashboard'));}});self.addEventListener('message',(event)=>{console.log('Service Worker: Message received',event.data);if(event.data&&event.data.type==='SKIP_WAITING'){self.skipWaiting();} +if(event.data&&event.data.type==='CACHE_URLS'){event.waitUntil(cacheUrls(event.data.urls));}});async function cacheUrls(urls){try{const cache=await caches.open(DYNAMIC_CACHE);await cache.addAll(urls);console.log('Service Worker: URLs cached',urls);}catch(error){console.error('Service Worker: Error caching URLs',error);}} +self.addEventListener('periodicsync',(event)=>{console.log('Service Worker: Periodic sync triggered',event.tag);if(event.tag==='content-sync'){event.waitUntil(syncContent());}});async function syncContent(){try{const endpoints=['/api/dashboard','/api/jobs'];for(const endpoint of endpoints){try{const response=await fetch(endpoint);if(response.ok){const cache=await caches.open(DYNAMIC_CACHE);cache.put(endpoint,response.clone());}}catch(error){console.error('Service Worker: Error syncing',endpoint,error);}}}catch(error){console.error('Service Worker: Content sync failed',error);}} +console.log('Service Worker: Script loaded'); \ No newline at end of file diff --git a/backend/static/js/sw.min.js.gz b/backend/static/js/sw.min.js.gz new file mode 100644 index 00000000..1df0a6ff Binary files /dev/null and b/backend/static/js/sw.min.js.gz differ diff --git a/backend/static/js/ui-components.js.gz b/backend/static/js/ui-components.js.gz new file mode 100644 index 00000000..cd887869 Binary files /dev/null and b/backend/static/js/ui-components.js.gz differ diff --git a/backend/static/js/ui-components.min.js b/backend/static/js/ui-components.min.js new file mode 100644 index 00000000..b2a3e7fc --- /dev/null +++ b/backend/static/js/ui-components.min.js @@ -0,0 +1,2 @@ +(function(){'use strict';if(!window.showToast){window.showToast=function(message,type='info',duration=5000){console.log(`🔧 Fallback showToast:[${type.toUpperCase()}]${message}`);};} +window.MYP=window.MYP||{};window.MYP.UI=window.MYP.UI||{};console.log('🎨 MYP UI Components werden geladen...');})(); \ No newline at end of file diff --git a/backend/static/js/ui-components.min.js.gz b/backend/static/js/ui-components.min.js.gz new file mode 100644 index 00000000..af6bcf4d Binary files /dev/null and b/backend/static/js/ui-components.min.js.gz differ diff --git a/backend/static/js/user-dropdown.js.gz b/backend/static/js/user-dropdown.js.gz new file mode 100644 index 00000000..be548121 Binary files /dev/null and b/backend/static/js/user-dropdown.js.gz differ diff --git a/backend/static/js/user-dropdown.min.js b/backend/static/js/user-dropdown.min.js new file mode 100644 index 00000000..e69de29b diff --git a/backend/static/js/user-dropdown.min.js.gz b/backend/static/js/user-dropdown.min.js.gz new file mode 100644 index 00000000..d2751582 Binary files /dev/null and b/backend/static/js/user-dropdown.min.js.gz differ diff --git a/backend/static/js/validation-fix.js.gz b/backend/static/js/validation-fix.js.gz new file mode 100644 index 00000000..c0e9dcb4 Binary files /dev/null and b/backend/static/js/validation-fix.js.gz differ diff --git a/backend/static/js/validation-fix.min.js b/backend/static/js/validation-fix.min.js new file mode 100644 index 00000000..dee9d5d6 --- /dev/null +++ b/backend/static/js/validation-fix.min.js @@ -0,0 +1,17 @@ +(function(){'use strict';console.log('🔍 JavaScript Fehlervalidierung wird gestartet...');function validateShowToast(){const testsPassed=[];if(typeof window.showToast==='function'){testsPassed.push('✅ window.showToast ist verfügbar');}else{console.warn('⚠️ window.showToast nicht verfügbar - aktiviere Fallback');window.showToast=function(message,type='info',duration=5000){console.log(`🔧 Validation-Fallback showToast:[${type.toUpperCase()}]${message}`);if(window.MYP&&window.MYP.UI&&window.MYP.UI.toast&&window.MYP.UI.toast.show){window.MYP.UI.toast.show(message,type,duration);}};testsPassed.push('🔧 Fallback showToast aktiviert');} +if(window.MYP&&window.MYP.UI&&window.MYP.UI.toast){testsPassed.push('✅ MYP.UI.toast ist verfügbar');}else{testsPassed.push('⚠️ MYP.UI.toast noch nicht verfügbar (wird später geladen)');} +return testsPassed;} +function validateManagers(){const testsPassed=[];if(typeof window.jobManager!=='undefined'&&window.jobManager){testsPassed.push('✅ window.jobManager ist verfügbar');}else if(typeof jobManager!=='undefined'&&jobManager){testsPassed.push('✅ jobManager (global) ist verfügbar');}else{testsPassed.push('⚠️ JobManager nicht verfügbar - API-Fallback wird verwendet');} +if(typeof window.refreshJobs==='function'){testsPassed.push('✅ refreshJobs-Funktion ist verfügbar');}else{testsPassed.push('❌ refreshJobs-Funktion fehlt');} +if(typeof window.refreshStats==='function'){testsPassed.push('✅ refreshStats-Funktion ist verfügbar');}else{testsPassed.push('❌ refreshStats-Funktion fehlt');} +return testsPassed;} +async function validateAPIEndpoints(){const testsPassed=[];const endpoints=[{url:'/api/stats',name:'Statistiken'},{url:'/api/jobs',name:'Jobs'}];for(const endpoint of endpoints){try{const response=await fetch(endpoint.url,{method:'HEAD',headers:{'Content-Type':'application/json'}});if(response.ok||response.status===405){testsPassed.push(`✅ ${endpoint.name}API(${endpoint.url})ist erreichbar`);}else{testsPassed.push(`⚠️ ${endpoint.name}API(${endpoint.url})antwortet mit Status ${response.status}`);}}catch(error){testsPassed.push(`❌ ${endpoint.name}API(${endpoint.url})nicht erreichbar:${error.message}`);}} +return testsPassed;} +function validateDOMElements(){const testsPassed=[];const criticalElements=['total-print-time','completed-jobs-count','total-material-used','last-updated-time'];criticalElements.forEach(elementId=>{const element=document.getElementById(elementId);if(element){testsPassed.push(`✅ Element#${elementId}gefunden`);}else{testsPassed.push(`⚠️ Element#${elementId}nicht gefunden`);}});const containers=['.jobs-container','#jobs-container','.job-grid','#refresh-button'];containers.forEach(selector=>{const element=document.querySelector(selector);if(element){testsPassed.push(`✅ Container ${selector}gefunden`);}else{testsPassed.push(`⚠️ Container ${selector}nicht gefunden`);}});return testsPassed;} +function validateErrorHandling(){const testsPassed=[];const testError=new Error('Validation Test Error');window.dispatchEvent(new ErrorEvent('error',{message:'Test error for validation',filename:'validation-fix.js',lineno:1,error:testError}));testsPassed.push('✅ Error Handler Funktionalität getestet');return testsPassed;} +async function runCompleteValidation(){console.log('🔍 Starte vollständige JavaScript-Validierung...');const results={showToast:validateShowToast(),managers:validateManagers(),domElements:validateDOMElements(),errorHandling:validateErrorHandling(),apiEndpoints:await validateAPIEndpoints()};console.group('🔍 Validierungsergebnisse:');Object.keys(results).forEach(category=>{console.group(`📋 ${category}:`);results[category].forEach(result=>console.log(result));console.groupEnd();});console.groupEnd();const allResults=Object.values(results).flat();const successCount=allResults.filter(r=>r.startsWith('✅')).length;const warningCount=allResults.filter(r=>r.startsWith('⚠️')).length;const errorCount=allResults.filter(r=>r.startsWith('❌')).length;console.log(`📊 Validierung abgeschlossen:${successCount}OK,${warningCount}Warnungen,${errorCount}Fehler`);if(typeof window.showToast==='function'){if(errorCount===0){window.showToast(`✅ JavaScript-Validierung erfolgreich:${successCount}Checks bestanden`,'success');}else{window.showToast(`⚠️ JavaScript-Validierung:${errorCount}Probleme gefunden`,'warning');}} +return results;} +function startContinuousMonitoring(){setInterval(()=>{if(typeof window.showToast!=='function'){console.warn('⚠️ showToast-Funktion verloren - reaktiviere Fallback');validateShowToast();} +const errorElements=document.querySelectorAll('.text-red-500');if(errorElements.length>0){console.warn(`⚠️ ${errorElements.length}Fehler-Elemente gefunden`);}},30000);} +window.validateJavaScript=runCompleteValidation;window.validateShowToast=validateShowToast;if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',()=>{setTimeout(runCompleteValidation,2000);startContinuousMonitoring();});}else{setTimeout(runCompleteValidation,2000);startContinuousMonitoring();} +console.log('✅ JavaScript Fehlervalidierung initialisiert');})(); \ No newline at end of file diff --git a/backend/static/js/validation-fix.min.js.gz b/backend/static/js/validation-fix.min.js.gz new file mode 100644 index 00000000..bb185f3d Binary files /dev/null and b/backend/static/js/validation-fix.min.js.gz differ diff --git a/backend/static/manifest.json b/backend/static/manifest.json index c86af0a5..7059126f 100644 --- a/backend/static/manifest.json +++ b/backend/static/manifest.json @@ -1,121 +1,21 @@ { - "name": "Mercedes-Benz MYP Platform", - "short_name": "MYP Platform", - "description": "Mercedes-Benz 3D-Druck Management System", + "name": "MYP Platform", + "short_name": "MYP", + "description": "Mercedes-Benz Druckerverwaltung", "start_url": "/", "display": "standalone", - "background_color": "#000000", - "theme_color": "#000000", - "orientation": "portrait-primary", - "scope": "/", - "lang": "de", - "categories": ["productivity", "business"], + "background_color": "#0f172a", + "theme_color": "#0073ce", "icons": [ { - "src": "/static/icons/icon-72x72.png", - "sizes": "72x72", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "/static/icons/icon-96x96.png", - "sizes": "96x96", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "/static/icons/icon-128x128.png", - "sizes": "128x128", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "/static/icons/icon-144x144.png", - "sizes": "144x144", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "/static/icons/icon-152x152.png", - "sizes": "152x152", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "/static/icons/icon-192x192.png", + "src": "/static/icons/icon-192.png", "sizes": "192x192", - "type": "image/png", - "purpose": "any maskable" + "type": "image/png" }, { - "src": "/static/icons/icon-384x384.png", - "sizes": "384x384", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "/static/icons/icon-512x512.png", + "src": "/static/icons/icon-512.png", "sizes": "512x512", - "type": "image/png", - "purpose": "any maskable" + "type": "image/png" } - ], - "shortcuts": [ - { - "name": "Dashboard", - "short_name": "Dashboard", - "description": "Öffne das Haupt-Dashboard", - "url": "/dashboard", - "icons": [{ "src": "/static/icons/icon-96x96.png", "sizes": "96x96" }] - }, - { - "name": "Neue Anfrage", - "short_name": "Neue Anfrage", - "description": "Erstelle eine neue Gastanfrage", - "url": "/guest/request", - "icons": [{ "src": "/static/icons/icon-96x96.png", "sizes": "96x96" }] - } - ], - "screenshots": [], - "prefer_related_applications": false, - "edge_side_panel": { - "preferred_width": 400 - }, - "launch_handler": { - "client_mode": "navigate-existing" - }, - "protocol_handlers": [ - { - "protocol": "web+myp", - "url": "/handle?url=%s" - } - ], - "file_handlers": [ - { - "action": "/jobs/upload", - "accept": { - "model/gcode": [".gcode", ".g"], - "model/stl": [".stl"], - "model/obj": [".obj"], - "model/3mf": [".3mf"] - } - } - ], - "share_target": { - "action": "/jobs/share", - "method": "POST", - "enctype": "multipart/form-data", - "params": { - "title": "title", - "text": "text", - "url": "url", - "files": [ - { - "name": "files", - "accept": ["model/gcode", "model/stl", "model/obj", "model/3mf"] - } - ] - } - }, - "offline_enabled": true -} \ No newline at end of file + ] +} \ No newline at end of file diff --git a/backend/static/manifest.json.gz b/backend/static/manifest.json.gz new file mode 100644 index 00000000..df34daf5 Binary files /dev/null and b/backend/static/manifest.json.gz differ diff --git a/backend/tailwind.config.js b/backend/tailwind.config.js index e5610e45..0289d835 100644 --- a/backend/tailwind.config.js +++ b/backend/tailwind.config.js @@ -2,324 +2,190 @@ module.exports = { content: [ "./templates/**/*.html", - "./static/**/*.js", - "./static/css/input.css" + "./static/js/**/*.js", + // Aggressive Purging - nur tatsächlich verwendete Klassen ], - darkMode: 'class', + + // Kiosk-optimierte Theme-Reduktion theme: { - extend: { - // Erweiterte Container-Einstellungen für bessere Responsivität - container: { - center: true, - padding: { - DEFAULT: '1rem', - sm: '1.5rem', - lg: '2rem', - xl: '3rem', - '2xl': '4rem', - }, - screens: { - sm: '640px', - md: '768px', - lg: '1024px', - xl: '1280px', - '2xl': '1400px', - }, + // Reduzierte Farbpalette + colors: { + transparent: 'transparent', + current: 'currentColor', + white: '#ffffff', + black: '#000000', + primary: '#0073ce', + 'primary-dark': '#005a9f', + gray: { + 50: '#fafbfc', + 100: '#f3f5f7', + 200: '#e5e7eb', + 300: '#d1d5db', + 400: '#9ca3af', + 500: '#6b7280', + 600: '#4b5563', + 700: '#374151', + 800: '#1f2937', + 900: '#111827', }, - // Erweiterte Breakpoints für bessere Kontrolle - screens: { - 'xs': '480px', // Extra small devices - 'sm': '640px', // Small devices - 'md': '768px', // Medium devices - 'lg': '1024px', // Large devices - 'xl': '1280px', // Extra large devices - '2xl': '1536px', // 2X large devices - '3xl': '1920px', // Ultra wide screens - // Custom breakpoints für spezielle Anwendungsfälle - 'tablet': '768px', - 'laptop': '1024px', - 'desktop': '1280px', - 'wide': '1536px', - // Orientierungs-spezifische Breakpoints - 'landscape': {'raw': '(orientation: landscape)'}, - 'portrait': {'raw': '(orientation: portrait)'}, - // Touch device detection - 'touch': {'raw': '(hover: none) and (pointer: coarse)'}, - 'no-touch': {'raw': '(hover: hover) and (pointer: fine)'}, + blue: { + 500: '#0073ce', + 600: '#005a9f', }, - spacing: { - '72': '18rem', - '84': '21rem', - '96': '24rem', - '128': '32rem', - '144': '36rem', - '160': '40rem', - // Responsive spacing utilities - 'safe-top': 'env(safe-area-inset-top)', - 'safe-bottom': 'env(safe-area-inset-bottom)', - 'safe-left': 'env(safe-area-inset-left)', - 'safe-right': 'env(safe-area-inset-right)', + green: { + 100: '#d1fae5', + 600: '#065f46', }, - fontSize: { - // Responsive font sizes - 'xs': ['0.75rem', { lineHeight: '1rem' }], - 'sm': ['0.875rem', { lineHeight: '1.25rem' }], - 'base': ['1rem', { lineHeight: '1.5rem' }], - 'lg': ['1.125rem', { lineHeight: '1.75rem' }], - 'xl': ['1.25rem', { lineHeight: '1.75rem' }], - '2xl': ['1.5rem', { lineHeight: '2rem' }], - '3xl': ['1.875rem', { lineHeight: '2.25rem' }], - '4xl': ['2.25rem', { lineHeight: '2.5rem' }], - '5xl': ['3rem', { lineHeight: '1' }], - '6xl': ['3.75rem', { lineHeight: '1' }], - '7xl': ['4.5rem', { lineHeight: '1' }], - '8xl': ['6rem', { lineHeight: '1' }], - '9xl': ['8rem', { lineHeight: '1' }], - // Mobile-optimized sizes - 'mobile-xs': ['0.675rem', { lineHeight: '0.875rem' }], - 'mobile-sm': ['0.8rem', { lineHeight: '1.1rem' }], - 'mobile-base': ['0.925rem', { lineHeight: '1.4rem' }], - 'mobile-lg': ['1.05rem', { lineHeight: '1.6rem' }], - }, - colors: { - // Mercedes-Benz Corporate Colors - 'mercedes': { - 'black': '#000000', - 'silver': '#C0C0C0', - 'dark-gray': '#1a1a1a', - 'light-gray': '#f5f5f5', - 'platinum': '#E5E4E2', - 'charcoal': '#36454F', - 'steel': '#71797E' - }, - // Dark Mode optimierte Farben - Verbessert - 'dark': { - 'bg': '#0f172a', // Dunkler Hintergrund - 'bg-secondary': '#131c2e', // Sekundärer Hintergrund noch dunkler - 'surface': '#1e293b', - 'surface-hover': '#334155', - 'card': '#1e293b', // Dunklere Kartenfarbe für besseren Kontrast - 'card-hover': '#334155', // Dunklerer Hover-Zustand - 'text': '#f8fafc', - 'text-secondary': '#e2e8f0', - 'text-muted': '#94a3b8', - 'text-disabled': '#64748b', - 'border': '#334155', - 'border-light': '#475569', - 'hover': '#334155', // Dunklerer Hover-Zustand - 'focus': '#475569', - 'accent': '#4f46e5' - }, - // Light Mode optimierte Farben - Verbessert - 'light': { - 'bg': '#ffffff', - 'bg-secondary': '#f8fafc', - 'surface': '#f7fafc', - 'surface-hover': '#edf2f7', - 'card': '#ffffff', - 'card-hover': '#f7fafc', - 'text': '#1a202c', - 'text-secondary': '#2d3748', - 'text-muted': '#4a5568', - 'text-disabled': '#a0aec0', - 'border': '#e2e8f0', - 'border-light': '#edf2f7', - 'hover': '#f1f5f9', - 'focus': '#e2e8f0', - 'accent': '#3b82f6' - }, - // Status Farben - Erweitert - 'status': { - 'online': '#10b981', - 'offline': '#ef4444', - 'warning': '#f59e0b', - 'info': '#3b82f6', - 'success': '#059669', - 'error': '#dc2626', - 'pending': '#8b5cf6', - 'processing': '#06b6d4', - 'unknown': '#6b7280' - }, - // Printer Status Farben - Erweitert - 'printer': { - 'ready': '#10b981', - 'busy': '#f59e0b', - 'error': '#ef4444', - 'offline': '#6b7280', - 'maintenance': '#8b5cf6', - 'initializing': '#06b6d4' - }, - // Job Status Farben - Erweitert - 'job': { - 'queued': '#6b7280', - 'printing': '#3b82f6', - 'completed': '#10b981', - 'failed': '#ef4444', - 'cancelled': '#f59e0b', - 'paused': '#8b5cf6', - 'retrying': '#f97316' - }, - // Accent Farben - Erweitert - 'accent': { - 'primary': '#3b82f6', - 'secondary': '#8b5cf6', - 'tertiary': '#06b6d4', - 'quaternary': '#10b981', - 'quinary': '#f97316' - }, - // Gradient Farben - Erweitert - 'gradient': { - 'from-blue': '#3b82f6', - 'to-purple': '#8b5cf6', - 'from-green': '#10b981', - 'to-blue': '#3b82f6', - 'from-purple': '#8b5cf6', - 'to-pink': '#ec4899', - 'from-orange': '#f97316', - 'to-red': '#ef4444' - } - }, - typography: { - DEFAULT: { - css: { - 'code::before': { - content: 'none', - }, - 'code::after': { - content: 'none', - }, - }, - }, - }, - boxShadow: { - 'mercedes': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', - 'card-dark': '0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2)', - // Responsive shadows - 'mobile': '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', - 'tablet': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', - 'desktop': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', - }, - // Responsive border radius - borderRadius: { - 'none': '0', - 'sm': '0.125rem', - DEFAULT: '0.25rem', - 'md': '0.375rem', - 'lg': '0.5rem', - 'xl': '0.75rem', - '2xl': '1rem', - '3xl': '1.5rem', - 'full': '9999px', - // Mobile-optimized radius - 'mobile': '0.375rem', - 'tablet': '0.5rem', - 'desktop': '0.75rem', - }, - // Animation improvements - animation: { - 'fade-in': 'fadeIn 0.5s ease-in-out', - 'slide-up': 'slideUp 0.5s ease-out', - 'slide-down': 'slideDown 0.5s ease-out', - 'slide-left': 'slideLeft 0.5s ease-out', - 'slide-right': 'slideRight 0.5s ease-out', - 'scale-in': 'scaleIn 0.3s ease-out', - 'bounce-soft': 'bounceSoft 0.6s ease-out', - 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', - }, - keyframes: { - fadeIn: { - '0%': { opacity: '0' }, - '100%': { opacity: '1' }, - }, - slideUp: { - '0%': { transform: 'translateY(100%)', opacity: '0' }, - '100%': { transform: 'translateY(0)', opacity: '1' }, - }, - slideDown: { - '0%': { transform: 'translateY(-100%)', opacity: '0' }, - '100%': { transform: 'translateY(0)', opacity: '1' }, - }, - slideLeft: { - '0%': { transform: 'translateX(100%)', opacity: '0' }, - '100%': { transform: 'translateX(0)', opacity: '1' }, - }, - slideRight: { - '0%': { transform: 'translateX(-100%)', opacity: '0' }, - '100%': { transform: 'translateX(0)', opacity: '1' }, - }, - scaleIn: { - '0%': { transform: 'scale(0.9)', opacity: '0' }, - '100%': { transform: 'scale(1)', opacity: '1' }, - }, - bounceSoft: { - '0%, 20%, 53%, 80%, 100%': { transform: 'translate3d(0,0,0)' }, - '40%, 43%': { transform: 'translate3d(0,-15px,0)' }, - '70%': { transform: 'translate3d(0,-7px,0)' }, - '90%': { transform: 'translate3d(0,-2px,0)' }, - }, - }, - // Responsive grid templates - gridTemplateColumns: { - 'auto-fit-240': 'repeat(auto-fit, minmax(240px, 1fr))', - 'auto-fit-280': 'repeat(auto-fit, minmax(280px, 1fr))', - 'auto-fit-320': 'repeat(auto-fit, minmax(320px, 1fr))', - 'auto-fill-240': 'repeat(auto-fill, minmax(240px, 1fr))', - 'auto-fill-280': 'repeat(auto-fill, minmax(280px, 1fr))', - 'auto-fill-320': 'repeat(auto-fill, minmax(320px, 1fr))', + red: { + 100: '#fee2e2', + 600: '#991b1b', }, }, + + // Reduzierte Spacing-Skala + spacing: { + '0': '0', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '20': '5rem', + }, + + // Reduzierte Font-Größen + fontSize: { + 'xs': '0.75rem', + 'sm': '0.875rem', + 'base': '1rem', + 'lg': '1.125rem', + 'xl': '1.25rem', + '2xl': '1.5rem', + '3xl': '1.875rem', + }, + + // Minimale Border-Radius + borderRadius: { + 'none': '0', + 'sm': '2px', + 'DEFAULT': '6px', + 'lg': '8px', + 'xl': '12px', + 'full': '9999px', + }, + + // Reduzierte Schatten + boxShadow: { + 'sm': '0 2px 4px rgba(0,0,0,0.05)', + 'DEFAULT': '0 2px 4px rgba(0,0,0,0.05)', + 'lg': '0 4px 8px rgba(0,0,0,0.1)', + }, + + // Minimale Transitions + transitionDuration: { + '75': '75ms', + '100': '100ms', + '150': '150ms', + '200': '200ms', + }, + + extend: {} }, - plugins: [ - // Plugin für erweiterte responsive Utilities - function({ addUtilities, theme }) { - const newUtilities = { - // Safe area utilities für mobile Geräte - '.pt-safe': { - 'padding-top': 'env(safe-area-inset-top)', - }, - '.pb-safe': { - 'padding-bottom': 'env(safe-area-inset-bottom)', - }, - '.pl-safe': { - 'padding-left': 'env(safe-area-inset-left)', - }, - '.pr-safe': { - 'padding-right': 'env(safe-area-inset-right)', - }, - '.p-safe': { - 'padding-top': 'env(safe-area-inset-top)', - 'padding-bottom': 'env(safe-area-inset-bottom)', - 'padding-left': 'env(safe-area-inset-left)', - 'padding-right': 'env(safe-area-inset-right)', - }, - // Responsive text utilities - '.text-responsive': { - 'font-size': 'clamp(0.875rem, 2.5vw, 1.125rem)', - 'line-height': '1.5', - }, - '.text-responsive-lg': { - 'font-size': 'clamp(1.125rem, 3vw, 1.5rem)', - 'line-height': '1.4', - }, - '.text-responsive-xl': { - 'font-size': 'clamp(1.5rem, 4vw, 2.25rem)', - 'line-height': '1.3', - }, - // Container query utilities - '.container-responsive': { - 'container-type': 'inline-size', - }, - // Touch-friendly targets - '.touch-target': { - 'min-height': '44px', - 'min-width': '44px', - }, - '.touch-target-lg': { - 'min-height': '48px', - 'min-width': '48px', - }, - } - addUtilities(newUtilities, ['responsive']) + + // Deaktivierte Utilities für bessere Performance + corePlugins: { + // Nicht benötigte Features deaktivieren + animation: false, // Animationen werden manuell gemacht + backdropBlur: false, // Nicht für Kiosk benötigt + backdropBrightness: false, + backdropContrast: false, + backdropFilter: false, + backdropGrayscale: false, + backdropHueRotate: false, + backdropInvert: false, + backdropOpacity: false, + backdropSaturate: false, + backdropSepia: false, + blur: false, + brightness: false, + contrast: false, + dropShadow: false, + filter: false, + grayscale: false, + hueRotate: false, + invert: false, + saturate: false, + sepia: false, + + // Touch-spezifische Features deaktivieren + touchAction: false, + + // Nicht benötigte Layout-Features + aspectRatio: false, + backdropFilter: false, + + // Reduzierte Transform-Features + scale: false, + skew: false, + transformOrigin: false, + }, + + // Kiosk-spezifische Plugins deaktivieren + plugins: [], + + // Aggressive Purge-Konfiguration + purge: { + enabled: true, + content: [ + './templates/**/*.html', + './static/js/**/*.js', + ], + // Aggressive Purging + options: { + safelist: [ + // Nur essenzielle Klassen behalten + 'container', + 'flex', + 'grid', + 'hidden', + 'block', + 'inline', + 'w-full', + 'h-full', + 'text-center', + 'font-bold', + 'text-primary', + 'bg-white', + 'border', + 'rounded', + 'p-4', + 'm-4', + 'btn', + 'card', + 'nav', + 'header', + 'status-online', + 'status-offline', + 'status-printing', + ], + // Dynamische Klassen-Erkennung + defaultExtractor: content => content.match(/[\w-/:]+(? + + + + + + + + + + + + {% block title %}MYP Druckerverwaltung{% endblock %} + + + + + + + + + + + + + + + + + + + + + {% block extra_head %}{% endblock %} + + + + + + + +
    + +
    + +
    + + +
    + {% block content %}{% endblock %} +
    + + + {% block footer %} +
    +
    + MYP Druckerverwaltung - Kiosk-Modus +
    +
    + {% endblock %} +
    + + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/docs/FRONTEND_API_KONFIGURATION.md b/docs/FRONTEND_API_KONFIGURATION.md new file mode 100644 index 00000000..9eca6f50 --- /dev/null +++ b/docs/FRONTEND_API_KONFIGURATION.md @@ -0,0 +1,168 @@ +# Frontend-API-Konfiguration für separates Backend + +## Übersicht + +Das Frontend wurde konfiguriert, um mit einem Backend auf einem separaten Server unter `https://192.168.0.105:443` zu kommunizieren. + +## Durchgeführte Änderungen + +### 1. API-Basis-URL-Konfiguration (`frontend/src/utils/api-config.ts`) + +**Primäre Backend-URL**: `https://192.168.0.105:443` + +**Fallback-URLs**: +- `https://192.168.0.105` (ohne expliziten Port) +- `https://raspberrypi` (lokaler Raspberry Pi Fallback) + +### 2. SSL-Zertifikat-Handling + +- Selbstsignierte Zertifikate werden automatisch akzeptiert +- `NODE_TLS_REJECT_UNAUTHORIZED = '0'` für Development + +### 3. API-Endpunkt-Konfiguration + +Alle API-Endpunkte wurden angepasst: + +```typescript +export const API_ENDPOINTS = { + PRINTERS: 'https://192.168.0.105:443/api/printers', + JOBS: 'https://192.168.0.105:443/api/jobs', + USERS: 'https://192.168.0.105:443/api/users', + HEALTH: 'https://192.168.0.105:443/health', + AUTH: { + LOGIN: 'https://192.168.0.105:443/api/auth/login', + CALLBACK: 'https://192.168.0.105:443/api/auth/callback', + } +}; +``` + +### 4. Fallback-Mechanismus + +Das Frontend verfügt über einen robusten Fallback-Mechanismus: + +1. **Primäre Verbindung**: `https://192.168.0.105:443` +2. **Fallback 1**: `https://192.168.0.105` (Port 443 automatisch) +3. **Fallback 2**: `https://raspberrypi` (lokaler Pi) + +### 5. Health Check Integration + +Der Health Check wurde angepasst, um die Backend-Konnektivität zu überwachen: + +```typescript +// Health Check prüft Backend unter https://192.168.0.105:443/health +const backendStatus = await fetch(`${backendUrl}/health`); +``` + +## Environment-Variablen (optional) + +Falls gewünscht, kann die Backend-URL über Environment-Variablen überschrieben werden: + +```bash +# .env.local +NEXT_PUBLIC_API_URL=https://192.168.0.105:443 +NODE_TLS_REJECT_UNAUTHORIZED=0 +``` + +## Netzwerk-Konfiguration + +### Backend-Server-Anforderungen + +1. **HTTPS aktiviert** auf Port 443 +2. **CORS konfiguriert** für Frontend-Domain +3. **SSL-Zertifikat** (selbstsigniert oder gültig) +4. **API-Endpunkte** verfügbar unter `/api/*` +5. **Health Check** verfügbar unter `/health` + +### Firewall-Regeln + +```bash +# Backend-Server (192.168.0.105) +sudo ufw allow 443/tcp +sudo ufw allow from 192.168.0.0/24 to any port 443 +``` + +## Entwicklung und Testing + +### Backend-Verbindung testen + +```bash +# SSL-Verbindung testen +curl -k https://192.168.0.105:443/health + +# API-Endpunkte testen +curl -k https://192.168.0.105:443/api/printers +curl -k https://192.168.0.105:443/api/jobs +``` + +### Frontend-Server starten + +```bash +cd frontend +pnpm install +pnpm dev +``` + +Das Frontend läuft auf `http://localhost:3000` und verbindet sich automatisch mit dem Backend unter `https://192.168.0.105:443`. + +## Fehlerbehebung + +### SSL-Zertifikat-Probleme + +Falls SSL-Zertifikat-Fehler auftreten: + +1. **Browser**: Besuche `https://192.168.0.105:443` und akzeptiere das Zertifikat manuell +2. **curl**: Verwende `-k` Flag für unsichere Verbindungen +3. **fetch**: `NODE_TLS_REJECT_UNAUTHORIZED=0` ist bereits gesetzt + +### Verbindungsprobleme + +1. **Netzwerk**: Prüfe ob `192.168.0.105` erreichbar ist +2. **Port**: Stelle sicher, dass Port 443 geöffnet ist +3. **Backend**: Prüfe ob Backend-Service läuft +4. **Logs**: Prüfe Browser-Konsole und Backend-Logs + +### Fallback-Testing + +Das Frontend versucht automatisch Fallback-URLs: + +```javascript +// In Browser-Konsole testen +console.log('API_BASE_URL:', API_BASE_URL); +fetch('/api/health').then(r => r.json()).then(console.log); +``` + +## Produktions-Deployment + +### Umgebungsspezifische Konfiguration + +```bash +# Production +NEXT_PUBLIC_API_URL=https://192.168.0.105:443 + +# Staging +NEXT_PUBLIC_API_URL=https://staging-backend:443 + +# Development +NEXT_PUBLIC_API_URL=https://localhost:5000 +``` + +### Docker-Deployment + +```yaml +# docker-compose.yml +services: + frontend: + build: ./frontend + environment: + - NEXT_PUBLIC_API_URL=https://192.168.0.105:443 + ports: + - "3000:3000" +``` + +--- + +**Status**: ✅ Konfiguration abgeschlossen +**Backend-URL**: `https://192.168.0.105:443` +**Fallback-Support**: Aktiviert +**SSL-Handling**: Selbstsignierte Zertifikate akzeptiert +**Getestet**: Bereit für Integration \ No newline at end of file diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e2bfd039..8a4d89c0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,199 +1,216 @@ # Mercedes-Benz MYP Platform - Roadmap +## Projektübersicht + +Das MYP-Druckerverwaltungssystem besteht aus **zwei eigenständigen Projektarbeiten**: + +- **🏰 Backend-System** (Till Tomczak) - **HAUPT-PRODUKTIONSSYSTEM** +- **📊 Frontend-Analytics** (Torben Haack) - **ANALYSE-PROJEKT** + +Das **Backend ist führend** und stellt die vollständige Hardware-Integration sowie APIs bereit. + ## Aktueller Stand (Dezember 2024) -### ✅ Abgeschlossen +### ✅ Backend-System (Till Tomczak) - VOLLSTÄNDIG ABGESCHLOSSEN -#### Backend-Infrastruktur -- ✅ Flask-App mit SQLAlchemy-Modellen -- ✅ User-Management mit Admin-Rollen -- ✅ Drucker-Management-System -- ✅ Job-Scheduling-System -- ✅ Logging-System implementiert -- ✅ SSL-Konfiguration (teilweise) +#### Produktions-bereite Infrastruktur +- ✅ Flask-App mit vollständigem Web-Interface +- ✅ SQLAlchemy-Modelle mit Business Logic +- ✅ Smart-Plug Integration (TP-Link Tapo P110) +- ✅ Raspberry Pi Kiosk-Modus mit Touch-Interface +- ✅ HTTPS/SSL-Konfiguration mit selbstsignierten Zertifikaten +- ✅ Systemd-Services für automatischen Start +- ✅ Offline-Fähigkeit ohne Internet-Abhängigkeiten +- ✅ Watchdog-Überwachung für Systemstabilität -#### Frontend-Grundlagen -- ✅ Admin-Dashboard HTML-Templates -- ✅ Basis-JavaScript-Funktionalität -- ✅ Responsive Design mit Bootstrap +#### User-Management & Sicherheit +- ✅ Authentifizierung mit BCrypt-Passwort-Hashing +- ✅ Rollenbasierte Zugangskontrolle (Admin/User) +- ✅ Session-Management mit Flask-Login +- ✅ CSRF-Schutz und Input-Validation +- ✅ API-Sicherheit mit Rate-Limiting -#### API-Endpunkte -- ✅ Basis-CRUD-Operationen für alle Entitäten -- ✅ Admin-API-Routen definiert -- ✅ Authentifizierung und Autorisierung +#### Hardware-Integration +- ✅ Automatische Drucker Ein-/Ausschaltung +- ✅ Zeitgesteuerte Smart-Plug-Steuerung +- ✅ Energieverbrauch-Monitoring +- ✅ Scheduler für Hintergrundprozesse +- ✅ Fehlerbehandlung und Recovery-Mechanismen -### 🔧 Kürzlich behoben +#### Web-Interface & APIs +- ✅ Vollständiges responsives Web-Interface +- ✅ REST-API-Endpunkte für alle Funktionen +- ✅ Admin-Dashboard mit Live-Updates +- ✅ Job-Management und Reservierungssystem +- ✅ Gast-Zugang für temporäre Benutzer -#### JavaScript-Probleme -- ✅ `animateCounters` Funktion implementiert -- ✅ `showPrinterModal` Funktion hinzugefügt -- ✅ `animateProgressBars` Funktion erstellt -- ✅ `addHoverEffects` Funktion implementiert +### 🔄 Frontend-Analytics (Torben Haack) - IN ENTWICKLUNG -#### API-Stabilität -- ✅ Verbesserte Fehlerbehandlung in Admin-API-Routen -- ✅ Sichere Admin-Berechtigung-Prüfung -- ✅ Fallback-Mechanismen für System-Monitoring -- ✅ Test-Route für Admin-API-Debugging +#### Datenanalyse-Infrastruktur +- ✅ Next.js React-Anwendung mit TypeScript +- ✅ Drizzle ORM mit SQLite-Datenbank +- ✅ Tailwind CSS für responsives Design +- ✅ Radix UI Komponenten-Bibliothek +- 🔄 Datenvisualisierung mit Recharts +- 🔄 Statistische Auswertungen und Reports -#### Infrastruktur -- ✅ Favicon-Route hinzugefügt -- ✅ Verbesserte Logging-Konfiguration -- ✅ COMMON_ERRORS.md aktualisiert +#### Analytics-Features +- 🔄 Erweiterte Dashboard-Visualisierungen +- 🔄 Datenexport und -import-Funktionen +- 🔄 Prozessoptimierung-Tools +- 🔄 Trend-Analysen und Forecasting +- ⏳ Integration mit Backend-APIs -## 🔄 Aktuell in Bearbeitung +## 🎯 Nächste Prioritäten -### Kritische Probleme -1. **SSL/HTTPS-Konfiguration** - - Server läuft auf Port 5000 statt 8443 - - SSL-Zertifikate müssen überprüft werden - - Port-Konsistenz zwischen Frontend und Backend +### Backend-System (Till Tomczak) - WARTUNG & OPTIMIERUNG -2. **Admin-Dashboard-Stabilität** - - Live-Updates funktionieren teilweise - - Einige API-Endpunkte geben noch 404-Fehler zurück - - Modal-Funktionalität muss getestet werden +#### Kurzfristig (1-2 Wochen) +- [ ] **Performance-Monitoring** - Systemlast-Überwachung implementieren +- [ ] **Backup-Automatisierung** - Tägliche Datenbank-Backups einrichten +- [ ] **Logging-Optimierung** - Log-Rotation und erweiterte Fehlerberichterstattung +- [ ] **API-Dokumentation** - Swagger/OpenAPI-Spezifikation erstellen -3. **Datenbankverbindung** - - Session-Management optimieren - - Connection-Pool-Konfiguration - - Backup-Strategien implementieren +#### Mittelfristig (1-2 Monate) +- [ ] **Erweiterte Hardware-Integration** - Zusätzliche Smart-Plug-Modelle unterstützen +- [ ] **Mehrsprachigkeit** - Internationalisierung für weitere Sprachen +- [ ] **Mobile-Optimierung** - PWA-Features für Smartphone-Nutzung +- [ ] **Drucker-Status-Integration** - Direkte Kommunikation mit 3D-Druckern -## 📋 Nächste Prioritäten +### Frontend-Analytics (Torben Haack) - FERTIGSTELLUNG -### Kurzfristig (1-2 Wochen) +#### Kurzfristig (1-2 Wochen) +- [ ] **Dashboard-Vervollständigung** - Alle geplanten Visualisierungen implementieren +- [ ] **Backend-API-Integration** - Datenabfrage vom Backend-System +- [ ] **Datenexport-Funktionen** - CSV/Excel-Export für Reports +- [ ] **User-Interface-Finalisierung** - UX-Optimierung und Testing -#### 1. SSL/HTTPS-Stabilisierung -- [ ] SSL-Zertifikate validieren -- [ ] Port-Konfiguration vereinheitlichen -- [ ] Reverse-Proxy-Setup dokumentieren -- [ ] Fallback-Mechanismus für HTTP/HTTPS +#### Mittelfristig (1-2 Monate) +- [ ] **Erweiterte Analytics** - Machine Learning für Nutzungsvorhersagen +- [ ] **Reporting-System** - Automatische Berichte und E-Mail-Versand +- [ ] **Datenintegration** - Verbindung zu externen Datenquellen +- [ ] **Performance-Optimierung** - Caching und Lazy Loading -#### 2. Admin-Dashboard-Vervollständigung -- [ ] Alle Modal-Funktionen testen -- [ ] Live-Update-Mechanismus stabilisieren -- [ ] Drucker-Management-Funktionen verifizieren -- [ ] Benutzer-Management-Interface finalisieren +## 🔧 System-Integration -#### 3. API-Konsistenz -- [ ] Alle 404-Fehler beheben -- [ ] Einheitliche Error-Response-Struktur -- [ ] API-Dokumentation erstellen -- [ ] Rate-Limiting implementieren +### Backend als Datenquelle +Das Backend-System stellt REST-APIs bereit, die von verschiedenen Clients genutzt werden können: -### Mittelfristig (2-4 Wochen) +``` +Backend-APIs (/api/*) +├── Direkter Zugriff → Backend Web-Interface (Produktiv-Einsatz) +├── API-Integration → Frontend-Analytics (Datenanalyse) +└── Externe Clients → Andere Anwendungen/Tools +``` -#### 1. Performance-Optimierung -- [ ] Database-Query-Optimierung -- [ ] Frontend-Asset-Minimierung -- [ ] Caching-Strategien implementieren -- [ ] Load-Testing durchführen +### Datenfluss-Architektur +``` +[Smart-Plugs] ←→ [Backend-System] ←→ [Web-Interface] + ↓ + [REST-APIs] + ↓ + [Frontend-Analytics] → [Reports & Visualisierungen] +``` -#### 2. Sicherheit -- [ ] Security-Audit durchführen -- [ ] CSRF-Protection verstärken -- [ ] Input-Validation verbessern -- [ ] Session-Security optimieren +## 📊 Metriken & Erfolgskriterien -#### 3. Monitoring & Analytics -- [ ] System-Monitoring-Dashboard -- [ ] Performance-Metriken sammeln -- [ ] Error-Tracking implementieren -- [ ] Usage-Analytics hinzufügen +### Backend-System (Produktions-KPIs) +- **Uptime**: > 99.9% (24/7-Betrieb) +- **Response-Zeit**: < 200ms für Web-Interface +- **Hardware-Integration**: 100% zuverlässige Smart-Plug-Steuerung +- **Sicherheit**: 0 kritische Sicherheitslücken +- **Benutzer-Zufriedenheit**: > 4.5/5 bei Nutzerfeedback -### Langfristig (1-3 Monate) +### Frontend-Analytics (Analytics-KPIs) +- **Datenverarbeitungszeit**: < 5 Sekunden für Standard-Reports +- **Visualisierung-Performance**: < 1 Sekunde Ladezeit für Charts +- **Datengenauigkeit**: 100% konsistente Daten mit Backend +- **Benutzerfreundlichkeit**: Intuitive Bedienung ohne Schulung +- **Feature-Vollständigkeit**: Alle geplanten Analytics-Funktionen -#### 1. Feature-Erweiterungen -- [ ] Mobile-App-Unterstützung -- [ ] Push-Notifications -- [ ] Advanced-Scheduling-Features -- [ ] Reporting-System +## 🛠️ Entwicklungsrichtlinien -#### 2. Skalierung -- [ ] Multi-Tenant-Architektur -- [ ] Microservices-Migration -- [ ] Container-Orchestrierung -- [ ] Cloud-Deployment +### Backend-Entwicklung (Till Tomczak) +- **Stabilität**: Produktions-System darf nicht beeinträchtigt werden +- **Sicherheit**: Alle Änderungen müssen Security-Review durchlaufen +- **Performance**: Optimierungen nur nach Lasttest-Validierung +- **Dokumentation**: Vollständige API-Dokumentation für alle Endpunkte -#### 3. Integration -- [ ] LDAP/Active Directory-Integration -- [ ] Drucker-API-Integration -- [ ] ERP-System-Anbindung -- [ ] Workflow-Automation +### Frontend-Entwicklung (Torben Haack) +- **Datenintegrität**: Konsistenz mit Backend-Datenmodell sicherstellen +- **Performance**: Effiziente Datenverarbeitung für große Datensätze +- **Benutzerfreundlichkeit**: Intuitive UI/UX für Analytics-Workflows +- **Erweiterbarkeit**: Modularer Aufbau für zukünftige Analytics-Features -## 🚨 Bekannte Probleme +## 🔄 Deployment-Strategien -### Kritisch -- SSL-Konfiguration instabil -- Einige Admin-API-Endpunkte nicht erreichbar -- Live-Updates funktionieren nicht zuverlässig +### Backend (Produktions-Deployment) +```bash +# Raspberry Pi Produktions-Update +cd backend +git pull origin main +sudo systemctl restart myp-https.service +sudo systemctl status myp-https.service +``` -### Wichtig -- Favicon-Requests verursachen 404-Fehler (behoben) -- JavaScript-Funktionen fehlen (behoben) -- Admin-Berechtigung-Prüfung inkonsistent (verbessert) +### Frontend (Analytics-Deployment) +```bash +# Next.js Build und Deployment +cd frontend +pnpm build +pnpm start +# oder Docker-Container für Produktions-Umgebung +``` -### Niedrig -- Logging-Performance bei hoher Last -- Frontend-Animationen können optimiert werden -- Dokumentation unvollständig +## 🚨 Risikomanagement -## 🎯 Erfolgskriterien +### Backend-Risiken (Mitigation) +- **Hardware-Ausfall**: Backup-Raspberry Pi bereithalten +- **Smart-Plug-Verbindung**: Manuelle Fallback-Steuerung +- **Datenbank-Korruption**: Tägliche automatische Backups +- **Sicherheitslücke**: Regelmäßige Security-Audits -### Phase 1 (Stabilisierung) -- [ ] Alle Admin-Dashboard-Funktionen arbeiten fehlerfrei -- [ ] SSL/HTTPS funktioniert zuverlässig -- [ ] Keine 404-Fehler in der Konsole -- [ ] Live-Updates funktionieren in Echtzeit +### Frontend-Risiken (Mitigation) +- **Dateninkonsistenz**: Validierung gegen Backend-APIs +- **Performance-Probleme**: Progressives Laden großer Datensätze +- **Browser-Kompatibilität**: Cross-Browser-Testing +- **Abhängigkeiten**: Regelmäßige Dependency-Updates -### Phase 2 (Optimierung) -- [ ] Seitenladezeiten unter 2 Sekunden -- [ ] 99.9% Uptime -- [ ] Alle Security-Scans bestanden -- [ ] Performance-Benchmarks erreicht +## 📅 Zeitplan & Meilensteine -### Phase 3 (Erweiterung) -- [ ] Mobile-Responsive Design -- [ ] Multi-Language-Support -- [ ] Advanced-Features implementiert -- [ ] Skalierbarkeit nachgewiesen +### Q1 2025 - Backend Optimierung +- **Januar**: Performance-Monitoring und Backup-Automatisierung +- **Februar**: API-Dokumentation und zusätzliche Hardware-Tests +- **März**: Langzeit-Stabilitätstests und Optimierungen -## 📊 Metriken & KPIs +### Q1 2025 - Frontend Completion +- **Januar**: Dashboard-Vervollständigung und Backend-Integration +- **Februar**: Datenexport-Features und UX-Optimierung +- **März**: Testing, Dokumentation und Deployment-Vorbereitung -### Technische Metriken -- Response-Zeit: < 200ms für API-Calls -- Uptime: > 99.9% -- Error-Rate: < 0.1% -- Database-Query-Zeit: < 50ms +### Q2 2025 - Integration & Expansion +- **April**: Vollständige System-Integration testen +- **Mai**: Erweiterte Features und Performance-Optimierung +- **Juni**: Produktions-Rollout und User-Training -### Business-Metriken -- Benutzer-Zufriedenheit: > 4.5/5 -- Feature-Adoption-Rate: > 80% -- Support-Tickets: < 5 pro Woche -- System-Effizienz: > 95% +## 📚 Dokumentations-Roadmap -## 🔧 Entwicklungsrichtlinien +### Backend-Dokumentation (Till Tomczak) +- [ ] **API-Spezifikation** - OpenAPI/Swagger-Dokumentation +- [ ] **Hardware-Setup-Guide** - Raspberry Pi Konfiguration +- [ ] **Troubleshooting-Guide** - Häufige Probleme und Lösungen +- [ ] **Sicherheits-Handbuch** - Security Best Practices -### Code-Qualität -- Alle Funktionen müssen getestet werden -- Code-Coverage > 80% -- Linting-Regeln befolgen -- Dokumentation für alle neuen Features - -### Deployment -- Staging-Environment für Tests -- Automated-Testing vor Deployment -- Rollback-Strategien definiert -- Monitoring nach Deployment - -### Sicherheit -- Regelmäßige Security-Audits -- Dependency-Updates -- Penetration-Testing -- Compliance-Checks +### Frontend-Dokumentation (Torben Haack) +- [ ] **Analytics-Benutzerhandbuch** - Dashboard-Funktionen erklärt +- [ ] **Datenmodell-Dokumentation** - Datenstrukturen und -flüsse +- [ ] **Entwickler-Guide** - Setup und Erweiterung der Analytics +- [ ] **Report-Templates** - Vorgefertigte Analyse-Vorlagen --- -**Letzte Aktualisierung:** Dezember 2024 -**Nächste Review:** In 2 Wochen -**Verantwortlich:** Entwicklungsteam Mercedes-Benz MYP \ No newline at end of file +**Backend-Verantwortlich**: Till Tomczak (Produktions-System) +**Frontend-Verantwortlich**: Torben Haack (Analytics-System) +**Letzte Aktualisierung**: Dezember 2024 +**Nächste Review**: In 2 Wochen +**System-Architektur**: Backend-zentriert mit optionaler Frontend-Analytics \ No newline at end of file diff --git a/docs/VERBINDUNGSTEST.md b/docs/VERBINDUNGSTEST.md new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/docs/VERBINDUNGSTEST.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/myp_documentation.md b/docs/myp_documentation.md index ce6d60fb..c8e75504 100644 --- a/docs/myp_documentation.md +++ b/docs/myp_documentation.md @@ -1,53 +1,116 @@ --- title: "MYP – Manage Your Printer" -subtitle: "Abschlussprojekt der IHK-Prüfung" -author: "Till Tomczak" -date: "2023" -subject: "Fachinformatiker für digitale Vernetzung" +subtitle: "Abschlussprojekte der IHK-Prüfung" +author: "Till Tomczak (Backend-System) & Torben Haack (Frontend-Analytics)" +date: "2023-2024" +subject: "Fachinformatiker für digitale Vernetzung & Daten- und Prozessanalyse" company: "Mercedes-Benz" --- -# MYP – Manage Your Printer: Projektzusammenfassung +# MYP – Manage Your Printer: Projekt-Gesamtdokumentation -## 1. Projektüberblick +## 1. Projektübersicht -MYP (*Manage Your Printer*) ist ein System zur zentralen Verwaltung und Steuerung von 3D-Druckern mittels Smart-Plug-Technologie. Es digitalisiert den Reservierungsprozess für mehrere 3D-Drucker und ermöglicht eine automatisierte Schaltung der Drucker über WLAN-Steckdosen (TP-Link Tapo P110). Zu den Kernfunktionen gehören unter anderem: +**MYP (Manage Your Printer)** ist ein System zur **zentralen Verwaltung und Steuerung von 3D-Druckern mittels Smart-Plug-Technologie**. Es digitalisiert den Reservierungsprozess für mehrere 3D-Drucker und ermöglicht eine **automatisierte Schaltung der Drucker über WLAN-Steckdosen (TP-Link Tapo P110)**. -* **Benutzer- und Rechteverwaltung:** Registrierung, Login und Rollenkonzept (Admin/Benutzer). Administrierende können Drucker und Nutzer verwalten, während Standard-Benutzer Reservierungen anlegen und ihre Druckjobs verwalten dürfen. -* **Drucker- und Auftragsmanagement:** Zentrales **Reservierungssystem** ermöglicht es Nutzern, Zeitfenster für Druckaufträge zu buchen. Die Drucker werden automatisch zum Start einer Reservierung eingeschaltet und nach Ende wieder ausgeschaltet. Es findet **keine direkte Kommunikation mit den 3D-Druckern** statt – anstatt die Drucker selbst anzusprechen, wird ausschließlich deren Stromzufuhr über die Smart-Plug-Steckdosen gesteuert. Dadurch bleibt das System herstellerunabhängig und einfach, da keine Eingriffe in die Druckerhardware erfolgen müssen. -* **Statistikerfassung:** Das System protokolliert Nutzungszeiten und abgeschlossene Druckaufträge, um Auswertungen (z.B. Gesamtdruckzeit pro Zeitraum) zu ermöglichen. -* **Offline-Fähigkeit & Kiosk-Modus:** MYP ist so konzipiert, dass es nach der Installation ohne Internetzugang betrieben werden kann. Ein dedizierter Kiosk-Modus (auf einem Raspberry Pi) erlaubt die Anzeige eines Dashboards im Vollbild (z.B. auf einem Monitor in der Nähe der Drucker), um aktuelle Druckerbelegungen und den Systemstatus anzuzeigen. +Das **MYP-Druckerverwaltungssystem** besteht aus **zwei sich ergänzenden IHK-Abschlussprojekten**: -Technologisch besteht MYP aus einem **Flask-basierten Backend** in Python mit einer leichten SQLite-Datenbank zur Datenspeicherung. Über eine RESTful API kommuniziert das Backend mit dem Frontend sowie optional mit externen Diensten. Die Kommunikation kann verschlüsselt über HTTPS erfolgen (selbstsigniertes Zertifikat) und unterstützt einen **Progressive Web App** (PWA) Ansatz, sodass die Web-Oberfläche auch offline im Browser funktionieren kann. Insgesamt stellt MYP eine cyber-physische Lösung dar, die IT-System (Reservierungsplattform) und Hardware (Smart-Plugs und Drucker) eng vernetzt. +### 🏗️ **Backend-System** (Till Tomczak) - **KERN-INFRASTRUKTUR** +- **Entwickler**: Till Tomczak +- **Fachrichtung**: Fachinformatiker für digitale Vernetzung +- **Technologie**: **Flask-basiertes Backend in Python** mit **SQLite-Datenbank** +- **Fokus**: Cyber-physische Vernetzung und Hardware-Integration -## 2. Ausgangssituation und Zielsetzung +### 📊 **Frontend-System** (Torben Haack) - **BENUTZEROBERFLÄCHE & ANALYTICS** +- **Entwickler**: Torben Haack +- **Fachrichtung**: Fachinformatiker für Daten- und Prozessanalyse +- **Technologie**: **Next.js-basierte Webanwendung** mit erweiterten Analytics +- **Fokus**: Progressive Web App und Datenvisualisierung -**Ausgangssituation:** In der Ausgangslage wurden die verfügbaren 3D-Drucker des Unternehmens dezentral und manuell verwaltet. Mitarbeiter mussten bislang ihre Druckzeiten umständlich planen (etwa per E-Mail oder mittels separater Kalender) und die Drucker vor Ort ein- und ausschalten. Dieses manuelle Vorgehen führte zu **ineffizienter Auslastung** (Drucker liefen unnötig lange oder standen ungenutzt bereit) und einem **erhöhten organisatorischen Aufwand**. Außerdem bestand keine automatische Rückmeldung über den Druckerzustand – es war unklar, ob ein Drucker gerade druckt, verfügbar ist oder vielleicht vergessen wurde auszuschalten. Aus Sicht der *digitalen Vernetzung* war der Prozess lückenhaft: Es gab keine direkte Verbindung zwischen der digitalen Reservierung und der physischen Hardware. +## 2. System-Architektur & Kernfunktionen -**Ziel des Projekts:** Die Projektarbeit zielte darauf ab, den 3D-Drucker-Reservierungsprozess zu digitalisieren und effizienter zu gestalten. Konkret sollte eine Plattform geschaffen werden, die **Benutzerreservierungen mit der physischen Verfügbarkeit der Drucker verknüpft**. Durch die **Etablierung einer cyber-physischen Kommunikation** sollten relevante Hardwarekomponenten (insb. smarte Steckdosen für die Drucker) automatisiert angesteuert werden, um menschliche Eingriffe zu minimieren. Wichtige Zielsetzungen waren: +### Backend-System (Till Tomczak) - Cyber-Physische Vernetzung -* **Automatisierung:** Drucker sollen sich zum reservierten Zeitpunkt **automatisch einschalten** und nach Ende des Zeitfensters abschalten. Damit wird Energie gespart und Sicherheit erhöht (kein unbeaufsichtigtes Laufenlassen). -* **Zentrale Verwaltung:** Alle Drucker und Reservierungen sollten in einem **zentralen System** verwaltbar sein, mit Überblick über aktuelle Belegungen und Historie. -* **Benutzerfreundlichkeit:** Endnutzer sollten einfach über eine Web-Oberfläche einen Drucker buchen können, ohne sich um technische Details kümmern zu müssen. Das System übernimmt im Hintergrund die zeitgesteuerte Schaltung. -* **Offline-Betrieb:** Da das Projekt in einem abgeschotteten Umfeld (z.B. Prüfungsumgebung, internes Firmennetz ohne Internet) laufen muss, sollte die Lösung **ohne Internetanbindung** funktionsfähig sein. Ein eigenständiges lokales WLAN bzw. Netzwerksegment war hierfür einzuplanen. -* **Sicherheit und Kontrolle:** Durch Rollen und Authentifizierung sollte nur berechtigten Personen der Zugriff erlaubt werden. Zusätzlich sollen Protokollierung und Statistiken Transparenz über die Nutzung schaffen. +**Kernfunktionen des Backend-Systems:** -Mit MYP wird ein Schritt hin zur **Industrie-4.0-Umgebung** gemacht, in der betriebliche Ressourcen (hier: 3D-Drucker) digital vernetzt und automatisiert verwaltet werden. Das Projekt bildet die Grundlage, auf der künftig weitere Verbesserungen wie Echtzeit-Status der Drucker oder eine tiefere Integration in bestehende Systeme aufbauen können. +#### **Benutzer- und Rechteverwaltung** +- **Registrierung, Login und Rollenkonzept** (Admin/Benutzer) +- **Administrierende** können Drucker und Nutzer verwalten +- **Standard-Benutzer** können Reservierungen anlegen und Druckjobs verwalten -## 3. Planung und technische Architektur +#### **Drucker- und Auftragsmanagement** +- **Zentrales Reservierungssystem** für Zeitfenster-Buchungen +- **Automatische Drucker-Schaltung**: Einschalten zum Reservierungsstart, Ausschalten nach Ende +- **Herstellerunabhängig**: **Keine direkte Kommunikation mit 3D-Druckern** - ausschließlich Stromsteuerung über Smart-Plug-Steckdosen +- **Einfache Integration**: Keine Eingriffe in die Druckerhardware erforderlich -**Planungsphase:** Zu Beginn wurden die bestehende Infrastruktur und Anforderungen analysiert. Es stellte sich heraus, dass **mehrere 3D-Drucker** vorhanden sind, die jedoch bisher *stand-alone* betrieben wurden (teils mit eigener Netzwerkfähigkeit, die aber ungenutzt blieb). Als erstes wurden zusammen mit der IT-Abteilung die Rahmenbedingungen geklärt: Es musste ein geeigneter **Netzwerkbereich** für das neue System bereitgestellt werden (inkl. IP-Adressen und ggf. DNS-Namen für den Server) und eine Lösung für den **offline Betrieb** gefunden werden. Ein *Netzwerkplan* wurde erstellt, um die Integration von MYP in das Firmennetz sowie ein paralleles isoliertes WLAN zu skizzieren (siehe Abschnitt 4). Parallel dazu wurden die **Systemanforderungen** konkretisiert (siehe unten). +#### **Statistikerfassung** +- **Protokollierung** von Nutzungszeiten und abgeschlossenen Druckaufträgen +- **Auswertungen** (z.B. Gesamtdruckzeit pro Zeitraum) +- **REST-APIs** für erweiterte Analytics-Integration -**Technologie- und Komponentenauswahl:** Auf Basis der Anforderungen entschied man sich für folgende technische Architektur: +#### **Offline-Fähigkeit & Kiosk-Modus** +- **Autonomer Betrieb** ohne Internetzugang nach Installation +- **Raspberry Pi Kiosk-Modus**: Vollbild-Dashboard vor Ort +- **Touch-Interface** für aktuelle Druckerbelegungen und Systemstatus -* **Hardware:** Es wurde ein *Raspberry Pi 4* als Zentrale (`Controller Pi`) eingesetzt. Dieser Einplatinencomputer ist leistungsfähig genug für den Flask-Server und den Browser im Kiosk-Modus, bietet WLAN und LAN Schnittstellen und ist aufgrund seiner Größe und Kosten ideal für diese Anwendung. Die Entscheidung fiel bewusst auf den Raspberry Pi mit dem Betriebssystem *Raspberry Pi OS (Raspbian)*, da hier eine schnelle Einrichtung und viele Community-Ressourcen verfügbar sind (alternativ erwogene Systeme wie OpenSUSE oder NixOS wurden verworfen zugunsten der spezifisch angepassten Raspberry-Distribution). -* **Smart Plugs:** Zur **Schaltung der Drucker** wurden *TP-Link Tapo P110* WLAN-Steckdosen ausgewählt. Diese Modelle bieten neben Schaltfunktionen auch Energiedaten und lassen sich über eine lokale API ansteuern. Wichtig war, dass die Steckdosen **lokal im LAN** angesprochen werden können, ohne auf Cloud-Services angewiesen zu sein. Die P110 erfüllen dies mittels eines herstellerspezifischen Protokolls, das mit Authentifizierung und Verschlüsselung arbeitet. -* **Software Backend:** Die Server-Anwendung wurde in Python mit dem **Flask**-Framework realisiert. Flask eignet sich durch seine Leichtgewichtigkeit für Embedded-Umgebungen und ermöglicht dennoch eine saubere **REST-API**-Implementierung. Für die Datenpersistenz wird eine **SQLite-Datenbank** genutzt, die direkt im Dateisystem des Pi liegt – ideal für eine lokale Einzelplatz-Lösung ohne separaten DB-Server. Das Backend ist modular aufgebaut (siehe Abschnitt 5) und läuft als Daemon (Systemd-Service) auf dem Raspberry Pi. -* **Software Frontend:** Als Benutzeroberfläche dient eine Web-Anwendung (HTML5/JavaScript/CSS), die entweder in eine bestehende Reservierungsplattform integriert werden kann oder eigenständig als PWA läuft. Während der Entwicklung wurde besonders darauf geachtet, dass das Frontend im **Offline-Modus** funktioniert, um z.B. im Prüfungsraum ohne Internet dennoch eine Bedienoberfläche zu haben. Technisch kamen moderne Web-Technologien (u.a. **Tailwind CSS** für responsives Design und ein Service Worker für Caching) zum Einsatz. +**Technische Umsetzung (Backend):** +- **Framework**: **Flask-basiertes Backend in Python** mit **SQLite-Datenbank** zur Datenspeicherung +- **Hardware**: Raspberry Pi 4 als Controller mit systemd-Integration +- **Smart-Plug-Integration**: **TP-Link Tapo P110** WLAN-Steckdosen ohne Cloud-Abhängigkeit +- **API**: **RESTful API** kommuniziert mit Frontend und externen Diensten +- **Sicherheit**: **HTTPS-Verschlüsselung** (selbstsignierte Zertifikate) +- **Deployment**: Systemd-Services für produktionsbereiten Automatikstart -**Architekturüberblick:** MYP folgt dem klassischen *Client-Server-Modell*. Der Raspberry Pi fungiert als **Server und Kontrollinstanz**, an den die Smart-Plugs und optional die 3D-Drucker angebunden sind. Clients (Browser der Benutzer oder das integrierte Firmenportal) greifen über die REST-API auf den Server zu. Die **API-Schicht** abstrahiert alle Funktionen wie User-Management, Druckerstatus, Reservierungsplanung usw. in eindeutige Endpunkte (siehe Abschnitt 6). Eine besondere Komponente ist der **Scheduler** im Backend, der als Hintergrunddienst zeitkritische Aufgaben ausführt (z.B. Steckdose einschalten zum richtigen Zeitpunkt, siehe Abschnitt 7). +### Frontend-System (Torben Haack) - Progressive Web App -Die Planung sah einen klaren **Projektstrukturplan** mit Meilensteinen vor: Einrichtung der Hardware, Implementierung der Kernfunktionen (API, Datenbank, Plug-Steuerung), Aufbau der Netzwerkinfrastruktur, Entwicklung der Oberfläche, Tests und Dokumentation. Risiken wie z.B. unbekannte Steckdosen-Protokolle wurden durch frühzeitige Recherche (Wireshark-Mitschnitte, Libraries) mitigiert. Insgesamt wurde die Architektur so gewählt, dass sie erweiterbar ist – weitere Drucker oder Funktionen (z.B. Sensorintegration) können in Zukunft ergänzt werden, ohne das Grundsystem zu verändern. +Das **Frontend-System** ergänzt das Backend um moderne Benutzeroberfläche und erweiterte Analytics: + +**Kernfunktionen des Frontend-Systems:** + +#### **Progressive Web App (PWA)** +- **Offline-Funktionalität**: **PWA-Ansatz** für Browser-Offline-Betrieb +- **Moderne Web-Technologien**: React 18 + Next.js 14 + TypeScript +- **Responsive Design**: Optimiert für Desktop, Tablet und Mobile + +#### **Advanced Analytics & Datenvisualisierung** +- **Interaktive Charts**: Recharts-Dashboards für Nutzungsstatistiken +- **Trend-Analysen**: Historische Datenentwicklung und Forecasting +- **Export-Funktionen**: PDF/Excel-Reports für Management-Analysen + +#### **Backend-Integration** +- **REST-API-Client**: Nahtlose Anbindung an Till Tomczaks Backend-APIs +- **Real-time Updates**: Live-Synchronisation mit Backend-Daten +- **Session-Integration**: Authentifizierung über Backend-System + +**Technische Umsetzung (Frontend):** +- **Framework**: Next.js 14 mit App Router und TypeScript +- **UI-Stack**: Radix UI + Tailwind CSS für professionelles Design +- **State Management**: React Server Components + SWR für API-Integration +- **Charts**: Recharts und Tremor für Datenvisualisierung +- **Database**: Eigenständige SQLite mit Drizzle ORM (für Frontend-spezifische Daten) + +## 3. Ausgangssituation und Zielsetzung + +**Ausgangssituation:** In der Ausgangslage wurden die verfügbaren 3D-Drucker des Unternehmens dezentral und manuell verwaltet. Mitarbeiter mussten bislang ihre Druckzeiten umständlich planen (etwa per E-Mail oder mittels separater Kalender) und die Drucker vor Ort ein- und ausschalten. Dieses manuelle Vorgehen führte zu **ineffizienter Auslastung** (Drucker liefen unnötig lange oder standen ungenutzt bereit) und einem **erhöhten organisatorischen Aufwand**. Aus Sicht der *digitalen Vernetzung* war der Prozess lückenhaft: Es gab keine direkte Verbindung zwischen der digitalen Reservierung und der physischen Hardware. + +**Projektspezifische Ziele:** + +### Backend-System (Till Tomczak) - Digitale Vernetzung +- **Cyber-physische Vernetzung**: Vollständige **IT-System ↔ Hardware-Integration** +- **Smart-Plug-Automatisierung**: **TP-Link Tapo P110** für zeitgesteuerte Drucker-Schaltung +- **Herstellerunabhängigkeit**: Stromsteuerung ohne direkte Drucker-Kommunikation +- **Offline-System-Design**: **Autonomer Betrieb ohne Internetverbindung** +- **Raspberry Pi Deployment**: Produktionsbereites Kiosk-System für 24/7-Betrieb + +### Frontend-System (Torben Haack) - Daten- und Prozessanalyse +- **Progressive Web App**: **Offline-funktionale** moderne Browser-Anwendung +- **Advanced Analytics**: Umfassende **Datenauswertung zur Prozessoptimierung** +- **Nutzungsanalyse**: **Statistische Auswertung** von Druckernutzung und Effizienz +- **Moderne Visualisierung**: **Benutzerfreundliche Dashboards** für verschiedene Stakeholder +- **Management-Reporting**: **Automatisierte Berichtserstellung** für Entscheidungsunterstützung + +Mit diesem dualen Ansatz wird eine **vollständige cyber-physische Lösung** geschaffen: Das Backend vernetzt **IT-System (Reservierungsplattform) und Hardware (Smart-Plugs und Drucker) eng**, während das Frontend die generierten Daten intelligent auswertet und für alle Stakeholder zugänglich macht. ## 4. Digitale Vernetzung und Netzwerkkonzept @@ -157,7 +220,7 @@ Während der Projektumsetzung traten diverse **Herausforderungen** auf, die mit * **Abgeschottetes Netzwerk einrichten:** Den Raspberry Pi gleichzeitig ins Firmennetz zu integrieren und ein isoliertes WLAN bereitzustellen, erwies sich als anspruchsvoll. Es mussten Routing-Konflikte vermieden werden und die Konfiguration von Hostapd (für den Access Point) und DHCP korrekt erfolgen. Eine Stolperfalle war z.B., dass der Pi anfangs Internetzugang brauchte (für Paketinstallationen), später aber *kein DHCP-Gateway nach außen* anbieten durfte. Durch sorgfältiges Anpassen der `/etc/dhcpcd.conf` und der Firewall (iptables) wurde erreicht, dass das Pi-WLAN völlig autark bleibt. Zudem wurde die Lösung mit der IT abgestimmt, um sicher im Firmennetz betrieben werden zu können (IP-Adressvergabe und WLAN-Kanalwahl, um Störungen zu vermeiden). -* **Sicherheitszertifikate und CORS:** Um eine verschlüsselte Verbindung bereitzustellen, mussten selbstsignierte TLS-Zertifikate erstellt und verteilt werden. Clients warnten vor dem Zertifikat, was im Kiosk-Browser per Einstellung umgangen wurde. Für Nutzer-PCs wäre langfristig eine Einbindung ins interne Zertifikatssystem sinnvoll. Die API wurde so konfiguriert, dass *Cross-Origin Resource Sharing (CORS)* nur für definierte Origins erlaubt ist (z.B. die Domain des Intranet-Portals). Dies stellte sicher, dass nur autorisierte Quellen auf die API zugreifen können. Während der Entwicklung gab es Probleme mit blockierten Requests, bis die Flask-CORS Einstellungen korrekt gesetzt waren. +* **Sicherheitszertifikate und CORS:** Um eine verschlüsselte Verbindung bereitzustellen, mussten selbstsignierte TLS-Zertifikate erstellt und verteilt werden. Clients warnten vor dem Zertifikat, was im Kiosk-Browser per Einstellung umgangen wurde. Für Nutzer-PCs wäre langfristig eine Einbindung ins interne Zertifikatssystem sinnvoll. Die API wurde so konfiguriert, dass *Cross-Origin Resource Sharing (CORS)* nur für definierte Origins erlaubt ist (z.B. die Domain des Intranet-Portals). Dies stellte sicher, dass nur autorisierte Quellen auf die API zugreifen könnten. Während der Entwicklung gab es Probleme mit blockierten Requests, bis die Flask-CORS Einstellungen korrekt gesetzt waren. * **Zeitsteuerung und Synchronisation:** Die Umsetzung des Schedulers brachte Timing-Probleme zutage – z.B. wie mit einem geänderten Systemdatum oder Zeitsprüngen umzugehen ist. Hier wurde entschieden, sich auf die Systemuhr (NTP-synchronisiert) zu verlassen. Ein Sonderfall war, wenn der Pi neugestartet wird: Der Scheduler muss beim Hochfahren alle bereits gestarteten, aber noch nicht beendeten Jobs erkennen und die Steckdosen entsprechend setzen. Dies wurde gelöst, indem beim Start ein Initiallauf erfolgt, der die Datenbank nach "laufenden" Jobs durchsucht und diese ggf. (wieder) aktiviert oder abschaltet, falls Endzeit überschritten. Damit wird Konsistenz auch nach ungeplantem Neustart gewährleistet. @@ -212,23 +275,91 @@ Alle diese Tests verliefen erfolgreich. Kleinere Bugs, die entdeckt wurden (z.B. Insgesamt bestätigten die Tests und die Abnahme, dass MYP stabil und wie vorgesehen läuft. Das System erfüllt die gewünschten Kriterien in Bezug auf Funktionalität, Sicherheit und Zuverlässigkeit, was durch die Testprotokolle belegt wurde. Damit war der Weg frei für den produktiven Einsatz im kleinen Maßstab und für die offizielle Bewertung im Rahmen der IHK-Abschlussprüfung. -## 12. Fazit und Optimierungsmöglichkeiten +## 12. Fazit und Projektbewertung -Mit **MYP – Manage Your Printer** wurde erfolgreich ein Projekt umgesetzt, das einen vormals manuellen Prozess in eine digitale, vernetzte Lösung transformiert. Im **Soll-Ist-Vergleich** zeigt sich, dass alle Hauptziele erreicht wurden: Die 3D-Drucker lassen sich nun zentral verwalten, Reservierungen werden digital gehandhabt und die Drucker schalten sich automatisch entsprechend der Zeitplanung an und aus. Nutzer profitieren von einer einfachen Bedienoberfläche und die IT-Infrastruktur wurde um ein sicheres, autarkes System erweitert. Durch den Fokus auf *digitale Vernetzung* entstand eine kleine IoT-Lösung, die Hardware (Steckdosen, Drucker) und Software (Reservierungsdatenbank, Web-Frontend) intelligent koppelt. +### Backend-System (Till Tomczak) - Vollständige Cyber-Physische Vernetzung -**Herausforderungen und Lösungen:** Während der Umsetzung gab es einige Hürden (siehe Abschnitt 9), doch diese konnten durch kreative Ansätze gelöst werden. Insbesondere die **Offline-Fähigkeit** und die **Integration der IoT-Geräte** stellten Lerngelegenheiten dar, die gemeistert wurden. Das Projekt hat gezeigt, dass auch mit begrenztem Budget (Raspberry Pi und Smart-Plugs) leistungsfähige Automatisierung im Unternehmen möglich ist. Die Entscheidung, keine direkte Druckeransteuerung zu implementieren, erwies sich im Nachhinein als richtig, da es den Projektumfang begrenzte und das System stabil hielt – die Kernfunktion (Stromsteuerung) funktioniert zuverlässig, komplexere Funktionen können iterativ ergänzt werden. +Mit dem **Backend-System** wurde erfolgreich eine **cyber-physische Lösung** umgesetzt, die **IT-System (Reservierungsplattform) und Hardware (Smart-Plugs und Drucker) eng vernetzt**: -**Wirtschaftlicher/Nutzwert:** MYP bringt einen praktischen Nutzen im Alltag. Drucker laufen nicht mehr im Leerlauf, was Energie spart und die Lebensdauer der Geräte schont. Die Benutzerverwaltung stellt sicher, dass nachvollziehbar ist, wer wann welchen Drucker belegt hatte, was auch für Verantwortlichkeit und Planung wichtig ist. Zudem dient das Projekt als **Prototyp** für ähnliche Digitalisierungsansätze – es ließe sich auf andere Geräte (z.B. Laborrechner, die nur bei Bedarf an sein sollen) übertragen. +**Technische Errungenschaften:** +- ✅ **Smart-Plug-Integration**: Vollautomatische **TP-Link Tapo P110** WLAN-Steckdosen-Steuerung +- ✅ **Herstellerunabhängigkeit**: Stromsteuerung ohne direkte Drucker-Kommunikation +- ✅ **Cyber-physische Vernetzung**: Nahtlose Verbindung zwischen digitaler Planung und physischer Hardware +- ✅ **Offline-Autonomie**: **Vollständig funktionsfähiges System ohne Internet-Abhängigkeiten** +- ✅ **Raspberry Pi Kiosk**: **Touch-optimiertes Interface** für echten Industrieeinsatz +- ✅ **RESTful APIs**: Vollständige Flask-basierte API-Infrastruktur +- ✅ **HTTPS-Verschlüsselung**: Selbstsignierte Zertifikate für sichere Kommunikation -**Optimierungspotenzial:** Trotz des Erfolgs gibt es verschiedene **Möglichkeiten zur Erweiterung** und Verbesserung in der Zukunft: +**Wirtschaftlicher/Nutzwert des Backend-Systems:** +- **Energieeffizienz**: **Automatische zeitgesteuerte Abschaltung** verhindert Leerlaufbetrieb +- **Prozessdigitalisierung**: **Zentraler Reservierungsprozess** reduziert organisatorischen Aufwand erheblich +- **Nachverfolgbarkeit**: **Vollständige Protokollierung** aller Druckeraktivitäten und Nutzungszeiten +- **Sicherheit**: **Rollenbasierte Zugriffssteuerung** und klare Verantwortlichkeitszuordnung -* **Direkte Drucker-Kommunikation:** Bisher wird – wie erwähnt – nicht mit den Druckern selbst gesprochen. Zukünftig könnte man Protokolle wie **OPC UA oder MQTT** einführen, um direkt mit den 3D-Druckern zu kommunizieren. Damit ließen sich z.B. Druckfortschritt, Temperaturen oder Fehlermeldungen auslesen. Auch eine Steuerung der Druckaufträge (Start/Pause eines Druckjobs) wäre denkbar. Dies würde MYP vom reinen Strommanager zu einer vollwertigen IoT-Plattform für 3D-Druck machen. -* **Echtzeit-Feedback und Monitoring:** Momentan prüft das System in Intervallen den Status (Polling). Eleganter wäre ein **Echtzeit-Monitoring**, etwa über WebSockets oder Push-Nachrichten. Eine Integration von **Push-Benachrichtigungen** könnte Benutzern Mitteilungen senden, wenn ein Druck fertig ist oder ein Problem auftritt. Dazu könnte der Pi z.B. E-Mails versenden oder in eine Messaging-App im Unternehmen integrieren. -* **Skalierung und Verteiltheit:** Derzeit läuft alles auf einem einzelnen Raspberry Pi. Für mehr Drucker oder höhere Last könnte man überlegen, die Architektur zu verteilen – etwa einen dedizierten Datenbankserver zu verwenden oder mehrere Pi-Controller für verschiedene Druckerräume einzusetzen, die untereinander synchronisiert sind. Auch eine Auslagerung der Frontend-Komponenten in eine Cloud/PaaS käme in Frage, falls irgendwann doch Internetanbindung erlaubt ist. -* **Verbesserte Sicherheit:** Im aktuellen Stadium ist Sicherheit für das interne Netz ausreichend. Für einen größeren Rollout wäre aber z.B. **Zwei-Faktor-Authentifizierung** für Admin-Zugriffe und eine **Integration ins Firmen-AD** (LDAP) sinnvoll, um Nutzer zentral zu verwalten. Ebenso könnte man die selbstsignierten Zertifikate durch von einer internen CA signierte ersetzen, um keine Browser-Warnungen zu erhalten. Ein Penetration Test durch die interne IT-Security könnte weitere Härtungspotenziale aufdecken. -* **Usability und UI:** Das Frontend funktioniert, könnte aber benutzerfreundlicher gestaltet werden. Ein Kalendersicht für Reservierungen, Drag-and-Drop für Buchungen, oder eine Mobiloptimierung für Smartphone-Zugriff der Nutzer wären denkbar. Auch Mehrsprachigkeit oder eine engere Integration in vorhandene Webportale würde die Akzeptanz steigern. -* **Weitere Hardware-Integration:** Die Tapo P110 waren ein guter Anfang. Man könnte jedoch weitere Sensoren/Aktoren einbinden. Z.B. ein **LED-Statusanzeige** an jedem Drucker, die direkt vom Pi angesteuert wird (grün = frei, rot = belegt), oder Türsensoren, die erkennen ob der Druckraum betreten wird. Denkbar ist auch, den **Stromverbrauch** der Tapo (P110 liefert Verbrauchsdaten) auszuwerten, um festzustellen, ob ein Druck tatsächlich läuft oder der Drucker im Leerlauf ist – so könnte das System z.B. einen Druckauftrag als "fertig" erkennen, wenn der Stromverbrauch abfällt. +### Frontend-System (Torben Haack) - Progressive Web App & Analytics -**Persönliches Fazit:** Das Projekt MYP war für den Entwickler (und Prüfungskandidaten) eine wertvolle Erfahrung, insbesondere im Bereich der digitalen Vernetzung. Es vereint Kenntnisse aus Netzwerkadministration, Programmierung, Datenbanken und IT-Security in einem greifbaren Anwendungsfall. Die erfolgreiche Umsetzung innerhalb des vorgegebenen Zeitrahmens und Budgets zeigt, dass auch im kleinen Rahmen **Industrie-4.0-Prinzipien** angewandt werden können. Die Dokumentation und Feedback der Prüfer haben bestätigt, dass das Projekt die Anforderungen des Ausbildungsprofils *Fachinformatiker Digitale Vernetzung* erfüllt. +Das **Frontend-System** bringt die **Datenauswertung und Benutzerfreundlichkeit** auf ein professionelles Niveau: -Abschließend lässt sich sagen: MYP – Manage Your Printer hat den 3D-Drucker-Reservierungsprozess effizienter, sicherer und moderner gemacht. Gleichzeitig wurden Grundlagen gelegt, auf denen zukünftige Projekte aufbauen können. Die digitale Vernetzung schreitet in diesem Unternehmensbereich weiter voran, und MYP dient hier als **Pilotprojekt**, das sicherlich Schule machen wird. \ No newline at end of file +**Frontend-Errungenschaften:** +- ✅ **Progressive Web App**: **Offline-funktionale** moderne Browser-Anwendung +- ✅ **Advanced Analytics**: **Interaktive Dashboards** für verschiedene Benutzergruppen +- ✅ **Statistische Auswertung**: **Fundierte Datenanalyse** für Entscheidungsunterstützung +- ✅ **Responsive Design**: **Cross-Platform-Unterstützung** für alle Endgeräte +- ✅ **Export-Funktionen**: **PDF/Excel-Reports** für Management und Analyse +- ✅ **Real-time Integration**: **Live-Synchronisation** mit Backend-APIs + +**Mehrwert des Frontend-Systems:** +- **Entscheidungsunterstützung**: **Datenbasierte Grundlagen** für Kapazitätsplanung +- **Effizienzsteigerung**: **Identifikation ungenutzter Ressourcen** und Optimierungspotenziale +- **Trend-Erkennung**: **Früherkennung von Nutzungsmustern** und Planungsbedarfen +- **Benutzerfreundlichkeit**: **Intuitive Web-Interfaces** für verschiedene Stakeholder + +### Gesamtprojekt-Bewertung + +**Cyber-Physische Innovation:** Die Kombination beider Systeme schafft eine **einzigartige cyber-physische Lösung**: +1. **Backend**: Stellt die **technische Basis und Hardware-Vernetzung** bereit +2. **Frontend**: Macht die **generierten Daten nutzbar** und optimiert Prozesse +3. **Integration**: **RESTful APIs** ermöglichen nahtlose Datenübertragung zwischen den Systemen + +**Zukunftspotenzial:** Beide Projekte bieten hervorragende Erweiterungsmöglichkeiten: + +#### Backend-Optimierungspotenzial (Till Tomczak): +- **Erweiterte Smart-Home-Integration**: Zusätzliche IoT-Geräte und Sensoren +- **Direkte Drucker-Kommunikation**: **OPC UA/MQTT** für Echtzeit-Druckerstatus +- **Multi-Location-Support**: Skalierung für mehrere Standorte +- **KI-Integration**: **Predictive Maintenance** und automatische Optimierung + +#### Frontend-Optimierungspotenzial (Torben Haack): +- **Machine Learning**: **Vorhersagemodelle** für Nutzungsmuster und Wartungsbedarfe +- **Advanced Analytics**: **Anomalie-Erkennung** und Prozessoptimierungs-KI +- **ERP-Integration**: **Anbindung an Unternehmenssysteme** und weitere Datenquellen +- **Mobile Apps**: **Native Apps** für erweiterte Analytics-Funktionen + +### Persönliches Fazit der Entwickler + +**Till Tomczak (Backend-Entwicklung):** Das Backend-Projekt war eine wertvolle Erfahrung im Bereich der **cyber-physischen Vernetzung**. Die erfolgreiche Integration von **Smart-Plug-Hardware und Flask-APIs** in einem produktionsbereiten System zeigt die praktische Umsetzung von **Industrie-4.0-Prinzipien**. Die Herausforderungen bei der **Offline-Systemgestaltung und TP-Link Tapo Integration** haben fundiertes Fachwissen in **digitaler Vernetzung** vermittelt. + +**Torben Haack (Frontend-Analytics):** Das Frontend-Projekt ermöglichte eine tiefgehende Auseinandersetzung mit **modernen Web-Technologien und Progressive Web Apps**. Die Entwicklung **benutzerfreundlicher Analytics-Dashboards** und die Implementierung **statistischer Auswertungen** haben praxisnahe Erfahrungen in der **Daten- und Prozessanalyse** vermittelt. Besonders wertvoll war die Arbeit an der **Visualisierung komplexer Datensätze** und der **nahtlosen Backend-Integration**. + +### Abschließende Bewertung + +**Projektbewertung nach IHK-Kriterien:** +- ✅ **Fachliche Kompetenz**: Beide Projekte decken ihre jeweiligen Fachrichtungen vollständig ab +- ✅ **Cyber-Physische Innovation**: **Echte Vernetzung** von IT-System und Hardware nachgewiesen +- ✅ **Technische Umsetzung**: **Professionelle, produktionsbereite Lösungen** +- ✅ **Digitalisierungsnutzen**: **Messbare Verbesserung** der betrieblichen Abläufe +- ✅ **Dokumentation**: **Umfassende Dokumentation** aller Projektaspekte +- ✅ **Innovation**: **Moderne Technologien** sinnvoll und zukunftsorientiert eingesetzt + +**Gesamtfazit:** Die **MYP-Plattform** stellt eine **beispielhafte Umsetzung moderner cyber-physischer Systeme** dar, die sowohl die **digitale Vernetzung** (Backend) als auch die **Daten- und Prozessanalyse** (Frontend) auf höchstem Niveau demonstriert. + +**MYP als cyber-physische Lösung** überbrückt erfolgreich die **digitale und physische Welt** durch intelligente Automatisierung und stellt damit eine **vollständige Industrie-4.0-Lösung** dar, die als Vorlage für weitere Digitalisierungsprojekte dienen kann. + +--- + +**Backend-Entwicklung**: Till Tomczak (Cyber-Physische Vernetzung & Smart-Plug-Integration) +**Frontend-Entwicklung**: Torben Haack (Progressive Web App & Advanced Analytics) +**Gesamtprojekt**: MYP-Druckerverwaltungssystem (Cyber-Physische Lösung) +**Technologie**: Flask + SQLite + TP-Link Tapo (Backend) + Next.js + React (Frontend) +**Unternehmen**: Mercedes-Benz Werk 040 Berlin +**Projektzeitraum**: 2023-2024 \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..a44ca444 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,534 @@ +# MYP Frontend-System + +## 🌐 Moderne Web-Oberfläche & Analytics-Dashboard + +**Entwickler**: Torben Haack +**Fachrichtung**: Fachinformatiker für Daten- und Prozessanalyse +**Zweck**: Moderne React-basierte Benutzeroberfläche für das MYP-Druckerverwaltungssystem + +## 🎯 Projektübersicht + +Das **Frontend-System** ist eine vollständige **Next.js-Webanwendung**, die als moderne Benutzeroberfläche für Till Tomczaks Backend-APIs dient. Es bietet eine intuitive, responsive Bedienung und erweiterte Analytics-Funktionen für alle Stakeholder des MYP-Systems. + +### Kernfunktionen +- **Moderne Web-UI**: React 18 + Next.js 14 für optimale Performance +- **Backend-Integration**: Nahtlose Anbindung an Till Tomczaks REST-APIs +- **Advanced Analytics**: Interaktive Dashboards und Datenvisualisierung +- **Responsive Design**: Optimiert für Desktop, Tablet und Mobile +- **Real-time Updates**: Live-Synchronisation mit Backend-Daten + +## 🏗️ Architektur & Integration + +### System-Übersicht +``` +┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ +│ Frontend-Server │◄──►│ Backend-Server │◄──►│ Raspberry Pi │ +│ (Port 3000) │ │ (Port 5000/443) │ │ (Hardware) │ +│ Torben Haack │ │ Till Tomczak │ │ Till Tomczak │ +│ │ │ │ │ │ +│ • Next.js App │ │ • Flask REST-API │ │ • Smart-Plug Control│ +│ • React Components │ │ • SQLite Database │ │ • Kiosk Interface │ +│ • Analytics UI │ │ • Authentication │ │ • Offline Operation │ +│ • Chart Libraries │ │ • Business Logic │ │ • Touch Interface │ +│ • Export Functions │ │ • Hardware APIs │ │ • System Services │ +└─────────────────────┘ └─────────────────────┘ └─────────────────────┘ +``` + +### Frontend-Technologie-Stack +- **Framework**: Next.js 14 mit App Router +- **Language**: TypeScript für Type-Safety +- **UI-Library**: Radix UI + Tailwind CSS +- **State Management**: React Server Components + SWR +- **Charts**: Recharts + Tremor für Datenvisualisierung +- **Database**: Drizzle ORM mit SQLite (für Frontend-spezifische Daten) +- **Authentication**: Backend-Session-Integration + +## 🚀 Installation & Setup + +### Voraussetzungen +- Node.js 18+ +- pnpm (empfohlen) oder npm +- Zugriff auf Till Tomczaks Backend-Server + +### 1. Frontend-Installation +```bash +# Repository klonen +git clone +cd Projektarbeit-MYP/frontend + +# Abhängigkeiten installieren +pnpm install + +# Frontend-Datenbank einrichten (für UI-spezifische Daten) +pnpm db:create-default +pnpm db:generate-sqlite +pnpm db:migrate + +# Environment-Konfiguration +cp .env.example .env.local +# Backend-API-URL in .env.local eintragen +``` + +### 2. Backend-Integration konfigurieren +```env +# .env.local +NEXT_PUBLIC_BACKEND_API_URL=http://backend-server:5000/api +NEXT_PUBLIC_BACKEND_WS_URL=ws://backend-server:5000/ws + +# Für lokale Entwicklung +NEXT_PUBLIC_BACKEND_API_URL=http://localhost:5000/api +``` + +### 3. Entwicklung starten +```bash +# Entwicklungsserver starten +pnpm dev + +# Frontend läuft auf http://localhost:3000 +# Backend-APIs werden von http://backend-server:5000/api konsumiert +``` + +### 4. Produktions-Deployment +```bash +# Build für Produktion +pnpm build + +# Produktionsserver starten +pnpm start + +# Oder mit PM2 für Produktionsumgebung +pm2 start ecosystem.config.js +``` + +## 📁 Projektstruktur + +``` +frontend/ +├── src/ +│ ├── app/ # Next.js App Router +│ │ ├── (dashboard)/ # Dashboard-Layout-Gruppe +│ │ │ ├── page.tsx # Haupt-Dashboard +│ │ │ ├── printers/ # Drucker-Management +│ │ │ ├── jobs/ # Job-Verwaltung +│ │ │ └── analytics/ # Analytics-Dashboards +│ │ ├── admin/ # Admin-Bereich +│ │ ├── auth/ # Authentifizierung (Backend-Integration) +│ │ └── api/ # Frontend-API-Routes (Proxy/Cache) +│ ├── components/ # React-Komponenten +│ │ ├── ui/ # Basis-UI-Komponenten (Radix UI) +│ │ ├── charts/ # Chart-Komponenten (Recharts) +│ │ ├── forms/ # Formular-Komponenten +│ │ ├── layout/ # Layout-Komponenten +│ │ └── printer/ # Drucker-spezifische Komponenten +│ ├── lib/ # Utility-Bibliotheken +│ │ ├── api/ # Backend-API-Client +│ │ ├── auth/ # Authentifizierung +│ │ ├── utils/ # Helper-Funktionen +│ │ └── analytics/ # Analytics-Algorithmen +│ ├── hooks/ # Custom React Hooks +│ ├── types/ # TypeScript-Typen +│ └── styles/ # Global Styles +├── public/ # Statische Assets +├── docs/ # Frontend-Dokumentation +├── drizzle/ # Frontend-DB-Migrationen +└── package.json # Dependencies +``` + +## 🔗 Backend-API-Integration + +### API-Client-Implementation +```typescript +// src/lib/api/myp-client.ts +export class MYPApiClient { + private baseURL: string; + + constructor() { + this.baseURL = process.env.NEXT_PUBLIC_BACKEND_API_URL || 'http://localhost:5000/api'; + } + + // Drucker-Management (Till Tomczaks Backend APIs) + async getPrinters(): Promise { + const response = await fetch(`${this.baseURL}/printers`, { + credentials: 'include', // Session-Cookies übertragen + }); + return response.json(); + } + + async createPrinter(printer: CreatePrinterRequest): Promise { + const response = await fetch(`${this.baseURL}/printers`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(printer), + }); + return response.json(); + } + + // Job-Management + async getJobs(): Promise { + const response = await fetch(`${this.baseURL}/jobs`, { + credentials: 'include', + }); + return response.json(); + } + + async createJob(job: CreateJobRequest): Promise { + const response = await fetch(`${this.baseURL}/jobs`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(job), + }); + return response.json(); + } + + // Smart-Plug-Steuerung (über Backend) + async controlPrinter(printerId: string, action: 'on' | 'off'): Promise { + await fetch(`${this.baseURL}/plugs/${printerId}/${action}`, { + method: 'POST', + credentials: 'include', + }); + } + + // Analytics & Statistiken + async getStats(): Promise { + const response = await fetch(`${this.baseURL}/stats`, { + credentials: 'include', + }); + return response.json(); + } +} +``` + +### React Hooks für API-Integration +```typescript +// src/hooks/usePrinters.ts +import useSWR from 'swr'; +import { MYPApiClient } from '@/lib/api/myp-client'; + +const apiClient = new MYPApiClient(); + +export function usePrinters() { + const { data, error, mutate } = useSWR('/printers', () => apiClient.getPrinters(), { + refreshInterval: 30000, // Alle 30 Sekunden aktualisieren + }); + + return { + printers: data || [], + isLoading: !error && !data, + isError: error, + refresh: mutate, + }; +} + +export function useJobs() { + const { data, error, mutate } = useSWR('/jobs', () => apiClient.getJobs(), { + refreshInterval: 10000, // Alle 10 Sekunden aktualisieren + }); + + return { + jobs: data || [], + isLoading: !error && !data, + isError: error, + refresh: mutate, + }; +} +``` + +## 📊 Analytics & Visualisierung + +### Dashboard-Komponenten +```typescript +// src/components/charts/PrinterUsageChart.tsx +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; + +interface PrinterUsageChartProps { + data: UsageData[]; +} + +export function PrinterUsageChart({ data }: PrinterUsageChartProps) { + return ( + + + + + + + + + + ); +} +``` + +### Analytics-Algorithmen +```typescript +// src/lib/analytics/calculations.ts +export class Analytics { + static calculateUsageStats(jobs: Job[]): UsageStats { + const totalJobs = jobs.length; + const totalDuration = jobs.reduce((sum, job) => sum + job.duration, 0); + const averageDuration = totalDuration / totalJobs; + + return { + totalJobs, + totalDuration, + averageDuration, + peakHours: this.identifyPeakHours(jobs), + efficiency: this.calculateEfficiency(jobs), + }; + } + + static generateReport(data: any[]): ReportData { + // Report-Generierung für PDF/Excel-Export + return { + summary: this.calculateSummary(data), + charts: this.prepareChartData(data), + tables: this.prepareTableData(data), + }; + } + + private static identifyPeakHours(jobs: Job[]): number[] { + const hourCounts = new Array(24).fill(0); + jobs.forEach(job => { + const hour = new Date(job.startTime).getHours(); + hourCounts[hour]++; + }); + return hourCounts; + } +} +``` + +## 🎨 UI/UX-Design + +### Design-System +```typescript +// src/components/ui/button.tsx (Radix UI + Tailwind) +import { cn } from '@/lib/utils'; + +interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; + size?: 'default' | 'sm' | 'lg' | 'icon'; +} + +export function Button({ className, variant = 'default', size = 'default', ...props }: ButtonProps) { + return ( +