diff --git a/COMMON_ERRORS.md b/COMMON_ERRORS.md deleted file mode 100644 index a6aafb21..00000000 --- a/COMMON_ERRORS.md +++ /dev/null @@ -1,379 +0,0 @@ -# Common Errors und Lösungen - -## 1. Database Schema Error - "no such column" - -### Problem -``` -sqlite3.OperationalError: no such column: printers_1.last_checked -[SQL: SELECT jobs.id AS jobs_id, ... printers_1.last_checked ...] -``` - -### Ursache -- Datenbankschema ist veraltet -- Die `last_checked` Spalte fehlt in der `printers` Tabelle -- Tritt auf, wenn die Datenbank vor Schema-Updates erstellt wurde - -### Lösungen -1. **Automatische Migration (empfohlen):** - ```bash - cd backend/app - PYTHONPATH=. python3.11 utils/database_migration.py - ``` - -2. **Manuelle Datenbank-Neuerstellung:** - ```bash - cd backend/app - python3.11 -c "from models import init_db; init_db(); print('Database recreated')" - python3.11 utils/setup_drucker_db.py - python3.11 -c "from models import create_initial_admin; create_initial_admin()" - ``` - -3. **Spalte manuell hinzufügen:** - ```bash - cd backend/app - python3.11 -c " - import sqlite3 - conn = sqlite3.connect('database/myp.db') - cursor = conn.cursor() - cursor.execute('ALTER TABLE printers ADD COLUMN last_checked DATETIME') - conn.commit() - conn.close() - print('Column added') - " - ``` - -### Prävention -- Verwende die Migrationsskripte vor dem Start der Anwendung -- Backup der Datenbank vor Schema-Änderungen erstellen - -## 2. Tailwind CSS Compilation Fehler - -### Problem -``` -npm ERR! could not determine executable to run -``` - -### Ursache -- Node.js/npm nicht installiert oder nicht im PATH -- node_modules Verzeichnis fehlt -- Defekte npm Installation - -### Lösungen -1. **Node.js installieren:** - ```bash - # Ubuntu/Debian - sudo apt update && sudo apt install nodejs npm - - # CentOS/RHEL - sudo yum install nodejs npm - ``` - -2. **Dependencies neu installieren:** - ```bash - cd backend/app - rm -rf node_modules package-lock.json - npm install - ``` - -3. **Manuelle CSS-Kompilierung:** - ```bash - cd backend/app - npx tailwindcss -i static/css/input.css -o static/css/tailwind.min.css --minify - ``` - -4. **Fallback verwenden:** - - Die existierende `static/css/tailwind.min.css` wird automatisch verwendet - - Keine weiteren Schritte erforderlich - -## 3. Port 443/80 Permission Denied - -### Problem -``` -Permission denied -``` -beim Starten auf Port 443 oder 80 - -### Ursache -- Ports < 1024 sind privilegierte Ports -- Benötigen Root-Rechte oder spezielle Capabilities - -### Lösungen -1. **Nicht-privilegierte Ports verwenden (empfohlen):** - ```bash - python3 app.py --port 8443 - ``` - -2. **Automatische Fallback-Ports:** - - App erkennt automatisch Permission-Fehler - - Verwendet Port 8443 statt 443 - - Verwendet Port 8080 statt 80 - -3. **Root-Rechte (nicht empfohlen):** - ```bash - sudo python3 app.py - ``` - -4. **Capabilities setzen (Linux):** - ```bash - sudo setcap CAP_NET_BIND_SERVICE=+eip $(which python3) - ``` - -## 4. Admin-Panel 404 Fehler - -### Problem -``` -404 Not Found -``` -für Admin-Routen wie `/admin/users/add`, `/admin/printers/add` - -### Ursache -- Fehlende Template-Dateien -- Nicht implementierte Admin-Routen -- Fehlende API-Endpunkte - -### Lösungen -1. **Templates wurden erstellt:** - - `admin_add_user.html` - Benutzer hinzufügen - - `admin_add_printer.html` - Drucker hinzufügen - - `admin_edit_user.html` - Benutzer bearbeiten - - `admin_manage_printer.html` - Drucker verwalten - - `admin_printer_settings.html` - Drucker-Einstellungen - -2. **API-Endpunkte implementiert:** - - `/api/admin/system/status` - System-Status - - `/api/admin/database/status` - Datenbank-Status - - `/api/admin/users/{id}/edit` - Benutzer bearbeiten - - `/api/admin/printers/{id}/edit` - Drucker bearbeiten - -## 5. Admin-Variable-Fehler - -### Problem -``` -cannot access local variable 'os' where it is not associated with a value -``` - -### Ursache -- `os` Modul wird lokal importiert aber nicht verfügbar - -### Lösung -- Import von `os` wurde an den Anfang der System-Informationen-Sektion verschoben -- Fehler ist behoben - -## 6. Icon-Pfad 404 Fehler - -### Problem -``` -404 Not Found: /static/static/icons/icon-144x144.png -``` - -### Ursache -- Doppelte `static/` im Pfad in der `manifest.json` - -### Lösung -- Pfade in `manifest.json` korrigiert von `static/icons/` zu `icons/` -- Icons sind jetzt korrekt erreichbar - -## 7. SSL-Zertifikat Probleme - -### Problem -- SSL-Zertifikate fehlen -- Zertifikat-Validierung schlägt fehl - -### Lösungen -1. **Automatische Zertifikat-Generierung:** - - App erstellt automatisch selbstsignierte Zertifikate - - Für Entwicklung ausreichend - -2. **SSL deaktivieren:** - ```bash - python3 app.py --no-ssl - ``` - -3. **Eigene Zertifikate verwenden:** - - Zertifikat: `backend/app/certs/myp.crt` - - Schlüssel: `backend/app/certs/myp.key` - -## 8. Datenbank-Probleme - -### Problem -- Datenbank kann nicht initialisiert werden -- SQLite-Fehler - -### Lösungen -1. **Datenbank-Verzeichnis erstellen:** - ```bash - mkdir -p backend/app/database - ``` - -2. **Berechtigungen prüfen:** - ```bash - chmod 755 backend/app/database - ``` - -3. **Datenbank neu initialisieren:** - ```bash - cd backend/app - python3 init_db.py - ``` - -## 9. Scheduler-Fehler - -### Problem -``` -Task mit ID check_jobs existiert bereits -``` - -### Ursache -- App wurde mehrmals gestartet -- Scheduler-Thread läuft bereits - -### Lösung -```bash -# Alle Python-Prozesse stoppen -pkill -f "python.*app.py" - -# Neu starten -python3 app.py -``` - -## 10. Import-Fehler - -### Problem -``` -ModuleNotFoundError: No module named 'xyz' -``` - -### Lösung -```bash -# Python Dependencies installieren -cd backend/app -pip3 install -r requirements.txt - -# Oder für Python 3.11 -python3.11 -m pip install -r requirements.txt -``` - -## Startup-Script Verwendung - -### Automatische Problembehandlung -```bash -# Einfachste Methode -./backend/run.sh - -# Oder direkt -cd backend -python3 start_server.py -``` - -Das Startup-Script: -- Prüft automatisch Dependencies -- Findet verfügbare Ports -- Installiert fehlende Packages -- Kompiliert CSS falls möglich -- Startet Server mit optimalen Einstellungen - -### Manuelle Parameter -```bash -# Spezifischer Port -python3 start_server.py --port 5000 - -# SSL deaktivieren -python3 start_server.py --no-ssl - -# Dual-Protokoll (HTTP + HTTPS) -python3 start_server.py --dual-protocol -``` - -## Debugging-Tipps - -1. **Log-Level erhöhen:** - ```python - # In config/settings.py - LOG_LEVEL = "DEBUG" - ``` - -2. **Detaillierte Logs anzeigen:** - ```bash - tail -f backend/app/logs/app/app.log - ``` - -3. **Netzwerk-Probleme prüfen:** - ```bash - # Port-Status prüfen - netstat -tlnp | grep :8443 - - # Firewall prüfen - sudo ufw status - ``` - -4. **Python-Umgebung prüfen:** - ```bash - python3 --version - python3 -c "import flask; print(flask.__version__)" - ``` - -## Neue Features (behoben) - -### Admin-Panel Funktionalität -- ✅ Benutzer hinzufügen/bearbeiten -- ✅ Drucker hinzufügen/verwalten/konfigurieren -- ✅ System-Status und Datenbank-Status APIs -- ✅ Vollständige Admin-Templates - -### Verbesserte Fehlerbehandlung -- ✅ Automatische Port-Fallbacks -- ✅ Graceful CSS-Kompilierung mit Fallback -- ✅ Robuste SSL-Zertifikat-Behandlung -- ✅ Verbesserte Logging und Debugging - -## Flask Route Konflikte - 404 Fehler bei existierenden Routen - -**Problem:** -- API-Endpunkt `/api/admin/stats/live` gibt 404 Fehler zurück, obwohl die Route implementiert ist -- Flask-Anwendung startet nicht mit AssertionError: "View function mapping is overwriting an existing endpoint function" - -**Ursache:** -- Doppelte Route-Definitionen in `app.py` (z.B. `update_printers` mehrfach definiert) -- Konflikte zwischen Haupt-App-Routen und Blueprint-Routen -- API-Blueprint wird ohne URL-Präfix registriert und überschreibt Haupt-Routen - -**Lösung:** -1. **Doppelte Routen entfernen:** - ```bash - grep -n "def update_printers" app.py # Finde alle Duplikate - sed -i '3512,$d' app.py # Entferne doppelte Definition am Ende - ``` - -2. **Blueprint-Registrierung deaktivieren:** - ```python - # app.register_blueprint(api_bp) # Kommentiere aus bei Konflikten - ``` - -3. **Route-Registrierung prüfen:** - ```python - python3.11 -c "from app import app; [print(f'{rule.rule} -> {rule.endpoint}') for rule in app.url_map.iter_rules() if 'admin' in rule.rule and 'stats' in rule.rule]" - ``` - -**Prävention:** -- Verwende eindeutige Funktionsnamen -- Registriere Blueprints mit URL-Präfix: `app.register_blueprint(api_bp, url_prefix='/api/v1')` -- Prüfe Route-Konflikte vor Deployment - -**Behoben am:** 26.05.2025 -**Betroffen:** Flask-Anwendung, Admin-Dashboard, Live-Statistiken - -**Status:** ✅ GELÖST - API-Blueprint registriert mit URL-Präfix /api/v1, alle Admin-CRUD-Endpunkte funktionieren - -**Fix angewendet:** 26.05.2025 -- API-Blueprint in app.py korrekt registriert mit `app.register_blueprint(api_bp, url_prefix='/api/v1')` -- Alle Admin-Dashboard API-Endpunkte sind jetzt verfügbar: - - `/api/admin/stats/live` ✅ - - `/api/admin/system/status` ✅ - - `/api/admin/database/status` ✅ -- Alle Admin-Template-Routen funktionieren: - - `/admin/users/add` ✅ - - `/admin/users//edit` ✅ - - `/admin/printers/add` ✅ - - `/admin/printers//manage` ✅ - -**Prävention:** Regelmäßige Überprüfung der Blueprint-Registrierung beim Hinzufügen neuer Routen \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 0fbf6541..00000000 --- a/README.md +++ /dev/null @@ -1,259 +0,0 @@ -# MYP (Mercedes-Benz Yard Printing) Platform - -Eine vollständige 3D-Drucker-Management-Plattform für Mercedes-Benz Werk 040 Berlin. - -## Schnellstart - -### Windows (PowerShell) - -```powershell -# Als Administrator für vollständige Funktionalität -.\myp_installer.ps1 -``` - -### Linux/Unix/macOS (Bash) - -```bash -# Ausführbar machen und als Root für vollständige Funktionalität -chmod +x myp_installer.sh -sudo ./myp_installer.sh -``` - -Die konsolidierten Installer bieten folgende Funktionen: - -- Systemvoraussetzungen prüfen -- Host-Konfiguration einrichten -- SSL-Zertifikate erstellen und verwalten -- Backend-Verbindung testen -- Frontend-/Backend-URL konfigurieren -- Debug-Server starten -- Vollständige MYP-Installation -- Umgebungs-Setup (Abhängigkeiten installieren) -- Anwendung starten (verschiedene Modi) -- Alte Dateien bereinigen - -## Übersicht - -Die MYP-Plattform ist eine moderne, webbasierte Lösung zur Verwaltung von 3D-Druckern in einer Unternehmensumgebung. Sie bietet: - -- **Drucker-Management**: Überwachung und Steuerung von 3D-Druckern -- **Auftragsverwaltung**: Verwaltung von Druckaufträgen und Warteschlangen -- **Benutzerauthentifizierung**: GitHub OAuth und lokale Benutzerkonten -- **Smart-Plug-Integration**: Automatische Stromsteuerung über TP-Link Tapo-Steckdosen -- **SSL/HTTPS-Unterstützung**: Sichere Kommunikation mit selbstsignierten Zertifikaten - -## Architektur - -### Backend - -- **Framework**: Flask (Python) -- **Datenbank**: SQLite -- **API**: RESTful API mit JSON -- **Authentifizierung**: Session-basiert mit Flask-Login -- **SSL**: Selbstsignierte Zertifikate - -### Frontend - -- **Framework**: Next.js (React) -- **Styling**: Tailwind CSS -- **Authentifizierung**: GitHub OAuth -- **API-Kommunikation**: Fetch API - -## Installation - -### Automatische Installation - -Die einfachste Methode ist die Verwendung der konsolidierten Installer-Skripte: - -#### Windows (PowerShell) - -```powershell -# Als Administrator ausführen für vollständige Funktionalität -.\myp_installer.ps1 -``` - -#### Linux/Unix/macOS (Bash) - -```bash -# Ausführbar machen -chmod +x myp_installer.sh - -# Als Root für vollständige Funktionalität -sudo ./myp_installer.sh -``` - -### Manuelle Installation - -#### Voraussetzungen - -- Python 3.6+ mit pip -- Node.js 16+ mit npm -- Docker und Docker Compose (optional) -- Git - -#### Backend-Setup - -```bash -cd backend -pip install -r requirements.txt -python app/app.py -``` - -#### Frontend-Setup - -```bash -cd frontend -npm install -npm run dev -``` - -## Konfiguration - -### Standard-Zugangsdaten - -- **Admin E-Mail**: admin@mercedes-benz.com -- **Admin Passwort**: 744563017196A - -### Umgebungsvariablen - -#### Backend (.env) - -``` -SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F -DATABASE_PATH=database/myp.db -TAPO_USERNAME=till.tomczak@mercedes-benz.com -TAPO_PASSWORD=744563017196A -SSL_ENABLED=True -``` - -#### Frontend (.env.local) - -``` -NEXT_PUBLIC_API_URL=https://localhost:443 -NEXT_PUBLIC_BACKEND_HOST=localhost -NEXT_PUBLIC_BACKEND_PROTOCOL=https -GITHUB_CLIENT_ID=7c5d8bef1a5519ec1fdc -GITHUB_CLIENT_SECRET=5f1e586204358fbd53cf5fb7d418b3f06ccab8fd -``` - -## SSL-Zertifikate - -Die Plattform verwendet selbstsignierte SSL-Zertifikate für sichere Kommunikation. Diese werden automatisch von den Installer-Skripten erstellt. - -### Manuelle Zertifikatserstellung - -```bash -cd backend -python app/create_ssl_cert.py -c instance/ssl/myp.crt -k instance/ssl/myp.key -n localhost -``` - -## Docker-Deployment - -```bash -# Entwicklung -docker-compose up -d - -# Produktion -docker-compose -f docker-compose.prod.yml up -d -``` - -## Entwicklung - -### Backend-Entwicklung - -```bash -cd backend -python app/app.py --debug -``` - -### Frontend-Entwicklung - -```bash -cd frontend -npm run dev -``` - -### Tests ausführen - -```bash -# Backend-Tests -cd backend -python -m pytest - -# Frontend-Tests -cd frontend -npm test -``` - -## API-Dokumentation - -Die REST API ist unter `/api/docs` verfügbar, wenn der Backend-Server läuft. - -### Wichtige Endpunkte - -- `GET /api/printers` - Liste aller Drucker -- `POST /api/jobs` - Neuen Druckauftrag erstellen -- `GET /api/jobs/{id}` - Auftragsstatus abrufen -- `POST /api/auth/login` - Benutzeranmeldung - -## Fehlerbehebung - -### Häufige Probleme - -#### SSL-Zertifikatsfehler - -```bash -# Zertifikate neu erstellen -python backend/app/create_ssl_cert.py -c backend/instance/ssl/myp.crt -k backend/instance/ssl/myp.key -n localhost -``` - -#### Port bereits in Verwendung - -```bash -# Prozesse auf Port 443 beenden -sudo lsof -ti:443 | xargs kill -9 -``` - -#### Datenbankfehler - -```bash -# Datenbank zurücksetzen -rm backend/database/myp.db -python backend/app/models.py -``` - -## Sicherheit - -- Alle Passwörter sind in `CREDENTIALS.md` dokumentiert -- SSL/TLS-Verschlüsselung für alle Verbindungen -- Session-basierte Authentifizierung -- Rate Limiting für API-Endpunkte - -## Lizenz - -Dieses Projekt ist für den internen Gebrauch bei Mercedes-Benz AG bestimmt. - -## Support - -Bei Problemen oder Fragen wenden Sie sich an das Entwicklungsteam oder erstellen Sie ein Issue im Repository. - -## Changelog - -### Version 3.0 - -- Konsolidierte Installer-Skripte -- Verbesserte SSL-Unterstützung -- GitHub OAuth-Integration -- Erweiterte Drucker-Management-Funktionen - -### Version 2.0 - -- Frontend-Neugestaltung mit Next.js -- REST API-Implementierung -- Docker-Unterstützung - -### Version 1.0 - -- Grundlegende Drucker-Management-Funktionen -- Flask-Backend -- SQLite-Datenbank diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 31dce9ca..00000000 --- a/ROADMAP.md +++ /dev/null @@ -1,169 +0,0 @@ -# MYP System - Roadmap - -## ✅ Abgeschlossen (Version 2.0) - -### Architektur-Vereinfachung -- [x] **Blueprint-Integration**: Alle Flask Blueprints in zentrale `app.py` integriert -- [x] **Code-Konsolidierung**: Über 25 Routen in einer Datei vereint -- [x] **Struktur-Optimierung**: Verbesserte Wartbarkeit und Übersichtlichkeit - -### Datenbank-Verbesserungen (KRITISCH) -- [x] **Write-Ahead Logging (WAL)**: SQLite WAL-Modus für bessere Concurrency -- [x] **Connection Pooling**: Optimierte SQLAlchemy-Engine mit 20+30 Verbindungen -- [x] **Intelligentes Caching**: Thread-sicheres Caching-System mit automatischer Invalidierung -- [x] **Automatisches Backup-System**: Tägliche komprimierte Backups mit 30-Tage Rotation -- [x] **Datenbank-Monitoring**: Gesundheitsprüfung und Performance-Überwachung -- [x] **Automatische Wartung**: Hintergrund-Scheduler für Optimierung und Maintenance -- [x] **Admin-API**: Vollständige REST-API für Datenbank-Verwaltung - -### Performance-Optimierungen -- [x] **Cache-Strategien**: - - User-Daten: 5-10 Minuten - - Printer-Status: 1-2 Minuten - - Job-Daten: 30 Sekunden - 5 Minuten - - Statistiken: 10 Minuten -- [x] **SQLite-Tuning**: 64MB Cache, Memory Mapping, Foreign Keys -- [x] **Query-Optimierung**: Automatische ANALYZE und PRAGMA optimize - -### Sicherheit & Stabilität -- [x] **Backup-Sicherheit**: Komprimierte Backups mit Pfad-Validierung -- [x] **Datenintegrität**: Regelmäßige Integritätsprüfungen -- [x] **Fehlerbehandlung**: Umfassendes Logging und Error Recovery -- [x] **Admin-Berechtigung**: Sichere API-Endpunkte für kritische Operationen - -## 🚀 Aktuelle Features (Version 2.0) - -### Core-Funktionalitäten -- ✅ Benutzer-Authentifizierung und -Verwaltung -- ✅ Drucker-Management mit Live-Status -- ✅ Job-Verwaltung (Erstellen, Bearbeiten, Verlängern, Löschen) -- ✅ Admin-Panel mit erweiterten Funktionen -- ✅ Kiosk-Modus für öffentliche Terminals -- ✅ Vollständige API für Frontend-Integration -- ✅ Robuste Fehlerbehandlung und Logging - -### Datenbank-Features -- ✅ WAL-Modus für bessere Performance -- ✅ Automatische Backups und Wiederherstellung -- ✅ Intelligentes Caching-System -- ✅ Performance-Monitoring -- ✅ Automatische Wartung und Optimierung - -### API-Endpunkte -- ✅ **Authentifizierung**: `/auth/login`, `/auth/logout` -- ✅ **Benutzer**: `/user/profile`, `/user/settings` -- ✅ **Jobs**: `/api/jobs`, `/api/jobs/` -- ✅ **Drucker**: `/api/printers`, `/api/printers/` -- ✅ **Admin**: `/api/admin/users`, `/api/admin/stats` -- ✅ **Kiosk**: `/api/kiosk/status`, `/api/kiosk/activate` -- ✅ **Datenbank**: `/api/admin/database/*` (Backup, Stats, Health) - -## 📊 Performance-Metriken (Version 2.0) - -### Verbesserungen gegenüber Version 1.0 -- **50-80% schnellere Abfragen** durch Caching -- **Verbesserte Concurrency** durch WAL-Modus -- **Automatische Wartung** reduziert manuelle Eingriffe um 90% -- **Proaktive Überwachung** verhindert 95% der Ausfälle - -### Aktuelle Benchmarks -- Durchschnittliche Response-Zeit: <100ms -- Cache-Hit-Rate: >80% -- Backup-Erfolgsrate: 100% -- System-Verfügbarkeit: >99.9% - -## 🎯 Geplante Features (Version 2.1) - -### Frontend-Verbesserungen -- [ ] **React/Next.js Migration**: Moderne Frontend-Architektur -- [ ] **Real-time Updates**: WebSocket-Integration für Live-Updates -- [ ] **Mobile Responsiveness**: Optimierung für mobile Geräte -- [ ] **Dark Mode**: Benutzerfreundliche Themes - -### Erweiterte Datenbank-Features -- [ ] **Replikation**: High Availability Setup -- [ ] **Metriken-Dashboard**: Real-time Performance-Monitoring -- [ ] **Adaptive Caching**: Intelligente Cache-Größenanpassung -- [ ] **Cloud-Backup**: Integration mit Cloud-Storage - -### Neue Funktionalitäten -- [ ] **Benachrichtigungssystem**: E-Mail/SMS-Alerts -- [ ] **Reporting**: Erweiterte Statistiken und Reports -- [ ] **Multi-Tenant**: Support für mehrere Organisationen -- [ ] **API-Versionierung**: RESTful API v2 - -## 🔧 Technische Verbesserungen (Version 2.1) - -### Architektur -- [ ] **Microservices**: Aufteilen in kleinere Services -- [ ] **Container-Orchestrierung**: Kubernetes-Support -- [ ] **Load Balancing**: Horizontale Skalierung -- [ ] **Service Mesh**: Istio-Integration - -### Monitoring & Observability -- [ ] **Prometheus Integration**: Metriken-Sammlung -- [ ] **Grafana Dashboards**: Visualisierung -- [ ] **Distributed Tracing**: Jaeger-Integration -- [ ] **Log Aggregation**: ELK-Stack - -### Sicherheit -- [ ] **OAuth2/OIDC**: Moderne Authentifizierung -- [ ] **RBAC**: Role-Based Access Control -- [ ] **Audit Logging**: Compliance-Features -- [ ] **Encryption**: End-to-End Verschlüsselung - -## 🚨 Kritische Prioritäten - -### Sofort (Version 2.0.1) -1. **Monitoring-Dashboard**: Admin-Interface für DB-Metriken -2. **Backup-Validierung**: Automatische Backup-Tests -3. **Performance-Alerts**: Proaktive Benachrichtigungen - -### Kurzfristig (Version 2.1) -1. **Frontend-Modernisierung**: React/Next.js Migration -2. **Real-time Features**: WebSocket-Integration -3. **Mobile Optimierung**: Responsive Design - -### Mittelfristig (Version 2.2) -1. **High Availability**: Replikation und Failover -2. **Erweiterte Analytics**: Business Intelligence -3. **Multi-Tenant Support**: Organisationsverwaltung - -### Langfristig (Version 3.0) -1. **Cloud-Native**: Kubernetes-Deployment -2. **Microservices**: Service-orientierte Architektur -3. **AI/ML Integration**: Predictive Analytics - -## 📈 Erfolgsmetriken - -### Technische KPIs -- **Verfügbarkeit**: >99.9% -- **Response-Zeit**: <100ms (95. Perzentil) -- **Fehlerrate**: <0.1% -- **Cache-Hit-Rate**: >80% - -### Business KPIs -- **Benutzer-Zufriedenheit**: >4.5/5 -- **System-Adoption**: >90% -- **Support-Tickets**: <5/Monat -- **Wartungszeit**: <2h/Monat - -## 🛠 Entwicklungsrichtlinien - -### Code-Qualität -- **Test-Coverage**: >90% -- **Code-Review**: Mandatory für alle Changes -- **Documentation**: Vollständige API-Dokumentation -- **Security**: Regelmäßige Security-Audits - -### Deployment -- **CI/CD**: Automatisierte Pipelines -- **Blue-Green**: Zero-Downtime Deployments -- **Rollback**: Schnelle Rollback-Mechanismen -- **Monitoring**: Kontinuierliche Überwachung - ---- - -**Letzte Aktualisierung**: Dezember 2024 -**Version**: 2.0 -**Status**: ✅ Datenbank-Verbesserungen vollständig implementiert \ No newline at end of file diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 90923d81..00000000 --- a/backend/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# MYP - Manage Your Printer - -Ein System zur Verwaltung und Steuerung von 3D-Druckern über TP-Link Tapo P110 Smart Plugs. - -## Verzeichnisstruktur - -Diese MYP-Installation ist wie folgt organisiert: - -- **app/** - Enthält den Anwendungscode (app.py, models.py) -- **docs/** - Enthält die Dokumentation (README, Anleitungen, Fehlerbehebung) -- **install/** - Enthält Installationsskripte und Konfigurationsdateien - -## Installation - -Zur Installation und Konfiguration nutzen Sie bitte das Hauptinstallationsskript: - -```bash -chmod +x setup_myp.sh -./setup_myp.sh -``` - -Das Skript führt Sie durch die verfügbaren Optionen: -1. Standardinstallation (nur MYP-Anwendung) -2. Kiosk-Modus Installation (vollautomatischer Start nach Boot) -3. Dokumentation anzeigen - -## Ausführliche Dokumentation - -Die vollständige Dokumentation finden Sie im `docs/`-Verzeichnis: - -- Allgemeine Anleitung: [docs/README.md](docs/README.md) -- Kiosk-Modus Anleitung: [docs/KIOSK-SETUP.md](docs/KIOSK-SETUP.md) -- Fehlerbehebung: [docs/COMMON_ERRORS.md](docs/COMMON_ERRORS.md) -- Entwicklungsplan: [docs/ROADMAP.md](docs/ROADMAP.md) - -## Funktionsumfang - -- Benutzer- und Rechteverwaltung (Admin/User) -- Verwaltung von Druckern und Smart Plugs -- Reservierungssystem für Drucker (Zeitplanung) -- Automatisches Ein-/Ausschalten der Drucker über Smart Plugs -- Statistikerfassung für Druckaufträge -- **NEU**: Kiosk-Modus für automatischen Start auf Raspberry Pi - -## Systemvoraussetzungen - -- Raspberry Pi 4 (oder kompatibel) -- Python 3.11+ -- Internetzugang für die Installation (danach offline nutzbar) -- TP-Link Tapo P110 Smart Plugs im lokalen Netzwerk - -## Erster Start - -Beim ersten Start wird eine leere Datenbank angelegt. Es muss ein erster Administrator angelegt werden: - -``` -POST /api/create-initial-admin -``` - -Mit folgendem JSON-Body: -```json -{ - "email": "admin@example.com", - "password": "sicheres-passwort", - "name": "Administrator" -} -``` - -## API-Dokumentation - -Die Anwendung stellt folgende REST-API-Endpunkte bereit: - -### Authentifizierung - -- `POST /auth/register` - Neuen Benutzer registrieren -- `POST /auth/login` - Anmelden (erstellt eine Session für 7 Tage) - -### Drucker - -- `GET /api/printers` - Alle Drucker auflisten -- `GET /api/printers/` - Einzelnen Drucker abrufen -- `POST /api/printers` - Neuen Drucker anlegen -- `DELETE /api/printers/` - Drucker löschen - -### Jobs/Reservierungen - -- `GET /api/jobs` - Alle Reservierungen abrufen -- `POST /api/jobs` - Neue Reservierung anlegen -- `GET /api/jobs/` - Reservierungsdetails abrufen -- `POST /api/jobs//finish` - Job beenden (Plug ausschalten) -- `POST /api/jobs//abort` - Job abbrechen (Plug ausschalten) -- `POST /api/jobs//extend` - Endzeit verschieben -- `GET /api/jobs//status` - Aktuellen Plug-Status abrufen -- `GET /api/jobs//remaining-time` - Restzeit in Sekunden abrufen -- `DELETE /api/jobs/` - Job löschen - -### Benutzer - -- `GET /api/users` - Alle Benutzer auflisten (nur Admin) -- `GET /api/users/` - Einzelnen Benutzer abrufen -- `DELETE /api/users/` - Benutzer löschen (nur Admin) - -### Sonstiges - -- `GET /api/stats` - Globale Statistik (Druckzeit, etc.) -- `GET /api/test` - Health-Check - -## Sicherheitshinweise - -- Diese Anwendung speichert alle Zugangsdaten direkt im Code und in der Datenbank. -- Die Anwendung sollte ausschließlich in einem geschützten, lokalen Netzwerk betrieben werden. -- Es wird keine Verschlüsselung für die API-Kommunikation verwendet. - -## Wartung und Troubleshooting - -- Die Logdatei `myp.log` enthält alle wichtigen Ereignisse und Fehler -- Die Datenbank wird in `database/myp.db` gespeichert -- Häufig auftretende Probleme und Lösungen finden sich in [COMMON_ERRORS.md](COMMON_ERRORS.md) -- Zukünftige Entwicklungspläne sind in [ROADMAP.md](ROADMAP.md) dokumentiert - diff --git a/backend/app/DATABASE_ENHANCEMENT.md b/backend/app/DATABASE_ENHANCEMENT.md deleted file mode 100644 index 2d57ab77..00000000 --- a/backend/app/DATABASE_ENHANCEMENT.md +++ /dev/null @@ -1,276 +0,0 @@ -# Datenbank-Verbesserungen - MYP System - -## Übersicht - -Das MYP-System wurde mit umfassenden Datenbank-Verbesserungen ausgestattet, die Stabilität, Sicherheit und Performance erheblich steigern. Diese Dokumentation beschreibt alle implementierten Features. - -## 🚀 Implementierte Features - -### 1. Write-Ahead Logging (WAL) -- **Aktiviert**: SQLite WAL-Modus für bessere Concurrency -- **Vorteile**: - - Gleichzeitige Lese- und Schreiboperationen - - Bessere Performance bei hoher Last - - Reduzierte Sperrzeiten -- **Konfiguration**: Automatisch aktiviert in `models.py` - -### 2. Connection Pooling -- **Engine-Optimierung**: Konfigurierte SQLAlchemy-Engine mit Pooling -- **Parameter**: - - Pool-Größe: 20 Verbindungen - - Max Overflow: 30 zusätzliche Verbindungen - - Connection Timeout: 30 Sekunden - - Pool Recycle: 3600 Sekunden - -### 3. Intelligentes Caching-System -- **Thread-sicherer Cache**: Implementiert mit `threading.local()` -- **Cache-Strategien**: - - User-Daten: 5-10 Minuten - - Printer-Status: 1-2 Minuten - - Job-Daten: 30 Sekunden - 5 Minuten - - Statistiken: 10 Minuten -- **Cache-Invalidierung**: Automatisch bei Datenänderungen - -### 4. Automatisches Backup-System -- **Tägliche Backups**: Automatisch um Mitternacht -- **Komprimierung**: Gzip-Komprimierung für Speichereffizienz -- **Rotation**: Automatische Bereinigung alter Backups (30 Tage) -- **Manueller Trigger**: Admin-Interface für sofortige Backups - -### 5. Datenbank-Monitoring -- **Gesundheitsprüfung**: Integritätschecks und Performance-Monitoring -- **Statistiken**: Detaillierte DB-Metriken -- **Wartungsalerts**: Automatische Benachrichtigungen bei Problemen - -### 6. Automatische Wartung -- **Scheduler**: Hintergrund-Thread für Wartungsaufgaben -- **Operationen**: - - ANALYZE für Query-Optimierung - - WAL-Checkpoints - - Incremental Vacuum - - PRAGMA optimize - -## 📁 Dateistruktur - -``` -backend/app/ -├── models.py # Erweiterte Modelle mit Caching -├── utils/ -│ └── database_utils.py # Backup & Monitoring Utilities -├── database/ -│ ├── myp_database.db # Hauptdatenbank -│ ├── myp_database.db-wal # WAL-Datei -│ ├── myp_database.db-shm # Shared Memory -│ └── backups/ # Backup-Verzeichnis -│ ├── myp_backup_20241201_120000.db.gz -│ └── ... -``` - -## 🔧 Konfiguration - -### SQLite-Optimierungen -```sql -PRAGMA journal_mode = WAL; -PRAGMA synchronous = NORMAL; -PRAGMA cache_size = -64000; # 64MB Cache -PRAGMA temp_store = MEMORY; -PRAGMA mmap_size = 268435456; # 256MB Memory Map -PRAGMA foreign_keys = ON; -``` - -### Cache-Konfiguration -```python -# Cache-Zeiten (Sekunden) -USER_CACHE_TIME = 300 # 5 Minuten -PRINTER_CACHE_TIME = 120 # 2 Minuten -JOB_CACHE_TIME = 180 # 3 Minuten -STATS_CACHE_TIME = 600 # 10 Minuten -``` - -## 🛠 API-Endpunkte - -### Datenbank-Statistiken -```http -GET /api/admin/database/stats -``` -**Response:** -```json -{ - "success": true, - "stats": { - "database_size_mb": 15.2, - "wal_size_mb": 2.1, - "journal_mode": "wal", - "cache_size": -64000, - "table_counts": { - "users": 25, - "printers": 8, - "jobs": 1247 - } - } -} -``` - -### Gesundheitsprüfung -```http -GET /api/admin/database/health -``` -**Response:** -```json -{ - "success": true, - "health": { - "status": "healthy", - "issues": [], - "recommendations": [] - } -} -``` - -### Backup erstellen -```http -POST /api/admin/database/backup -Content-Type: application/json - -{ - "compress": true -} -``` - -### Backup-Liste abrufen -```http -GET /api/admin/database/backups -``` - -### Backup wiederherstellen -```http -POST /api/admin/database/backup/restore -Content-Type: application/json - -{ - "backup_path": "/path/to/backup.db.gz" -} -``` - -### Datenbank optimieren -```http -POST /api/admin/database/optimize -``` - -### Alte Backups bereinigen -```http -POST /api/admin/database/backup/cleanup -Content-Type: application/json - -{ - "keep_days": 30 -} -``` - -## 📊 Performance-Verbesserungen - -### Vor den Verbesserungen -- Einzelne DB-Verbindung -- Keine Caching-Mechanismen -- Manuelle Backups erforderlich -- Keine Performance-Überwachung - -### Nach den Verbesserungen -- **50-80% schnellere Abfragen** durch Caching -- **Verbesserte Concurrency** durch WAL-Modus -- **Automatische Wartung** reduziert manuelle Eingriffe -- **Proaktive Überwachung** verhindert Ausfälle - -## 🔒 Sicherheitsfeatures - -### Backup-Sicherheit -- Komprimierte Backups reduzieren Speicherbedarf -- Automatische Rotation verhindert Speicherüberläufe -- Sicherheitsbackup vor Wiederherstellung - -### Zugriffskontrolle -- Alle Admin-Endpunkte erfordern Admin-Berechtigung -- Pfad-Validierung bei Backup-Operationen -- Fehlerbehandlung mit detailliertem Logging - -### Datenintegrität -- Regelmäßige Integritätsprüfungen -- WAL-Modus verhindert Datenverlust -- Foreign Key Constraints aktiviert - -## 🚨 Monitoring & Alerts - -### Automatische Überwachung -- **WAL-Dateigröße**: Alert bei >100MB -- **Freier Speicherplatz**: Warnung bei <1GB -- **Integritätsprüfung**: Tägliche Checks -- **Performance-Metriken**: Kontinuierliche Sammlung - -### Log-Kategorien -``` -[DATABASE] - Allgemeine Datenbank-Operationen -[BACKUP] - Backup-Operationen -[CACHE] - Cache-Operationen -[MAINTENANCE] - Wartungsaufgaben -[HEALTH] - Gesundheitsprüfungen -``` - -## 🔄 Wartungsplan - -### Automatisch (Hintergrund) -- **Täglich**: Backup, Optimierung, Gesundheitsprüfung -- **Wöchentlich**: Bereinigung alter Backups -- **Stündlich**: Cache-Bereinigung, WAL-Checkpoints - -### Manuell (Admin-Interface) -- Sofortige Backups vor kritischen Änderungen -- Manuelle Optimierung bei Performance-Problemen -- Backup-Wiederherstellung bei Datenverlust - -## 📈 Metriken & KPIs - -### Performance-Indikatoren -- Durchschnittliche Query-Zeit -- Cache-Hit-Rate -- WAL-Checkpoint-Häufigkeit -- Backup-Erfolgsrate - -### Kapazitätsplanung -- Datenbankwachstum pro Monat -- Backup-Speicherverbrauch -- Cache-Effizienz - -## 🛡 Disaster Recovery - -### Backup-Strategie -1. **Tägliche automatische Backups** -2. **30-Tage Aufbewahrung** -3. **Komprimierte Speicherung** -4. **Schnelle Wiederherstellung** - -### Recovery-Prozess -1. System stoppen -2. Backup auswählen -3. Wiederherstellung durchführen -4. Integritätsprüfung -5. System neu starten - -## 🎯 Nächste Schritte - -### Geplante Erweiterungen -- [ ] Replikation für High Availability -- [ ] Erweiterte Metriken-Dashboard -- [ ] Automatische Performance-Tuning -- [ ] Cloud-Backup-Integration - -### Optimierungsmöglichkeiten -- [ ] Adaptive Cache-Größen -- [ ] Intelligente Backup-Zeitpunkte -- [ ] Predictive Maintenance -- [ ] Real-time Monitoring Dashboard - ---- - -**Status**: ✅ Vollständig implementiert und getestet -**Version**: 2.0 -**Letzte Aktualisierung**: Dezember 2024 \ No newline at end of file diff --git a/backend/app/README.md b/backend/app/README.md deleted file mode 100644 index 4348cfdc..00000000 --- a/backend/app/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# MYP Platform - 3D-Drucker Reservierungssystem - -Ein Reservierungssystem für 3D-Drucker, das automatisch TP-Link Tapo P110 Smart Plugs steuert, um die Stromversorgung von Druckern basierend auf den reservierten Zeitslots zu verwalten. - -## Features - -- Reservierung von 3D-Druckern für bestimmte Zeiträume -- Automatische Steuerung der TP-Link Tapo P110 Smart Plugs -- Echtzeit-Überwachung von laufenden Druckaufträgen -- Administrationsbereich für Druckerverwaltung -- Benutzerauthentifizierung und Autorisierung -- Statistiken zu Druckaufträgen und Materialverbrauch -- Dark Mode für eine angenehme Nutzung bei schlechten Lichtverhältnissen -- Responsive Design für verschiedene Gerätegrößen - -## Technologie-Stack - -- **Backend**: Python 3.11 mit Flask -- **Frontend**: HTML/CSS/JavaScript mit Tailwind CSS -- **Datenbank**: SQLite mit SQLAlchemy ORM -- **Authentifizierung**: Flask-Login -- **Hardware-Integration**: PyP100 für Tapo Smart Plug Steuerung -- **Automatisierung**: Integrierter Job-Scheduler für Drucker-Steuerung - -## Systemanforderungen - -- Python 3.11+ -- Node.js und npm (für Tailwind CSS) -- Netzwerkzugang zu TP-Link Tapo P110 Smart Plugs -- Unterstützte Betriebssysteme: Linux, Windows, macOS - -## Installation - -1. **Repository klonen**: - ```bash - git clone - cd myp-platform - ``` - -2. **Python-Abhängigkeiten installieren**: - ```bash - cd backend/app - pip install -r requirements.txt - ``` - -3. **Node-Abhängigkeiten installieren und CSS bauen**: - ```bash - npm install - npm run build:css - ``` - -4. **Datenbank initialisieren**: - ```bash - python init_db.py - ``` - -5. **Server starten**: - ```bash - # Standardstart - python app.py - - # Oder mit spezifischem Port (empfohlen) - python app.py --port=5000 - ``` - -## API Endpunkte - -| Methode | Endpunkt | Beschreibung | -|---------|------------------------|--------------------------------------------------------| -| GET | /api/jobs | Gibt alle Jobs (für Admins) oder eigene Jobs zurück | -| GET | /api/jobs/active | Gibt alle aktiven Druckaufträge zurück | -| GET | /api/jobs/{id} | Gibt Details zu einem bestimmten Job zurück | -| POST | /api/jobs | Erstellt eine neue Druckerreservierung | -| GET | /api/printers | Gibt alle verfügbaren Drucker zurück | - -## Scheduler - -Das System enthält einen eingebauten Scheduler, der automatisch: -- Drucker einschaltet, wenn ein Auftrag beginnt -- Drucker ausschaltet, wenn ein Auftrag endet -- Den Status von Druckern aktualisiert -- Die Auftragsstatistiken sammelt - -## Konfiguration - -Die Konfiguration erfolgt über die Datei `config/settings.py`. Wichtige Einstellungen umfassen: -- Datenbankpfad -- Tapo-Zugangsdaten -- Drucker-Konfigurationen -- Logging-Einstellungen -- Webserver-Einstellungen - -## CSS-System - -Die Anwendung nutzt Tailwind CSS für das Styling mit separaten Dateien für Light- und Dark-Mode. - -### Tailwind Build - -```bash -# CSS bauen -npm run build:css - -# CSS im Watch-Modus (für Entwicklung) -npm run watch:css -``` - -## Sicherheit - -- Nur angemeldete Benutzer können Druckaufträge erstellen -- Nur Besitzer eines Druckauftrags können diesen verwalten -- Nur Administratoren haben Zugriff auf alle Jobs und Systemeinstellungen -- Passwörter werden mit bcrypt gesichert - -## Fehlerbehandlung und Logging - -Das System führt detaillierte Logs in folgenden Kategorien: -- App-Log (allgemeine Anwendungslogs) -- Auth-Log (Authentifizierungsereignisse) -- Jobs-Log (Druckauftragsoperationen) -- Printer-Log (Druckerstatusänderungen) -- Scheduler-Log (Automatisierungsaktionen) -- Error-Log (Fehlerprotokollierung) - -## Entwicklung - -Diese Anwendung ist für den Offline-Betrieb konzipiert und verwendet keine externen CDNs. -Weitere Informationen zur Entwicklungsumgebung finden Sie in der `TAILWIND_SETUP.md`. \ No newline at end of file diff --git a/backend/app/static/js/admin-live.js b/backend/app/static/js/admin-live.js index c5866c47..e9a5c6cb 100644 --- a/backend/app/static/js/admin-live.js +++ b/backend/app/static/js/admin-live.js @@ -18,35 +18,21 @@ class AdminLiveDashboard { } detectApiBaseUrl() { - // Versuche verschiedene Ports zu erkennen const currentHost = window.location.hostname; - const currentPort = window.location.port; const currentProtocol = window.location.protocol; + const currentPort = window.location.port; - console.log('🔍 Aktuelle URL-Informationen:', { - host: currentHost, - port: currentPort, - protocol: currentProtocol, - fullUrl: window.location.href - }); + console.log('🔍 Live Dashboard API URL Detection:', { currentHost, currentProtocol, currentPort }); // Wenn wir bereits auf dem richtigen Port sind, verwende relative URLs - if (currentPort === '5000' || !currentPort) { - console.log('✅ Verwende relative URLs (gleicher Port oder Standard)'); + if (currentPort === '443' || !currentPort) { + console.log('✅ Verwende relative URLs (HTTPS Port 443)'); return ''; } - // Wenn wir auf 8443 sind, versuche 5000 (häufiger Fall) - if (currentPort === '8443') { - const fallbackUrl = `http://${currentHost}:5000`; - console.log('🔄 Fallback von HTTPS:8443 zu HTTP:5000:', fallbackUrl); - return fallbackUrl; - } - - // Für andere Ports, verwende Standard-Backend-Port - const defaultPort = currentProtocol === 'https:' ? '8443' : '5000'; - const fallbackUrl = `${currentProtocol}//${currentHost}:${defaultPort}`; - console.log('🔄 Standard-Fallback:', fallbackUrl); + // Für alle anderen Fälle, verwende HTTPS auf Port 443 + const fallbackUrl = `https://${currentHost}`; + console.log('🔄 Fallback zu HTTPS:443:', fallbackUrl); return fallbackUrl; } diff --git a/backend/app/static/js/admin.js b/backend/app/static/js/admin.js index 2d4ebca9..c54b4a4d 100644 --- a/backend/app/static/js/admin.js +++ b/backend/app/static/js/admin.js @@ -12,34 +12,20 @@ let csrfToken; // Dynamische API-Base-URL-Erkennung function detectApiBaseUrl() { const currentHost = window.location.hostname; - const currentPort = window.location.port; const currentProtocol = window.location.protocol; + const currentPort = window.location.port; - console.log('🔍 Admin API URL-Informationen:', { - host: currentHost, - port: currentPort, - protocol: currentProtocol, - fullUrl: window.location.href - }); + console.log('🔍 Admin API URL Detection:', { currentHost, currentProtocol, currentPort }); // Wenn wir bereits auf dem richtigen Port sind, verwende relative URLs - if (currentPort === '5000' || !currentPort) { - console.log('✅ Verwende relative URLs (gleicher Port oder Standard)'); + if (currentPort === '443' || !currentPort) { + console.log('✅ Verwende relative URLs (HTTPS Port 443)'); return ''; } - // Wenn wir auf 8443 sind, versuche 5000 (häufiger Fall) - if (currentPort === '8443') { - const fallbackUrl = `http://${currentHost}:5000`; - console.log('🔄 Admin Fallback von HTTPS:8443 zu HTTP:5000:', fallbackUrl); - return fallbackUrl; - } - - // Für andere Ports, verwende Standard-Backend-Port - const defaultPort = currentProtocol === 'https:' ? '8443' : '5000'; - const fallbackUrl = `${currentProtocol}//${currentHost}:${defaultPort}`; - console.log('🔄 Admin Standard-Fallback:', fallbackUrl); - + // Für alle anderen Fälle, verwende HTTPS auf Port 443 + const fallbackUrl = `https://${currentHost}`; + console.log('🔄 Admin Fallback zu HTTPS:443:', fallbackUrl); return fallbackUrl; } diff --git a/backend/myp.service b/backend/myp.service new file mode 100644 index 00000000..467b68b4 --- /dev/null +++ b/backend/myp.service @@ -0,0 +1,30 @@ +[Unit] +Description=MYP Reservation Platform Backend +After=network.target +Wants=network.target + +[Service] +Type=simple +User=user +Group=user +WorkingDirectory=/home/user/Projektarbeit-MYP/backend/app +Environment=PYTHONPATH=/home/user/Projektarbeit-MYP/backend/app +Environment=FLASK_ENV=production +Environment=FLASK_APP=app.py +ExecStart=/home/user/Projektarbeit-MYP/backend/venv/bin/python3 app.py --host 0.0.0.0 --port 443 --cert certs/backend.crt --key certs/backend.key +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=myp-backend + +# Security settings +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/home/user/Projektarbeit-MYP/backend/app/logs +ReadWritePaths=/home/user/Projektarbeit-MYP/backend/app/database + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a9fc4794..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,96 +0,0 @@ -version: '3' - -services: - # Backend (Flask) auf Port 443 mit SSL - backend: - build: - context: ./backend - dockerfile: Dockerfile - container_name: myp-backend - restart: unless-stopped - hostname: raspberrypi - ports: - - "80:80" # HTTP Fallback - - "443:443" # HTTPS - volumes: - - ./backend:/app - - ./backend/logs:/app/logs - - ./backend/instance:/app/instance - networks: - - myp-network - environment: - - FLASK_APP=app/app.py - - FLASK_ENV=production - - SSL_ENABLED=true - - SSL_HOSTNAME=raspberrypi - command: python -m app.app --dual-protocol - healthcheck: - test: ["CMD", "curl", "-k", "https://localhost:443/health || curl http://localhost:80/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Next.js Frontend - frontend: - build: - context: ./frontend - dockerfile: Dockerfile - container_name: myp-rp - restart: unless-stopped - environment: - - NODE_ENV=production - - NEXT_PUBLIC_API_URL=https://raspberrypi:443 - - NEXT_PUBLIC_BACKEND_HOST=raspberrypi:443 - volumes: - - ./frontend:/app - - /app/node_modules - - /app/.next - ports: - - "3000:3000" - networks: - - myp-network - healthcheck: - test: ["CMD", "wget", "--spider", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Caddy Proxy für Frontend auf Port 443 mit SSL - caddy: - image: caddy:2.7-alpine - container_name: myp-caddy - restart: unless-stopped - hostname: m040tbaraspi001 - ports: - - "80:80" - - "443:443" - volumes: - - ./frontend/docker/caddy/Caddyfile:/etc/caddy/Caddyfile - - caddy_data:/data - - caddy_config:/config - - ./backend/instance/ssl:/etc/caddy/ssl - networks: - - myp-network - extra_hosts: - - "host.docker.internal:host-gateway" - - "raspberrypi:backend" - - "m040tbaraspi001.de040.corpintra.net:127.0.0.1" - environment: - - CADDY_HOST=m040tbaraspi001.de040.corpintra.net - - CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net - cap_add: - - NET_ADMIN - depends_on: - - backend - - frontend - -networks: - myp-network: - driver: bridge - -volumes: - caddy_data: - caddy_config: - backend_ssl: \ No newline at end of file diff --git a/docker-compose_copy.yml b/docker-compose_copy.yml deleted file mode 100644 index a71c20d8..00000000 --- a/docker-compose_copy.yml +++ /dev/null @@ -1,95 +0,0 @@ -version: '3' - -services: - # Backend (Flask) auf Port 443 mit SSL - backend: - build: - context: ./backend - dockerfile: Dockerfile - container_name: myp-backend - restart: unless-stopped - hostname: raspberrypi - ports: - - "80:80" # HTTP Fallback - - "443:443" # HTTPS - volumes: - - ./backend:/app - - ./backend/logs:/app/logs - - ./backend/instance:/app/instance - networks: - - myp-network - environment: - - FLASK_APP=app/app.py - - FLASK_ENV=production - - SSL_ENABLED=true - - SSL_HOSTNAME=raspberrypi - healthcheck: - test: ["CMD", "curl", "-k", "https://localhost:443/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Next.js Frontend - frontend: - build: - context: ./frontend - dockerfile: Dockerfile - container_name: myp-rp - restart: unless-stopped - environment: - - NODE_ENV=production - - NEXT_PUBLIC_API_URL=https://raspberrypi:443 - - NEXT_PUBLIC_BACKEND_HOST=raspberrypi:443 - volumes: - - ./frontend:/app - - /app/node_modules - - /app/.next - ports: - - "3000:3000" - networks: - - myp-network - healthcheck: - test: ["CMD", "wget", "--spider", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Caddy Proxy für Frontend auf Port 443 mit SSL - caddy: - image: caddy:2.7-alpine - container_name: myp-caddy - restart: unless-stopped - hostname: m040tbaraspi001 - ports: - - "80:80" - - "443:443" - volumes: - - ./frontend/docker/caddy/Caddyfile:/etc/caddy/Caddyfile - - caddy_data:/data - - caddy_config:/config - - ./backend/instance/ssl:/etc/caddy/ssl - networks: - - myp-network - extra_hosts: - - "host.docker.internal:host-gateway" - - "raspberrypi:backend" - - "m040tbaraspi001.de040.corpintra.net:127.0.0.1" - environment: - - CADDY_HOST=m040tbaraspi001.de040.corpintra.net - - CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net - cap_add: - - NET_ADMIN - depends_on: - - backend - - frontend - -networks: - myp-network: - driver: bridge - -volumes: - caddy_data: - caddy_config: - backend_ssl: \ No newline at end of file diff --git a/BLUEPRINT_INTEGRATION.md b/docs/BLUEPRINT_INTEGRATION.md similarity index 100% rename from BLUEPRINT_INTEGRATION.md rename to docs/BLUEPRINT_INTEGRATION.md diff --git a/backend/COMMON_ERRORS.md b/docs/COMMON_ERRORS.md similarity index 100% rename from backend/COMMON_ERRORS.md rename to docs/COMMON_ERRORS.md diff --git a/CREDENTIALS.md b/docs/CREDENTIALS.md similarity index 100% rename from CREDENTIALS.md rename to docs/CREDENTIALS.md diff --git a/backend/DEPLOYMENT.md b/docs/DEPLOYMENT.md similarity index 100% rename from backend/DEPLOYMENT.md rename to docs/DEPLOYMENT.md diff --git a/GLASSMORPHISM_ENHANCEMENT.md b/docs/GLASSMORPHISM_ENHANCEMENT.md similarity index 100% rename from GLASSMORPHISM_ENHANCEMENT.md rename to docs/GLASSMORPHISM_ENHANCEMENT.md diff --git a/GLASSMORPHISM_SUMMARY.md b/docs/GLASSMORPHISM_SUMMARY.md similarity index 100% rename from GLASSMORPHISM_SUMMARY.md rename to docs/GLASSMORPHISM_SUMMARY.md diff --git a/INSTALLATION.md b/docs/INSTALLATION.md similarity index 100% rename from INSTALLATION.md rename to docs/INSTALLATION.md diff --git a/LICENSE.md b/docs/LICENSE.md similarity index 100% rename from LICENSE.md rename to docs/LICENSE.md diff --git a/frontend/PRODUCTION_DEPLOYMENT.md b/docs/PRODUCTION_DEPLOYMENT.md similarity index 100% rename from frontend/PRODUCTION_DEPLOYMENT.md rename to docs/PRODUCTION_DEPLOYMENT.md diff --git a/backend/RASPBERRY_PI_SETUP.md b/docs/RASPBERRY_PI_SETUP.md similarity index 100% rename from backend/RASPBERRY_PI_SETUP.md rename to docs/RASPBERRY_PI_SETUP.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..d43562ba --- /dev/null +++ b/docs/README.md @@ -0,0 +1,191 @@ +# MYP Reservation Platform + +Mercedes-Benz Werk 040 Berlin - 3D-Drucker Reservierungsplattform + +## 🚀 Schnellstart + +### Voraussetzungen + +- **Backend (Raspberry Pi)**: Python 3.11, systemd +- **Frontend (m040tbaraspi001)**: Docker, Docker Compose + +### Installation + +#### Backend Installation (Raspberry Pi) + +```bash +# Repository klonen +git clone +cd Projektarbeit-MYP + +# Backend installieren +./install.sh backend +``` + +#### Frontend Installation (m040tbaraspi001) + +```bash +# Repository klonen +git clone +cd Projektarbeit-MYP + +# Frontend installieren +./install.sh frontend +``` + +### Services starten + +#### Backend +```bash +sudo systemctl start myp.service +sudo systemctl status myp.service +``` + +#### Frontend +```bash +cd frontend +docker-compose up -d +docker-compose logs -f +``` + +## 🌐 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 + +### Backend Debug-Modus +```bash +cd backend/app +python3.11 app.py --debug +``` + +### Frontend Development +```bash +cd frontend +npm run dev +``` + +## 📁 Projektstruktur + +``` +Projektarbeit-MYP/ +├── backend/ +│ ├── app/ +│ │ ├── certs/ # TLS-Zertifikate +│ │ ├── database/ # SQLite-Datenbank +│ │ ├── logs/ # Anwendungslogs +│ │ └── app.py # Hauptanwendung +│ ├── myp.service # systemd Service +│ └── requirements.txt # Python-Abhängigkeiten +├── frontend/ +│ ├── certs/ # TLS-Zertifikate +│ ├── docker/ +│ │ └── caddy/ +│ │ └── Caddyfile # Reverse Proxy Konfiguration +│ ├── src/ # Next.js Anwendung +│ └── docker-compose.yml +├── docs/ # Dokumentation +├── scripts/ # Hilfsskripte +└── install.sh # Zentraler Installer +``` + +## 🔒 Sicherheit + +- HTTPS-only (Port 443) +- Selbstsignierte TLS-Zertifikate +- HTTP → HTTPS Redirect +- Security Headers (HSTS, CSP, etc.) + +## 📝 Logs + +### Backend +```bash +# systemd Journal +sudo journalctl -u myp.service -f + +# Anwendungslogs +tail -f backend/app/logs/app/app.log +``` + +### Frontend +```bash +# Docker Logs +docker-compose logs -f + +# Caddy Logs +docker-compose logs caddy +``` + +## 🆘 Troubleshooting + +### Backend startet nicht +```bash +# Service Status prüfen +sudo systemctl status myp.service + +# Logs prüfen +sudo journalctl -u myp.service --no-pager + +# Zertifikate prüfen +ls -la backend/app/certs/ +``` + +### Frontend nicht erreichbar +```bash +# Container Status prüfen +docker-compose ps + +# Netzwerk prüfen +docker network ls + +# Zertifikate prüfen +ls -la frontend/certs/ +``` + +### Verbindungsprobleme +```bash +# DNS auflösen +nslookup raspberrypi +nslookup m040tbaraspi001.de040.corpintra.net + +# Ports prüfen +netstat -tlnp | grep :443 +``` + +## 📋 Version + +- **Version**: 3.2-final +- **Build**: Production +- **Datum**: $(date) + +## 👥 Support + +Bei Problemen wenden Sie sich an das IT-Team des Mercedes-Benz Werk 040 Berlin. + diff --git a/backend/ROADMAP.md b/docs/ROADMAP.md similarity index 100% rename from backend/ROADMAP.md rename to docs/ROADMAP.md diff --git a/backend/SECURITY.md b/docs/SECURITY.md similarity index 100% rename from backend/SECURITY.md rename to docs/SECURITY.md diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 8faebf07..00000000 --- a/frontend/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# MYP - Manage Your Printer - -MYP (Manage Your Printer) ist eine Webanwendung zur Reservierung von 3D-Druckern. -Sie wurde im Rahmen des Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt. - -## Deployment - -### Voraussetzungen - -- Netzwerk auf Raspberry Pi ist eingerichtet -- Docker ist installiert - -### Schritte - -1. Docker-Container bauen (docker/build.sh) -2. Docker-Container speichern (docker/save.sh caddy:2.8 myp-rp:latest) -3. Docker-Container auf Raspberry Pi bereitstellen (docker/deploy.sh) - -## Entwicklerinformationen - -### Raspberry Pi Einstellungen - -Auf dem Raspberry Pi wurde Raspbian Lite installiert. -Unter /srv/* sind die Projektdateien zu finden. - -### Anmeldedaten - -``` -Benutzer: myp -Passwort: (persönlich bekannt) -``` - diff --git a/frontend/configure_ssl.js b/frontend/configure_ssl.js deleted file mode 100644 index 736d528a..00000000 --- a/frontend/configure_ssl.js +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env node - -/** - * Dieses Skript konfiguriert das Next.js-Frontend, um das selbstsignierte SSL-Zertifikat zu akzeptieren - * und die richtigen SSL-Einstellungen im Frontend zu setzen. - */ - -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); - -// Pfade definieren -const ENV_LOCAL_PATH = path.join(__dirname, '.env.local'); -const ENV_FRONTEND_PATH = path.join(__dirname, 'env.frontend'); -const SSL_DIR = path.join(__dirname, 'ssl'); -const NEXT_CONFIG_PATH = path.join(__dirname, 'next.config.js'); - -console.log('=== Frontend-SSL-Konfiguration ==='); - -// Verzeichnis erstellen, falls es nicht existiert -if (!fs.existsSync(SSL_DIR)) { - console.log(`SSL-Verzeichnis wird erstellt: ${SSL_DIR}`); - fs.mkdirSync(SSL_DIR, { recursive: true }); -} - -// Prüfen, ob SSL-Zertifikate existieren -if (!fs.existsSync(path.join(SSL_DIR, 'myp.crt')) || !fs.existsSync(path.join(SSL_DIR, 'myp.key'))) { - console.log('SSL-Zertifikate nicht gefunden. Prüfe Backend-Verzeichnis...'); - - // Versuche, die Zertifikate aus dem Backend zu kopieren - const backendCertPath = path.join('/home/user/Projektarbeit-MYP/backend/certs/myp.crt'); - const backendKeyPath = path.join('/home/user/Projektarbeit-MYP/backend/certs/myp.key'); - - if (fs.existsSync(backendCertPath) && fs.existsSync(backendKeyPath)) { - console.log('Zertifikate im Backend-Verzeichnis gefunden. Kopiere...'); - fs.copyFileSync(backendCertPath, path.join(SSL_DIR, 'myp.crt')); - fs.copyFileSync(backendKeyPath, path.join(SSL_DIR, 'myp.key')); - console.log('Zertifikate erfolgreich in das Frontend-Verzeichnis kopiert.'); - } else { - console.error('SSL-Zertifikate nicht gefunden. Bitte zuerst das Backend-Skript ausführen.'); - process.exit(1); - } -} - -console.log('SSL-Zertifikate gefunden. Konfiguriere Frontend...'); - -// Umgebungsvariablen konfigurieren -function updateEnvFile() { - try { - let envContent; - - // .env.local erstellen oder aktualisieren - if (fs.existsSync(ENV_LOCAL_PATH)) { - envContent = fs.readFileSync(ENV_LOCAL_PATH, 'utf8'); - } else if (fs.existsSync(ENV_FRONTEND_PATH)) { - envContent = fs.readFileSync(ENV_FRONTEND_PATH, 'utf8'); - } else { - envContent = `# MYP Frontend Umgebungsvariablen\n`; - } - - // SSL-Konfigurationen mit alternativen Testoptionen - const sslConfigs = [ - 'NODE_TLS_REJECT_UNAUTHORIZED=0', - 'HTTPS=true', - 'SSL_CRT_FILE=./ssl/myp.crt', - 'SSL_KEY_FILE=./ssl/myp.key', - 'NEXT_PUBLIC_API_URL=https://raspberrypi:443', - 'NEXT_PUBLIC_BACKEND_HOST=raspberrypi:443', - 'NEXT_PUBLIC_BACKEND_PROTOCOL=https', - - // Alternative Konfigurationen für Testversuche (auskommentiert) - '# Alternative ohne HTTPS', - '# HTTPS=false', - '# NEXT_PUBLIC_API_URL=http://raspberrypi:80', - '# NEXT_PUBLIC_BACKEND_HOST=raspberrypi:80', - '# NEXT_PUBLIC_BACKEND_PROTOCOL=http', - - '# Alternative mit IP statt Hostname', - '# NEXT_PUBLIC_API_URL=https://192.168.0.105:443', - '# NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:443', - - '# Alternative mit localhost', - '# NEXT_PUBLIC_API_URL=https://192.168.0.105:5000', - '# NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000', - - '# Alternative Ports testen', - '# NEXT_PUBLIC_API_URL=https://raspberrypi:8443', - '# NEXT_PUBLIC_BACKEND_HOST=raspberrypi:8443', - '# NEXT_PUBLIC_API_URL=http://raspberrypi:8080', - '# NEXT_PUBLIC_BACKEND_HOST=raspberrypi:8080' - ]; - - // Existierende Konfigurationen aktualisieren - sslConfigs.forEach(config => { - const [key, value] = config.split('='); - const regex = new RegExp(`^${key}=.*$`, 'm'); - - if (envContent.match(regex)) { - // Update existierende Konfiguration - envContent = envContent.replace(regex, config); - } else { - // Neue Konfiguration hinzufügen - envContent += `\n${config}`; - } - }); - - // Speichern der aktualisierten Umgebungsvariablen - fs.writeFileSync(ENV_LOCAL_PATH, envContent); - console.log('.env.local Datei aktualisiert mit SSL-Konfigurationen'); - return true; - } catch (error) { - console.error(`Fehler bei der Aktualisierung der Umgebungsvariablen: ${error.message}`); - return false; - } -} - -// Next.js-Konfiguration aktualisieren -function updateNextConfig() { - try { - let configContent; - - // next.config.js erstellen oder aktualisieren - if (fs.existsSync(NEXT_CONFIG_PATH)) { - configContent = fs.readFileSync(NEXT_CONFIG_PATH, 'utf8'); - } else { - configContent = `/** @type {import('next').NextConfig} */\n\nconst nextConfig = {}\n\nmodule.exports = nextConfig\n`; - } - - // Prüfen, ob bereits eine HTTPS-Konfiguration vorhanden ist - if (configContent.includes('serverOptions:') && configContent.includes('https:')) { - console.log('HTTPS-Konfiguration ist bereits in der next.config.js vorhanden.'); - return true; - } - - // HTTPS-Konfiguration hinzufügen - const httpsConfig = ` -/** @type {import('next').NextConfig} */ -const fs = require('fs'); -const path = require('path'); - -const nextConfig = { - reactStrictMode: true, - webpack: (config) => { - return config; - }, - // HTTPS-Konfiguration für die Entwicklung - devServer: { - https: { - key: fs.readFileSync(path.resolve(__dirname, 'ssl/myp.key')), - cert: fs.readFileSync(path.resolve(__dirname, 'ssl/myp.crt')), - }, - }, - // Konfiguration für selbstsignierte Zertifikate - serverOptions: { - https: { - key: fs.readFileSync(path.resolve(__dirname, 'ssl/myp.key')), - cert: fs.readFileSync(path.resolve(__dirname, 'ssl/myp.crt')), - }, - }, - // Zusätzliche Konfigurationen - async rewrites() { - return [ - { - source: '/api/:path*', - destination: 'https://raspberrypi:443/api/:path*', - }, - ] - } -}; - -module.exports = nextConfig; -`; - - // Speichern der aktualisierten Next.js-Konfiguration - fs.writeFileSync(NEXT_CONFIG_PATH, httpsConfig); - console.log('next.config.js Datei aktualisiert mit HTTPS-Konfiguration'); - return true; - } catch (error) { - console.error(`Fehler bei der Aktualisierung der Next.js-Konfiguration: ${error.message}`); - return false; - } -} - -// Update der Fetch-Konfiguration -function updateFetchConfig() { - try { - const fetchConfigPath = path.join(__dirname, 'src', 'utils', 'api-config.ts'); - - if (!fs.existsSync(fetchConfigPath)) { - console.warn('Datei api-config.ts nicht gefunden. Überspringe Aktualisierung.'); - return true; - } - - // Lesen der aktuellen Konfiguration - let configContent = fs.readFileSync(fetchConfigPath, 'utf8'); - - // Sicherstellen, dass SSL-Verbindungen akzeptiert werden - if (!configContent.includes('NODE_TLS_REJECT_UNAUTHORIZED=0')) { - // Hinzufügen eines Kommentars zu Beginn der Datei - configContent = `// SSL-Verbindungen akzeptieren (selbstsignierte Zertifikate) -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - -${configContent}`; - } - - // Speichern der aktualisierten Fetch-Konfiguration - fs.writeFileSync(fetchConfigPath, configContent); - console.log('api-config.ts Datei aktualisiert, um selbstsignierte Zertifikate zu akzeptieren'); - return true; - } catch (error) { - console.error(`Fehler bei der Aktualisierung der Fetch-Konfiguration: ${error.message}`); - return false; - } -} - -// Abhängigkeiten installieren -function installDependencies() { - try { - console.log('Installiere benötigte Abhängigkeiten...'); - execSync('npm install --save-dev https-localhost', { stdio: 'inherit' }); - console.log('Abhängigkeiten erfolgreich installiert'); - return true; - } catch (error) { - console.error(`Fehler bei der Installation der Abhängigkeiten: ${error.message}`); - return false; - } -} - -// Frontend neu starten -function restartFrontend() { - try { - console.log('Starte Frontend-Server neu...'); - - // Prüfen, ob wir uns in Docker befinden - if (process.env.CONTAINER) { - console.log('Docker-Umgebung erkannt, verwende Docker-Befehle...'); - execSync('docker-compose restart frontend', { stdio: 'inherit' }); - } else { - console.log('Lokale Umgebung, starte Next.js-Entwicklungsserver neu...'); - // Stoppe möglicherweise laufende Prozesse - try { - execSync('npx kill-port 3000', { stdio: 'ignore' }); - } catch (e) { - // Ignorieren, falls kein Prozess läuft - } - - // Starte den Entwicklungsserver mit HTTPS - console.log('Frontend wird mit HTTPS gestartet. Verwende https://localhost:3000 zum Zugriff.'); - console.log('Das Frontend wird im Hintergrund gestartet. Verwenden Sie "npm run dev", um es manuell zu starten.'); - } - - console.log('Frontend-Server erfolgreich konfiguriert.'); - return true; - } catch (error) { - console.error(`Fehler beim Neustart des Frontend-Servers: ${error.message}`); - return false; - } -} - -// Hauptfunktion -async function main() { - let success = true; - - success = updateEnvFile() && success; - success = updateNextConfig() && success; - success = updateFetchConfig() && success; - success = installDependencies() && success; - success = restartFrontend() && success; - - if (success) { - console.log('\n=== Konfiguration erfolgreich abgeschlossen ==='); - console.log('Das Frontend wurde für die Verwendung von HTTPS mit dem selbstsignierten Zertifikat konfiguriert.'); - console.log('Sie können nun auf das Frontend über https://localhost:3000 zugreifen.'); - console.log('Bei Sicherheitswarnungen im Browser können Sie das Zertifikat manuell akzeptieren.'); - } else { - console.error('\n=== Konfiguration nicht vollständig abgeschlossen ==='); - console.error('Es gab Probleme bei der Konfiguration des Frontends.'); - console.error('Bitte überprüfen Sie die Fehlermeldungen und versuchen Sie es erneut.'); - } -} - -// Ausführen der Hauptfunktion -main().catch(error => { - console.error(`Unerwarteter Fehler: ${error.message}`); - process.exit(1); -}); \ No newline at end of file diff --git a/frontend/debug-server/src/app.js b/frontend/debug-server/src/app.js index d2611ee0..880fcbb5 100644 --- a/frontend/debug-server/src/app.js +++ b/frontend/debug-server/src/app.js @@ -10,7 +10,7 @@ const PORT = process.env.PORT || 8081; // Konfigurationsdatei const CONFIG_FILE = path.join(__dirname, '../../../.env.local'); -const DEFAULT_BACKEND_URL = 'http://192.168.0.105:5000'; +const DEFAULT_BACKEND_URL = 'https://raspberrypi'; // Middleware app.use(express.json()); diff --git a/frontend/docker-compose.development.yml b/frontend/docker-compose.development.yml deleted file mode 100644 index cc2e9db5..00000000 --- a/frontend/docker-compose.development.yml +++ /dev/null @@ -1,62 +0,0 @@ -version: '3' - -services: - # Next.js Frontend - frontend: - build: - context: . - dockerfile: Dockerfile - container_name: myp-frontend - restart: unless-stopped - environment: - - NODE_ENV=production - - NEXT_PUBLIC_API_URL=https://raspberrypi:443 - - NEXT_PUBLIC_BACKEND_HOST=raspberrypi:443 - - PORT=80 - volumes: - - ./certs:/app/certs - ports: - - "80" - networks: - - myp-network - healthcheck: - test: ["CMD", "wget", "--spider", "http://localhost:80/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Caddy Proxy für SSL-Terminierung - caddy: - image: caddy:2.7-alpine - container_name: myp-caddy - restart: unless-stopped - ports: - - "80:80" - - "443:443" - volumes: - - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile - - ./certs:/etc/caddy/certs - - caddy_data:/data - - caddy_config:/config - networks: - - myp-network - depends_on: - - frontend - extra_hosts: - - "host.docker.internal:host-gateway" - - "raspberrypi:192.168.0.105" - - "m040tbaraspi001.de040.corpintra.net:127.0.0.1" - environment: - - CADDY_HOST=m040tbaraspi001.de040.corpintra.net - - CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net - cap_add: - - NET_ADMIN - -networks: - myp-network: - driver: bridge - -volumes: - caddy_data: - caddy_config: \ No newline at end of file diff --git a/frontend/docker-compose.frontend.yml b/frontend/docker-compose.frontend.yml index 463db433..03026f23 100644 --- a/frontend/docker-compose.frontend.yml +++ b/frontend/docker-compose.frontend.yml @@ -1,5 +1,5 @@ -# 🎨 MYP Frontend - Entwicklungsumgebung Konfiguration -# Frontend-Service für die Entwicklung mit Raspberry Pi Backend +# 🎨 MYP Frontend - Produktionsumgebung Konfiguration +# Frontend-Service für die Produktion mit Raspberry Pi Backend version: '3.8' @@ -8,152 +8,64 @@ services: frontend: build: context: . - dockerfile: Dockerfile.dev - args: - - BUILDKIT_INLINE_CACHE=1 - - NODE_ENV=development - image: myp/frontend:dev - container_name: myp-frontend-dev + dockerfile: Dockerfile + container_name: myp-frontend restart: unless-stopped - environment: - - NODE_ENV=development - - NEXT_TELEMETRY_DISABLED=1 - - # Backend API Konfiguration (Raspberry Pi) - - NEXT_PUBLIC_API_URL=http://192.168.0.105:5000 - - NEXT_PUBLIC_BACKEND_HOST=192.168.0.105:5000 - - # Frontend Server - - PORT=3000 - - HOSTNAME=0.0.0.0 - - # Auth Konfiguration (Entwicklung) - - NEXTAUTH_URL=http://localhost:3000 - - NEXTAUTH_SECRET=dev-frontend-auth-secret - - # Debug-Einstellungen - - DEBUG=true - - NEXT_DEBUG=true - + - NODE_ENV=production + - NEXT_PUBLIC_API_URL=https://raspberrypi + - NEXT_PUBLIC_BACKEND_HOST=raspberrypi + - NEXT_PUBLIC_FRONTEND_URL=https://m040tbaraspi001.de040.corpintra.net + - NEXTAUTH_URL=https://m040tbaraspi001.de040.corpintra.net + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-myp-secret-key-2024} + - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID} + - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET} + - NEXT_PUBLIC_OAUTH_CALLBACK_URL=https://m040tbaraspi001.de040.corpintra.net/auth/login/callback volumes: - - .:/app - - /app/node_modules - - /app/.next - - ./public:/app/public:ro - + - ./certs:/app/certs ports: - "3000:3000" # Direkter Port-Zugang für Frontend-Server - networks: - - frontend-network - - extra_hosts: - - "raspberrypi:192.168.0.105" - + - myp-network healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s - - labels: - - "service.type=frontend" - - "service.name=myp-frontend-dev" - - "service.environment=development" - # === FRONTEND CACHE (Optional: Redis für Session Management) === - frontend-cache: - image: redis:7.2-alpine - container_name: myp-frontend-cache + # === CADDY PROXY === + caddy: + image: caddy:2-alpine + container_name: myp-caddy restart: unless-stopped - - command: redis-server --appendonly yes --requirepass ${FRONTEND_REDIS_PASSWORD:-frontend_cache_password} - - volumes: - - frontend_redis_data:/data - ports: - - "6380:6379" # Separater Port vom Backend-Cache - - networks: - - frontend-network - - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 3 - - # === FRONTEND CDN/NGINX (Statische Assets) === - frontend-cdn: - image: nginx:alpine - container_name: myp-frontend-cdn - restart: unless-stopped - + - "80:80" + - "443:443" volumes: - - ./public:/usr/share/nginx/html/static:ro - - ./nginx.conf:/etc/nginx/nginx.conf:ro - - frontend_cdn_cache:/var/cache/nginx - - ports: - - "8080:80" # Separater Port für statische Assets - + - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile + - ./certs:/etc/ssl/certs/myp + - caddy_data:/data + - caddy_config:/config networks: - - frontend-network - + - myp-network depends_on: - frontend - - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/health"] - interval: 30s - timeout: 10s - retries: 3 - - labels: - - "service.type=cdn" - - "service.name=myp-frontend-cdn" + environment: + - CADDY_INGRESS_NETWORKS=myp-network # === PERSISTENTE VOLUMES === volumes: - frontend_data: + caddy_data: driver: local - - frontend_cache: - driver: local - - frontend_redis_data: - driver: local - - frontend_cdn_cache: + caddy_config: driver: local -# === FRONTEND-NETZWERK === +# === NETZWERK === networks: - frontend-network: + myp-network: driver: bridge - driver_opts: - com.docker.network.enable_ipv6: "false" - com.docker.network.bridge.enable_ip_masquerade: "true" labels: - - "description=MYP Frontend Server Netzwerk" + - "description=MYP Production Network" - "project=myp-frontend" - - "tier=frontend" - -# === KONFIGURATION FÜR FRONTEND === -x-frontend-defaults: &frontend-defaults - restart: unless-stopped - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" - labels: "service,environment,tier" - -x-healthcheck-frontend: &frontend-healthcheck - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s \ No newline at end of file + - "tier=production" \ No newline at end of file diff --git a/frontend/docker-compose.production.yml b/frontend/docker-compose.production.yml deleted file mode 100644 index a8b18a0a..00000000 --- a/frontend/docker-compose.production.yml +++ /dev/null @@ -1,62 +0,0 @@ -version: '3.8' - -services: - # Next.js Frontend - frontend: - build: - context: . - dockerfile: Dockerfile - container_name: myp-frontend-prod - restart: unless-stopped - environment: - - NODE_ENV=production - - NEXT_PUBLIC_API_URL=https://raspberrypi:443 - - NEXT_PUBLIC_BACKEND_HOST=raspberrypi:443 - - PORT=80 - volumes: - - ./certs:/app/certs - - frontend_data:/usr/src/app/db - networks: - - myp-network - healthcheck: - test: ["CMD", "wget", "--spider", "http://localhost:80/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Caddy Proxy für SSL-Terminierung - caddy: - image: caddy:2.7-alpine - container_name: myp-caddy-prod - restart: unless-stopped - ports: - - "80:80" - - "443:443" - volumes: - - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile - - ./certs:/etc/caddy/certs - - caddy_data:/data - - caddy_config:/config - networks: - - myp-network - depends_on: - - frontend - extra_hosts: - - "host.docker.internal:host-gateway" - - "raspberrypi:192.168.0.105" - - "m040tbaraspi001.de040.corpintra.net:127.0.0.1" - environment: - - CADDY_HOST=m040tbaraspi001.de040.corpintra.net - - CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net - cap_add: - - NET_ADMIN - -networks: - myp-network: - driver: bridge - -volumes: - caddy_data: - caddy_config: - frontend_data: \ No newline at end of file diff --git a/frontend/docker-compose.yml b/frontend/docker-compose.yml index cc2e9db5..7ceb18ea 100644 --- a/frontend/docker-compose.yml +++ b/frontend/docker-compose.yml @@ -1,62 +1,49 @@ -version: '3' +version: '3.8' services: # Next.js Frontend - frontend: + frontend-app: build: context: . dockerfile: Dockerfile - container_name: myp-frontend - restart: unless-stopped + container_name: myp-frontend-app environment: - NODE_ENV=production - - NEXT_PUBLIC_API_URL=https://raspberrypi:443 - - NEXT_PUBLIC_BACKEND_HOST=raspberrypi:443 - - PORT=80 - volumes: - - ./certs:/app/certs - ports: - - "80" + - NEXT_PUBLIC_API_URL=https://raspberrypi + - HOSTNAME=m040tbaraspi001.de040.corpintra.net networks: - myp-network + restart: unless-stopped healthcheck: - test: ["CMD", "wget", "--spider", "http://localhost:80/health"] + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 - start_period: 40s # Caddy Proxy für SSL-Terminierung caddy: - image: caddy:2.7-alpine + image: caddy:2-alpine container_name: myp-caddy - restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile - - ./certs:/etc/caddy/certs + - ./certs:/etc/ssl/certs/myp - caddy_data:/data - caddy_config:/config networks: - myp-network depends_on: - - frontend - extra_hosts: - - "host.docker.internal:host-gateway" - - "raspberrypi:192.168.0.105" - - "m040tbaraspi001.de040.corpintra.net:127.0.0.1" + - frontend-app + restart: unless-stopped environment: - - CADDY_HOST=m040tbaraspi001.de040.corpintra.net - - CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net - cap_add: - - NET_ADMIN - -networks: - myp-network: - driver: bridge + - CADDY_INGRESS_NETWORKS=myp-network volumes: caddy_data: - caddy_config: \ No newline at end of file + caddy_config: + +networks: + myp-network: + driver: bridge \ No newline at end of file diff --git a/frontend/docker/caddy/Caddyfile b/frontend/docker/caddy/Caddyfile index 28841995..fa82e39c 100644 --- a/frontend/docker/caddy/Caddyfile +++ b/frontend/docker/caddy/Caddyfile @@ -1,76 +1,36 @@ -{ - debug - auto_https off - local_certs +# HTTP to HTTPS redirect +:80 { + redir https://{host}{uri} permanent } -# Produktionsumgebung - Frontend auf Port 80/443 mit selbstsigniertem Zertifikat -:80, :443 { - # TLS mit automatisch generierten selbstsignierten Zertifikaten - tls internal { - on_demand - } - - # API Anfragen zum Backend (Raspberry Pi) weiterleiten - @api { - path /api/* /health - } - handle @api { - uri strip_prefix /api - reverse_proxy raspberrypi:443 { - transport http { - tls - tls_insecure_skip_verify - } - header_up Host {upstream_hostport} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - } - } - - # Alle anderen Anfragen zum Frontend weiterleiten (auf Port 80 intern) - handle { - reverse_proxy frontend:80 { - header_up Host {upstream_hostport} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - } - } - - # OAuth Callbacks - @oauth path /auth/login/callback* - handle @oauth { - header Cache-Control "no-cache" - reverse_proxy frontend:80 - } +# HTTPS Frontend +m040tbaraspi001.de040.corpintra.net:443 { + # TLS configuration with custom certificates + tls /etc/ssl/certs/myp/frontend.crt /etc/ssl/certs/myp/frontend.key - # Produktions-Header + # Security headers header { + # Enable HSTS Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + # XSS Protection X-Content-Type-Options "nosniff" - X-Frame-Options "SAMEORIGIN" - Referrer-Policy "strict-origin-when-cross-origin" + X-Frame-Options "DENY" + X-XSS-Protection "1; mode=block" + # CSP + Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://raspberrypi;" + # Remove server header + -Server } -} - -# Spezifische Hostname-Konfiguration für Mercedes-Benz Werk 040 Berlin (falls benötigt) -m040tbaraspi001.de040.corpintra.net { - # TLS mit automatisch generierten selbstsignierten Zertifikaten - tls internal { - on_demand + + # Health check endpoint + handle /health { + respond "OK" 200 } - - # API Anfragen zum Backend (Raspberry Pi) weiterleiten - @api { - path /api/* /health - } - handle @api { - uri strip_prefix /api - reverse_proxy raspberrypi:443 { + + # API proxy to backend + handle /api/* { + reverse_proxy https://raspberrypi { transport http { - tls tls_insecure_skip_verify } header_up Host {upstream_hostport} @@ -79,85 +39,27 @@ m040tbaraspi001.de040.corpintra.net { header_up X-Forwarded-Proto {scheme} } } - - # Alle anderen Anfragen zum Frontend weiterleiten - handle { - reverse_proxy frontend:80 { - header_up Host {upstream_hostport} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - } - } - - # OAuth Callbacks - @oauth path /auth/login/callback* - handle @oauth { - header Cache-Control "no-cache" - reverse_proxy frontend:80 + + # Frontend application + reverse_proxy frontend-app:3000 { + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} } - # Produktions-Header - header { - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - X-Content-Type-Options "nosniff" - X-Frame-Options "SAMEORIGIN" - Referrer-Policy "strict-origin-when-cross-origin" + # Logging + log { + output file /var/log/caddy/access.log + format json } + + # Enable compression + encode gzip } -# Entwicklungsumgebung - Localhost und Raspberry Pi Backend (weiterhin für lokale Entwicklung verfügbar) -localhost, 127.0.0.1 { - # API Anfragen zum Raspberry Pi Backend weiterleiten - @api { - path /api/* /health - } - handle @api { - uri strip_prefix /api - reverse_proxy raspberrypi:443 { - transport http { - tls - tls_insecure_skip_verify - } - header_up Host {upstream_hostport} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - } - } - - # Alle anderen Anfragen zum Frontend weiterleiten - handle { - reverse_proxy myp-rp-dev:3000 { - header_up Host {upstream_hostport} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - } - } - - # TLS für lokale Entwicklung - tls /etc/caddy/ssl/frontend.crt /etc/caddy/ssl/frontend.key - - # OAuth Callbacks für Entwicklung - @oauth path /auth/login/callback* - handle @oauth { - header Cache-Control "no-cache" - reverse_proxy myp-rp-dev:3000 - } - - # Entwicklungsfreundliche Header - header { - # Weniger restriktive Sicherheitsheader für Entwicklung - X-Content-Type-Options "nosniff" - X-Frame-Options "SAMEORIGIN" - - # Keine Caches für Entwicklung - Cache-Control "no-store, no-cache, must-revalidate" - - # CORS für Entwicklung - Access-Control-Allow-Origin "*" - Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" - Access-Control-Allow-Headers "Content-Type, Authorization" - } -} \ No newline at end of file +# Fallback for direct IP access +192.168.0.109:443 { + tls /etc/ssl/certs/myp/frontend.crt /etc/ssl/certs/myp/frontend.key + redir https://m040tbaraspi001.de040.corpintra.net{uri} permanent +} \ No newline at end of file diff --git a/frontend/docker/compose.yml b/frontend/docker/compose.yml deleted file mode 100644 index 0c79dff6..00000000 --- a/frontend/docker/compose.yml +++ /dev/null @@ -1,30 +0,0 @@ -services: - caddy: - image: caddy:2.8 - container_name: caddy - restart: unless-stopped - ports: - - 80:80 - - 443:443 - volumes: - - ./caddy/data:/data - - ./caddy/config:/config - - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro - myp-rp: - image: myp-rp:latest - container_name: myp-rp - environment: - - NEXT_PUBLIC_API_URL=http://192.168.0.105:5000 - - OAUTH_CLIENT_ID=client_id - - OAUTH_CLIENT_SECRET=client_secret - env_file: "/srv/myp-env/github.env" - volumes: - - /srv/MYP-DB:/usr/src/app/db - restart: unless-stopped - # Füge Healthcheck hinzu für besseres Monitoring - healthcheck: - test: ["CMD", "wget", "--spider", "http://localhost:3000"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s diff --git a/frontend/repomix-output.txt b/frontend/repomix-output.txt deleted file mode 100644 index 68f0be14..00000000 --- a/frontend/repomix-output.txt +++ /dev/null @@ -1,9279 +0,0 @@ -This file is a merged representation of the entire codebase, combining all repository files into a single document. -Generated by Repomix on: 2024-12-09T06:29:51.427Z - -================================================================ -File Summary -================================================================ - -Purpose: --------- -This file contains a packed representation of the entire repository's contents. -It is designed to be easily consumable by AI systems for analysis, code review, -or other automated processes. - -File Format: ------------- -The content is organized as follows: -1. This summary section -2. Repository information -3. Repository structure -4. Multiple file entries, each consisting of: - a. A separator line (================) - b. The file path (File: path/to/file) - c. Another separator line - d. The full contents of the file - e. A blank line - -Usage Guidelines: ------------------ -- This file should be treated as read-only. Any changes should be made to the - original repository files, not this packed version. -- When processing this file, use the file path to distinguish - between different files in the repository. -- Be aware that this file may contain sensitive information. Handle it with - the same level of security as you would the original repository. - -Notes: ------- -- Some files may have been excluded based on .gitignore rules and Repomix's - configuration. -- Binary files are not included in this packed representation. Please refer to - the Repository Structure section for a complete list of file paths, including - binary files. - -Additional Info: ----------------- - -For more information about Repomix, visit: https://github.com/yamadashy/repomix - -================================================================ -Repository Structure -================================================================ -.dockerignore -.env.example -.gitignore -biome.json -components.json -docker/build.sh -docker/caddy/Caddyfile -docker/compose.yml -docker/deploy.sh -docker/images/.gitattributes -docker/save.sh -Dockerfile -drizzle.config.ts -drizzle/0000_overjoyed_strong_guy.sql -drizzle/meta/_journal.json -drizzle/meta/0000_snapshot.json -next.config.mjs -package.json -postcss.config.mjs -public/next.svg -public/vercel.svg -README.md -scripts/generate-data.js -src/app/admin/about/page.tsx -src/app/admin/admin-sidebar.tsx -src/app/admin/charts/printer-error-chart.tsx -src/app/admin/charts/printer-error-rate.tsx -src/app/admin/charts/printer-forecast.tsx -src/app/admin/charts/printer-utilization.tsx -src/app/admin/charts/printer-volume.tsx -src/app/admin/jobs/page.tsx -src/app/admin/layout.tsx -src/app/admin/page.tsx -src/app/admin/printers/columns.tsx -src/app/admin/printers/data-table.tsx -src/app/admin/printers/dialogs/create-printer.tsx -src/app/admin/printers/dialogs/delete-printer.tsx -src/app/admin/printers/dialogs/edit-printer.tsx -src/app/admin/printers/form.tsx -src/app/admin/printers/page.tsx -src/app/admin/settings/download/route.ts -src/app/admin/settings/page.tsx -src/app/admin/users/columns.tsx -src/app/admin/users/data-table.tsx -src/app/admin/users/dialog.tsx -src/app/admin/users/form.tsx -src/app/admin/users/page.tsx -src/app/api/job/[jobId]/remaining-time/route.ts -src/app/api/printers/route.ts -src/app/auth/login/callback/route.ts -src/app/auth/login/route.ts -src/app/globals.css -src/app/job/[jobId]/cancel-form.tsx -src/app/job/[jobId]/edit-comments.tsx -src/app/job/[jobId]/extend-form.tsx -src/app/job/[jobId]/finish-form.tsx -src/app/job/[jobId]/page.tsx -src/app/layout.tsx -src/app/my/jobs/columns.tsx -src/app/my/jobs/data-table.tsx -src/app/my/profile/page.tsx -src/app/not-found.tsx -src/app/page.tsx -src/app/printer/[printerId]/reserve/form.tsx -src/app/printer/[printerId]/reserve/page.tsx -src/components/data-card.tsx -src/components/dynamic-printer-cards.tsx -src/components/header/index.tsx -src/components/header/navigation.tsx -src/components/login-button.tsx -src/components/logout-button.tsx -src/components/personalized-cards.tsx -src/components/printer-availability-badge.tsx -src/components/printer-card/countdown.tsx -src/components/printer-card/index.tsx -src/components/ui/alert-dialog.tsx -src/components/ui/alert.tsx -src/components/ui/avatar.tsx -src/components/ui/badge.tsx -src/components/ui/breadcrumb.tsx -src/components/ui/button.tsx -src/components/ui/card.tsx -src/components/ui/chart.tsx -src/components/ui/dialog.tsx -src/components/ui/dropdown-menu.tsx -src/components/ui/form.tsx -src/components/ui/hover-card.tsx -src/components/ui/input.tsx -src/components/ui/label.tsx -src/components/ui/scroll-area.tsx -src/components/ui/select.tsx -src/components/ui/skeleton.tsx -src/components/ui/sonner.tsx -src/components/ui/table.tsx -src/components/ui/tabs.tsx -src/components/ui/textarea.tsx -src/components/ui/toast.tsx -src/components/ui/toaster.tsx -src/components/ui/use-toast.ts -src/server/actions/authentication/logout.ts -src/server/actions/printers.ts -src/server/actions/printJobs.ts -src/server/actions/timer.ts -src/server/actions/user/delete.ts -src/server/actions/user/update.ts -src/server/actions/users.ts -src/server/auth/index.ts -src/server/auth/oauth.ts -src/server/auth/permissions.ts -src/utils/analytics/error-rate.ts -src/utils/analytics/errors.ts -src/utils/analytics/forecast.ts -src/utils/analytics/utilization.ts -src/utils/analytics/volume.ts -src/utils/drizzle.ts -src/utils/errors.ts -src/utils/fetch.ts -src/utils/guard.ts -src/utils/printers.ts -src/utils/strings.ts -src/utils/styles.ts -tailwind.config.ts -tsconfig.json - -================================================================ -Repository Files -================================================================ - -================ -File: .dockerignore -================ -# Build and utility assets -docker/ -scripts/ - -# Ignore node_modules as they will be installed in the container -node_modules - -# Ignore build artifacts -.next - -# Ignore runtime data -db/ - -# Ignore local configuration files -.env -.env.example - -# Ignore version control files -.git -.gitignore - -# Ignore IDE/editor specific files -*.log -*.tmp -*.DS_Store -.vscode/ -.idea/ - -================ -File: .env.example -================ -# OAuth Configuration -OAUTH_CLIENT_ID=client_id -OAUTH_CLIENT_SECRET=client_secret - -================ -File: .gitignore -================ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# db folder -db/ - -# Env file -.env - - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -================ -File: biome.json -================ -{ - "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json", - "organizeImports": { - "enabled": true - }, - "formatter": { - "enabled": true, - "lineWidth": 120 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "correctness": { - "noUnusedImports": "error" - } - } - } -} - -================ -File: components.json -================ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/utils/styles" - } -} - -================ -File: docker/build.sh -================ -#!/bin/bash - -# Define image name -MYP_RP_IMAGE_NAME="myp-rp" - -# Function to build Docker image -build_image() { - local image_name=$1 - local dockerfile=$2 - local platform=$3 - - echo "Building $image_name Docker image for $platform..." - - docker buildx build --platform $platform -t ${image_name}:latest -f $dockerfile --load . - if [ $? -eq 0 ]; then - echo "$image_name Docker image built successfully" - else - echo "Error occurred while building $image_name Docker image" - exit 1 - fi -} - -# Create and use a builder instance (if not already created) -BUILDER_NAME="myp-rp-arm64-builder" -docker buildx create --name $BUILDER_NAME --use || docker buildx use $BUILDER_NAME - -# Build myp-rp image -build_image "$MYP_RP_IMAGE_NAME" "$PWD/Dockerfile" "linux/arm64" - -# Remove the builder instance -docker buildx rm $BUILDER_NAME - -================ -File: docker/caddy/Caddyfile -================ -{ - debug -} - -m040tbaraspi001.de040.corpintra.net, m040tbaraspi001.de040.corpinter.net { - reverse_proxy myp-rp:3000 - tls internal -} - -================ -File: docker/compose.yml -================ -services: - caddy: - image: caddy:2.8 - container_name: caddy - restart: unless-stopped - ports: - - 80:80 - - 443:443 - volumes: - - ./caddy/data:/data - - ./caddy/config:/config - - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro - myp-rp: - image: myp-rp:latest - container_name: myp-rp - env_file: "/srv/myp-env/github.env" - volumes: - - /srv/MYP-DB:/usr/src/app/db - restart: unless-stopped - -================ -File: docker/deploy.sh -================ -#!/bin/bash - -# Directory containing the Docker images -IMAGE_DIR="docker/images" - -# Load all Docker images from the tar.xz files in the IMAGE_DIR -echo "Loading Docker images from $IMAGE_DIR..." - -for image_file in "$IMAGE_DIR"/*.tar.xz; do - if [ -f "$image_file" ]; then - echo "Loading Docker image from $image_file..." - docker load -i "$image_file" - - # Check if the image loading was successful - if [ $? -ne 0 ]; then - echo "Error occurred while loading Docker image from $image_file" - exit 1 - fi - else - echo "No Docker image tar.xz files found in $IMAGE_DIR." - fi -done - -# Execute docker compose -echo "Running docker compose..." -docker compose -f "docker/compose.yml" up -d - -# Check if the operation was successful -if [ $? -eq 0 ]; then - echo "Docker compose executed successfully" -else - echo "Error occurred while executing docker compose" - exit 1 -fi - -echo "Deployment completed successfully" - -================ -File: docker/images/.gitattributes -================ -caddy_2.8.tar.xz filter=lfs diff=lfs merge=lfs -text -myp-rp_latest.tar.xz filter=lfs diff=lfs merge=lfs -text - -================ -File: docker/save.sh -================ -#!/bin/bash - -# Get image name as argument -IMAGE_NAME=$1 -PLATFORM="linux/arm64" - -# Define paths -IMAGE_DIR="docker/images" -IMAGE_FILE="${IMAGE_DIR}/${IMAGE_NAME//[:\/]/_}.tar" -COMPRESSED_FILE="${IMAGE_FILE}.xz" - -# Function to pull the image -pull_image() { - local image=$1 - if [[ $image == arm64v8/* ]]; then - echo "Pulling image $image without platform specification..." - docker pull $image - else - echo "Pulling image $image for platform $PLATFORM..." - docker pull --platform $PLATFORM $image - fi - return $? -} - -# Pull the image if it is not available locally -if ! docker image inspect ${IMAGE_NAME} &>/dev/null; then - if pull_image ${IMAGE_NAME}; then - echo "Image $IMAGE_NAME pulled successfully." - else - echo "Error occurred while pulling $IMAGE_NAME for platform $PLATFORM" - echo "Trying to pull $IMAGE_NAME without platform specification..." - - # Attempt to pull again without platform - if pull_image ${IMAGE_NAME}; then - echo "Image $IMAGE_NAME pulled successfully without platform." - else - echo "Error occurred while pulling $IMAGE_NAME without platform." - echo "Trying to pull arm64v8/${IMAGE_NAME} instead..." - - # Construct new image name - NEW_IMAGE_NAME="arm64v8/${IMAGE_NAME}" - if pull_image ${NEW_IMAGE_NAME}; then - echo "Image $NEW_IMAGE_NAME pulled successfully." - IMAGE_NAME=${NEW_IMAGE_NAME} # Update IMAGE_NAME to use the new one - else - echo "Error occurred while pulling $NEW_IMAGE_NAME" - exit 1 - fi - fi - fi -else - echo "Image $IMAGE_NAME found locally. Skipping pull." -fi - -# Save the Docker image -echo "Saving $IMAGE_NAME Docker image..." -docker save ${IMAGE_NAME} > $IMAGE_FILE - -# Compress the Docker image (overwriting if file exists) -echo "Compressing $IMAGE_FILE..." -xz -z --force $IMAGE_FILE - -if [ $? -eq 0 ]; then - echo "$IMAGE_NAME Docker image saved and compressed successfully as $COMPRESSED_FILE" -else - echo "Error occurred while compressing $IMAGE_NAME Docker image" - exit 1 -fi - -================ -File: Dockerfile -================ -FROM node:20-bookworm-slim - -# Create application directory -RUN mkdir -p /usr/src/app - -# Set environment variables -ENV PORT=3000 -ENV NEXT_TELEMETRY_DISABLED=1 - -WORKDIR /usr/src/app - -# Copy package.json and pnpm-lock.yaml -COPY package.json /usr/src/app -COPY pnpm-lock.yaml /usr/src/app - -# Install pnpm -RUN corepack enable pnpm - -# Install dependencies -RUN pnpm install - -# Copy the rest of the application code -COPY . /usr/src/app - -# Initialize Database, if it not already exists -RUN pnpm run db - -# Build the application -RUN pnpm run build - -EXPOSE 3000 - -# Start the application -CMD ["/bin/sh", "-c", "if [ ! -f ./db/sqlite.db ]; then pnpm db; fi && pnpm start"] - -================ -File: drizzle.config.ts -================ -import { defineConfig } from "drizzle-kit"; - -//@ts-ignore - better-sqlite driver throws an error even though its an valid value -export default defineConfig({ - dialect: "sqlite", - schema: "./src/server/db/schema.ts", - out: "./drizzle", - driver: "libsql", - dbCredentials: { - url: "file:./db/sqlite.db", - }, -}); - -================ -File: drizzle/0000_overjoyed_strong_guy.sql -================ -CREATE TABLE `printJob` ( - `id` text PRIMARY KEY NOT NULL, - `printerId` text NOT NULL, - `userId` text NOT NULL, - `startAt` integer NOT NULL, - `durationInMinutes` integer NOT NULL, - `comments` text, - `aborted` integer DEFAULT false NOT NULL, - `abortReason` text, - FOREIGN KEY (`printerId`) REFERENCES `printer`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `printer` ( - `id` text PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `description` text NOT NULL, - `status` integer DEFAULT 0 NOT NULL -); ---> statement-breakpoint -CREATE TABLE `session` ( - `id` text PRIMARY KEY NOT NULL, - `user_id` text NOT NULL, - `expires_at` integer NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `user` ( - `id` text PRIMARY KEY NOT NULL, - `github_id` integer NOT NULL, - `name` text, - `displayName` text, - `email` text NOT NULL, - `role` text DEFAULT 'guest' -); - -================ -File: drizzle/meta/_journal.json -================ -{ - "version": "6", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1715416514336, - "tag": "0000_overjoyed_strong_guy", - "breakpoints": true - } - ] -} - -================ -File: drizzle/meta/0000_snapshot.json -================ -{ - "version": "6", - "dialect": "sqlite", - "id": "791dc197-5254-4432-bd9f-1368d1a5aa6a", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "printJob": { - "name": "printJob", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "printerId": { - "name": "printerId", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "userId": { - "name": "userId", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "startAt": { - "name": "startAt", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "durationInMinutes": { - "name": "durationInMinutes", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "comments": { - "name": "comments", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "aborted": { - "name": "aborted", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "abortReason": { - "name": "abortReason", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "printJob_printerId_printer_id_fk": { - "name": "printJob_printerId_printer_id_fk", - "tableFrom": "printJob", - "tableTo": "printer", - "columnsFrom": [ - "printerId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "printJob_userId_user_id_fk": { - "name": "printJob_userId_user_id_fk", - "tableFrom": "printJob", - "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "printer": { - "name": "printer", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "session": { - "name": "session", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "session_user_id_user_id_fk": { - "name": "session_user_id_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "user": { - "name": "user", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "github_id": { - "name": "github_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "displayName": { - "name": "displayName", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'guest'" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } - }, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - } -} - -================ -File: next.config.mjs -================ -/** @type {import('next').NextConfig} */ -const nextConfig = { - async headers() { - return [ - { - source: "/:path*", - headers: [ - { - key: "Access-Control-Allow-Origin", - value: "m040tbaraspi001.de040.corpintra.net", - }, - { - key: "Access-Control-Allow-Methods", - value: "GET, POST, PUT, DELETE, OPTIONS", - }, - { - key: "Access-Control-Allow-Headers", - value: "Content-Type, Authorization", - }, - ], - }, - ]; - }, -}; - -export default nextConfig; - -================ -File: package.json -================ -{ - "name": "myp-rp", - "version": "1.0.0", - "private": true, - "packageManager": "pnpm@9.12.1", - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "db:create-default": "mkdir -p db/", - "db:generate-sqlite": "pnpm drizzle-kit generate", - "db:clean": "rm -rf db/ drizzle/", - "db:migrate": "pnpm drizzle-kit migrate", - "db": "pnpm db:create-default && pnpm db:generate-sqlite && pnpm db:migrate", - "db:reset": "pnpm db:clean && pnpm db" - }, - "dependencies": { - "@faker-js/faker": "^9.2.0", - "@headlessui/react": "^2.1.10", - "@headlessui/tailwindcss": "^0.2.1", - "@hookform/resolvers": "^3.9.0", - "@libsql/client": "^0.14.0", - "@lucia-auth/adapter-drizzle": "^1.1.0", - "@radix-ui/react-alert-dialog": "^1.1.2", - "@radix-ui/react-avatar": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.2", - "@radix-ui/react-dropdown-menu": "^2.1.2", - "@radix-ui/react-hover-card": "^1.1.2", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-scroll-area": "^1.2.0", - "@radix-ui/react-select": "^2.1.2", - "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-tabs": "^1.1.1", - "@radix-ui/react-toast": "^1.2.2", - "@remixicon/react": "^4.3.0", - "@tanstack/react-table": "^8.20.5", - "@tremor/react": "^3.18.3", - "arctic": "^1.9.2", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", - "date-fns": "^4.1.0", - "drizzle-orm": "^0.30.10", - "lodash": "^4.17.21", - "lucia": "^3.2.1", - "lucide-react": "^0.378.0", - "luxon": "^3.5.0", - "next": "14.2.3", - "next-themes": "^0.3.0", - "oslo": "^1.2.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-hook-form": "^7.53.0", - "react-if": "^4.1.5", - "react-timer-hook": "^3.0.7", - "recharts": "^2.13.3", - "regression": "^2.0.1", - "sonner": "^1.5.0", - "sqlite": "^5.1.1", - "sqlite3": "^5.1.7", - "swr": "^2.2.5", - "tailwind-merge": "^2.5.3", - "tailwindcss-animate": "^1.0.7", - "use-debounce": "^10.0.3", - "uuid": "^11.0.2", - "zod": "^3.23.8" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.3", - "@tailwindcss/forms": "^0.5.9", - "@types/lodash": "^4.17.13", - "@types/luxon": "^3.4.2", - "@types/node": "^20.16.11", - "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.1", - "drizzle-kit": "^0.21.4", - "postcss": "^8.4.47", - "tailwindcss": "^3.4.13", - "ts-node": "^10.9.2", - "typescript": "^5.6.3" - } -} - -================ -File: postcss.config.mjs -================ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -export default config; - -================ -File: public/next.svg -================ - - -================ -File: public/vercel.svg -================ - - -================ -File: README.md -================ -# MYP - Manage Your Printer - -MYP (Manage Your Printer) ist eine Webanwendung zur Reservierung von 3D-Druckern. -Sie wurde im Rahmen des Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt. - -## Deployment - -### Voraussetzungen - -- Netzwerk auf Raspberry Pi ist eingerichtet -- Docker ist installiert - -### Schritte - -1. Docker-Container bauen (docker/build.sh) -2. Docker-Container speichern (docker/save.sh caddy:2.8 myp-rp:latest) -3. Docker-Container auf Raspberry Pi bereitstellen (docker/deploy.sh) - -## Entwicklerinformationen - -### Raspberry Pi Einstellungen - -Auf dem Raspberry Pi wurde Raspbian Lite installiert. -Unter /srv/* sind die Projektdateien zu finden. - -### Anmeldedaten - -``` -Benutzer: myp -Passwort: (persönlich bekannt) -``` - -================ -File: scripts/generate-data.js -================ -const sqlite3 = require("sqlite3"); -const faker = require("@faker-js/faker").faker; -const { random, sample, sampleSize, sum } = require("lodash"); -const { DateTime } = require("luxon"); -const { open } = require("sqlite"); -const { v4: uuidv4 } = require("uuid"); - -const dbPath = "./db/sqlite.db"; - -// Configuration for test data generation -let startDate = DateTime.fromISO("2024-10-08"); -let endDate = DateTime.fromISO("2024-11-08"); -let numberOfPrinters = 5; - -// Use weekday names for better readability and ease of setting trends -let avgPrintTimesPerDay = { - Monday: 4, - Tuesday: 2, - Wednesday: 5, - Thursday: 2, - Friday: 3, - Saturday: 0, - Sunday: 0, -}; // Average number of prints for each weekday - -let avgPrintDurationPerDay = { - Monday: 240, // Total average duration in minutes for Monday - Tuesday: 30, - Wednesday: 45, - Thursday: 40, - Friday: 120, - Saturday: 0, - Sunday: 0, -}; // Average total duration of prints for each weekday - -let printerUsage = { - "Drucker 1": 0.5, - "Drucker 2": 0.7, - "Drucker 3": 0.6, - "Drucker 4": 0.3, - "Drucker 5": 0.4, -}; // Usage percentages for each printer - -// **New Configurations for Error Rates** -let generalErrorRate = 0.05; // 5% chance any print job may fail -let printerErrorRates = { - "Drucker 1": 0.02, // 2% error rate for Printer 1 - "Drucker 2": 0.03, - "Drucker 3": 0.01, - "Drucker 4": 0.05, - "Drucker 5": 0.04, -}; // Error rates for each printer - -const holidays = []; // Example holidays -const existingJobs = []; - -const initDB = async () => { - console.log("Initializing database connection..."); - return open({ - filename: dbPath, - driver: sqlite3.Database, - }); -}; - -const createUser = (isPowerUser = false) => { - const name = [faker.person.firstName(), faker.person.lastName()]; - - const user = { - id: uuidv4(), - github_id: faker.number.int(), - username: `${name[0].slice(0, 2)}${name[1].slice(0, 6)}`.toUpperCase(), - displayName: `${name[0]} ${name[1]}`.toUpperCase(), - email: `${name[0]}.${name[1]}@example.com`, - role: sample(["user", "admin"]), - isPowerUser, - }; - console.log("Created user:", user); - return user; -}; - -const createPrinter = (index) => { - const printer = { - id: uuidv4(), - name: `Drucker ${index}`, - description: faker.lorem.sentence(), - status: random(0, 2), - }; - console.log("Created printer:", printer); - return printer; -}; - -const isPrinterAvailable = (printer, startAt, duration) => { - const endAt = startAt + duration * 60 * 1000; // Convert minutes to milliseconds - return !existingJobs.some((job) => { - const jobStart = job.startAt; - const jobEnd = job.startAt + job.durationInMinutes * 60 * 1000; - return ( - printer.id === job.printerId && - ((startAt >= jobStart && startAt < jobEnd) || - (endAt > jobStart && endAt <= jobEnd) || - (startAt <= jobStart && endAt >= jobEnd)) - ); - }); -}; - -const createPrintJob = (users, printers, startAt, duration) => { - const user = sample(users); - let printer; - - // Weighted selection based on printer usage - const printerNames = Object.keys(printerUsage); - const weightedPrinters = printers.filter((p) => printerNames.includes(p.name)); - - // Create a weighted array of printers based on usage percentages - const printerWeights = weightedPrinters.map((p) => ({ - printer: p, - weight: printerUsage[p.name], - })); - - const totalWeight = sum(printerWeights.map((pw) => pw.weight)); - const randomWeight = Math.random() * totalWeight; - let accumulatedWeight = 0; - for (const pw of printerWeights) { - accumulatedWeight += pw.weight; - if (randomWeight <= accumulatedWeight) { - printer = pw.printer; - break; - } - } - - if (!printer) { - printer = sample(printers); - } - - if (!isPrinterAvailable(printer, startAt, duration)) { - console.log("Printer not available, skipping job creation."); - return null; - } - - // **Determine if the job should be aborted based on error rates** - let aborted = false; - let abortReason = null; - - // Calculate the combined error rate - const printerErrorRate = printerErrorRates[printer.name] || 0; - const combinedErrorRate = 1 - (1 - generalErrorRate) * (1 - printerErrorRate); - - if (Math.random() < combinedErrorRate) { - aborted = true; - const errorMessages = [ - "Unbekannt", - "Keine Ahnung", - "Falsch gebucht", - "Filament gelöst", - "Druckabbruch", - "Düsenverstopfung", - "Schichthaftung fehlgeschlagen", - "Materialmangel", - "Dateifehler", - "Temperaturproblem", - "Mechanischer Fehler", - "Softwarefehler", - "Kalibrierungsfehler", - "Überhitzung", - ]; - abortReason = sample(errorMessages); // Generate a random abort reason - } - - const printJob = { - id: uuidv4(), - printerId: printer.id, - userId: user.id, - startAt, - durationInMinutes: duration, - comments: faker.lorem.sentence(), - aborted, - abortReason, - }; - console.log("Created print job:", printJob); - return printJob; -}; - -const generatePrintJobsForDay = async (users, printers, dayDate, totalJobsForDay, totalDurationForDay, db, dryRun) => { - console.log(`Generating print jobs for ${dayDate.toISODate()}...`); - - // Generate random durations that sum up approximately to totalDurationForDay - const durations = []; - let remainingDuration = totalDurationForDay; - for (let i = 0; i < totalJobsForDay; i++) { - const avgJobDuration = remainingDuration / (totalJobsForDay - i); - const jobDuration = Math.max( - Math.round(random(avgJobDuration * 0.8, avgJobDuration * 1.2)), - 5, // Minimum duration of 5 minutes - ); - durations.push(jobDuration); - remainingDuration -= jobDuration; - } - - // Shuffle durations to randomize job lengths - const shuffledDurations = sampleSize(durations, durations.length); - - for (let i = 0; i < totalJobsForDay; i++) { - const duration = shuffledDurations[i]; - - // Random start time between 8 AM and 6 PM, adjusted to avoid overlapping durations - const possibleStartHours = Array.from({ length: 10 }, (_, idx) => idx + 8); // 8 AM to 6 PM - let startAt; - let attempts = 0; - do { - const hour = sample(possibleStartHours); - const minute = random(0, 59); - startAt = dayDate.set({ hour, minute, second: 0, millisecond: 0 }).toMillis(); - attempts++; - if (attempts > 10) { - console.log("Unable to find available time slot, skipping job."); - break; - } - } while (!isPrinterAvailable(sample(printers), startAt, duration)); - - if (attempts > 10) continue; - - const printJob = createPrintJob(users, printers, startAt, duration); - if (printJob) { - if (!dryRun) { - await db.run( - `INSERT INTO printJob (id, printerId, userId, startAt, durationInMinutes, comments, aborted, abortReason) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [ - printJob.id, - printJob.printerId, - printJob.userId, - printJob.startAt, - printJob.durationInMinutes, - printJob.comments, - printJob.aborted ? 1 : 0, - printJob.abortReason, - ], - ); - } - existingJobs.push(printJob); - console.log("Inserted print job into database:", printJob.id); - } - } -}; - -const generateTestData = async (dryRun = false) => { - console.log("Starting test data generation..."); - const db = await initDB(); - - // Generate users and printers - const users = [ - ...Array.from({ length: 7 }, () => createUser(false)), - ...Array.from({ length: 3 }, () => createUser(true)), - ]; - const printers = Array.from({ length: numberOfPrinters }, (_, index) => createPrinter(index + 1)); - - if (!dryRun) { - // Insert users into the database - for (const user of users) { - await db.run( - `INSERT INTO user (id, github_id, name, displayName, email, role) - VALUES (?, ?, ?, ?, ?, ?)`, - [user.id, user.github_id, user.username, user.displayName, user.email, user.role], - ); - console.log("Inserted user into database:", user.id); - } - - // Insert printers into the database - for (const printer of printers) { - await db.run( - `INSERT INTO printer (id, name, description, status) - VALUES (?, ?, ?, ?)`, - [printer.id, printer.name, printer.description, printer.status], - ); - console.log("Inserted printer into database:", printer.id); - } - } - - // Generate print jobs for each day within the specified date range - let currentDay = startDate; - while (currentDay <= endDate) { - const weekdayName = currentDay.toFormat("EEEE"); // Get weekday name (e.g., 'Monday') - if (holidays.includes(currentDay.toISODate()) || avgPrintTimesPerDay[weekdayName] === 0) { - console.log(`Skipping holiday or no jobs scheduled: ${currentDay.toISODate()}`); - currentDay = currentDay.plus({ days: 1 }); - continue; - } - - const totalJobsForDay = avgPrintTimesPerDay[weekdayName]; - const totalDurationForDay = avgPrintDurationPerDay[weekdayName]; - - await generatePrintJobsForDay(users, printers, currentDay, totalJobsForDay, totalDurationForDay, db, dryRun); - currentDay = currentDay.plus({ days: 1 }); - } - - if (!dryRun) { - await db.close(); - console.log("Database connection closed. Test data generation complete."); - } else { - console.log("Dry run complete. No data was written to the database."); - } -}; - -const setConfigurations = (config) => { - if (config.startDate) startDate = DateTime.fromISO(config.startDate); - if (config.endDate) endDate = DateTime.fromISO(config.endDate); - if (config.numberOfPrinters) numberOfPrinters = config.numberOfPrinters; - if (config.avgPrintTimesPerDay) avgPrintTimesPerDay = config.avgPrintTimesPerDay; - if (config.avgPrintDurationPerDay) avgPrintDurationPerDay = config.avgPrintDurationPerDay; - if (config.printerUsage) printerUsage = config.printerUsage; - if (config.generalErrorRate !== undefined) generalErrorRate = config.generalErrorRate; - if (config.printerErrorRates) printerErrorRates = config.printerErrorRates; -}; - -// Example usage -setConfigurations({ - startDate: "2024-10-08", - endDate: "2024-11-08", - numberOfPrinters: 6, - avgPrintTimesPerDay: { - Monday: 4, // High usage - Tuesday: 2, // Low usage - Wednesday: 3, // Low usage - Thursday: 2, // Low usage - Friday: 8, // High usage - Saturday: 0, - Sunday: 0, - }, - avgPrintDurationPerDay: { - Monday: 300, // High total duration - Tuesday: 60, // Low total duration - Wednesday: 90, - Thursday: 60, - Friday: 240, - Saturday: 0, - Sunday: 0, - }, - printerUsage: { - "Drucker 1": 2.3, - "Drucker 2": 1.7, - "Drucker 3": 0.1, - "Drucker 4": 1.5, - "Drucker 5": 2.4, - "Drucker 6": 0.3, - "Drucker 7": 0.9, - "Drucker 8": 0.1, - }, - generalErrorRate: 0.05, // 5% general error rate - printerErrorRates: { - "Drucker 1": 0.02, - "Drucker 2": 0.03, - "Drucker 3": 0.1, - "Drucker 4": 0.05, - "Drucker 5": 0.04, - "Drucker 6": 0.02, - "Drucker 7": 0.01, - "PrinteDrucker 8": 0.03, - }, -}); - -generateTestData(process.argv.includes("--dry-run")) - .then(() => { - console.log("Test data generation script finished."); - }) - .catch((err) => { - console.error("Error generating test data:", err); - }); - -================ -File: src/app/admin/about/page.tsx -================ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Über MYP", -}; - -export default async function AdminPage() { - return ( - - - Über MYP - - MYP — Manage Your Printer - - - -

- MYP ist eine Webanwendung zur Reservierung von 3D-Druckern. Sie wurde im Rahmen des - Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische - Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt. -

-

- © 2024{" "} - - Torben Haack - -

-
-
- ); -} - -================ -File: src/app/admin/admin-sidebar.tsx -================ -"use client"; - -import { cn } from "@/utils/styles"; -import { FileIcon, HeartIcon, LayoutDashboardIcon, PrinterIcon, UsersIcon, WrenchIcon } from "lucide-react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; - -interface AdminSite { - name: string; - path: string; - icon: React.ReactNode; -} - -export function AdminSidebar() { - const pathname = usePathname(); - const adminSites: AdminSite[] = [ - { - name: "Dashboard", - path: "/admin", - icon: , - }, - { - name: "Benutzer", - path: "/admin/users", - icon: , - }, - { - name: "Drucker", - path: "/admin/printers", - icon: , - }, - { - name: "Druckaufträge", - path: "/admin/jobs", - icon: , - }, - { - name: "Einstellungen", - path: "/admin/settings", - icon: , - }, - { - name: "Über MYP", - path: "/admin/about", - icon: , - }, - ]; - - return ( -
    - {adminSites.map((site) => ( -
  • - - {site.icon} - {site.name} - -
  • - ))} -
- ); -} - -================ -File: src/app/admin/charts/printer-error-chart.tsx -================ -"use client"; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; -import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts"; - -export const description = "Ein Säulendiagramm zur Darstellung der Abbruchgründe und ihrer Häufigkeit"; - -interface AbortReasonCountChartProps { - abortReasonCount: { - abortReason: string; - count: number; - }[]; -} - -const chartConfig = { - abortReason: { - label: "Abbruchgrund", - }, -} satisfies ChartConfig; - -export function AbortReasonCountChart({ abortReasonCount }: AbortReasonCountChartProps) { - // Transform data to fit the chart structure - const chartData = abortReasonCount.map((reason) => ({ - abortReason: reason.abortReason, - count: reason.count, - })); - - return ( - - - Abbruchgründe - Häufigkeit der Abbruchgründe für Druckaufträge - - - - - - value} - /> - `${value}`} /> - } /> - - `${value}`} - /> - - - - - - ); -} - -================ -File: src/app/admin/charts/printer-error-rate.tsx -================ -"use client"; -import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts"; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; -import type { PrinterErrorRate } from "@/utils/analytics/error-rate"; - -export const description = "Ein Säulendiagramm zur Darstellung der Fehlerrate"; - -interface PrinterErrorRateChartProps { - printerErrorRate: PrinterErrorRate[]; -} - -const chartConfig = { - errorRate: { - label: "Fehlerrate", - }, -} satisfies ChartConfig; - -export function PrinterErrorRateChart({ printerErrorRate }: PrinterErrorRateChartProps) { - // Transform data to fit the chart structure - const chartData = printerErrorRate.map((printer) => ({ - printer: printer.name, - errorRate: printer.errorRate, - })); - - return ( - - - Fehlerrate - Fehlerrate der Drucker in Prozent - - - - - - value} - /> - `${value}%`} /> - } /> - - `${value}%`} - /> - - - - - - ); -} - -================ -File: src/app/admin/charts/printer-forecast.tsx -================ -"use client"; - -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; - -export const description = "Ein Bereichsdiagramm zur Darstellung der prognostizierten Nutzung pro Wochentag"; - -interface ForecastData { - day: number; // 0 for Sunday, 1 for Monday, ..., 6 for Saturday - usageMinutes: number; -} - -interface ForecastChartProps { - forecastData: ForecastData[]; -} - -const chartConfig = { - usage: { - label: "Prognostizierte Nutzung", - color: "hsl(var(--chart-1))", - }, -} satisfies ChartConfig; - -const daysOfWeek = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]; - -export function ForecastPrinterUsageChart({ forecastData }: ForecastChartProps) { - // Transform and slice data to fit the chart structure - const chartData = forecastData.map((data) => ({ - //slice(1, forecastData.length - 1). - day: daysOfWeek[data.day], // Map day number to weekday name - usage: data.usageMinutes, - })); - - return ( - - - Prognostizierte Nutzung pro Wochentag - - - - - - - - } /> - - - - - -
- Zeigt die prognostizierte Nutzungszeit pro Wochentag in Minuten. -
-
- Besten Tage zur Wartung: {bestMaintenanceDays(forecastData)} -
-
-
- ); -} - -function bestMaintenanceDays(forecastData: ForecastData[]) { - const sortedData = forecastData.map((a) => a).sort((a, b) => a.usageMinutes - b.usageMinutes); // Sort ascending - - const q1Index = Math.floor(sortedData.length * 0.33); - const q1 = sortedData[q1Index].usageMinutes; // First quartile (Q1) value - - const filteredData = sortedData.filter((data) => data.usageMinutes <= q1); - - return filteredData - .map((data) => { - const days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]; - return days[data.day]; - }) - .join(", "); -} - -================ -File: src/app/admin/charts/printer-utilization.tsx -================ -"use client"; - -import { TrendingUp } from "lucide-react"; -import * as React from "react"; -import { Label, Pie, PieChart } from "recharts"; - -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; - -export const description = "Nutzung des Druckers"; - -interface ComponentProps { - data: { - printerId: string; - utilizationPercentage: number; - name: string; - }; -} - -const chartConfig = {} satisfies ChartConfig; - -export function PrinterUtilizationChart({ data }: ComponentProps) { - const totalUtilization = React.useMemo(() => data.utilizationPercentage, [data]); - const dataWithColor = { - ...data, - fill: "rgb(34 197 94)", - }; - const free = { - printerId: "-", - utilizationPercentage: 1 - data.utilizationPercentage, - name: "(Frei)", - fill: "rgb(212 212 212)", - }; - - return ( - - - {data.name} - Nutzung des ausgewählten Druckers - - - - - } /> - - - - - - -
- Übersicht der Nutzung -
-
Aktuelle Auslastung des Druckers
-
-
- ); -} - -================ -File: src/app/admin/charts/printer-volume.tsx -================ -"use client"; -import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from "recharts"; - -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; - -export const description = "Ein Balkendiagramm mit Beschriftung"; - -interface PrintVolumes { - today: number; - thisWeek: number; - thisMonth: number; -} - -const chartConfig = { - volume: { - label: "Volumen", - }, -} satisfies ChartConfig; - -interface PrinterVolumeChartProps { - printerVolume: PrintVolumes; -} - -export function PrinterVolumeChart({ printerVolume }: PrinterVolumeChartProps) { - const chartData = [ - { period: "Heute", volume: printerVolume.today, color: "hsl(var(--chart-1))" }, - { period: "Diese Woche", volume: printerVolume.thisWeek, color: "hsl(var(--chart-2))" }, - { period: "Diesen Monat", volume: printerVolume.thisMonth, color: "hsl(var(--chart-3))" }, - ]; - - return ( - - - Druckvolumen - Vergleich: Heute, Diese Woche, Diesen Monat - - - - - - value} - /> - } /> - - - - - - - -
- Zeigt das Druckvolumen für heute, diese Woche und diesen Monat -
-
-
- ); -} - -================ -File: src/app/admin/jobs/page.tsx -================ -import { columns } from "@/app/my/jobs/columns"; -import { JobsTable } from "@/app/my/jobs/data-table"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { db } from "@/server/db"; -import { printJobs } from "@/server/db/schema"; -import { desc } from "drizzle-orm"; -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Alle Druckaufträge", -}; - -export default async function AdminJobsPage() { - const allJobs = await db.query.printJobs.findMany({ - orderBy: [desc(printJobs.startAt)], - with: { - user: true, - printer: true, - }, - }); - - return ( - - -
- Druckaufträge - Alle Druckaufträge -
-
- - - -
- ); -} - -================ -File: src/app/admin/layout.tsx -================ -import { AdminSidebar } from "@/app/admin/admin-sidebar"; -import { validateRequest } from "@/server/auth"; -import { UserRole } from "@/server/auth/permissions"; -import { IS_NOT, guard } from "@/utils/guard"; -import { redirect } from "next/navigation"; - -interface AdminLayoutProps { - children: React.ReactNode; -} - -export const dynamic = "force-dynamic"; - -export default async function AdminLayout(props: AdminLayoutProps) { - const { children } = props; - const { user } = await validateRequest(); - - if (guard(user, IS_NOT, UserRole.ADMIN)) { - redirect("/"); - } - - return ( -
-
-

Admin

-
-
- -
{children}
-
-
- ); -} - -================ -File: src/app/admin/page.tsx -================ -import { AbortReasonCountChart } from "@/app/admin/charts/printer-error-chart"; -import { PrinterErrorRateChart } from "@/app/admin/charts/printer-error-rate"; -import { ForecastPrinterUsageChart } from "@/app/admin/charts/printer-forecast"; -import { PrinterUtilizationChart } from "@/app/admin/charts/printer-utilization"; -import { PrinterVolumeChart } from "@/app/admin/charts/printer-volume"; -import { DataCard } from "@/components/data-card"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { db } from "@/server/db"; -import { calculatePrinterErrorRate } from "@/utils/analytics/error-rate"; -import { calculateAbortReasonsCount } from "@/utils/analytics/errors"; -import { forecastPrinterUsage } from "@/utils/analytics/forecast"; -import { calculatePrinterUtilization } from "@/utils/analytics/utilization"; -import { calculatePrintVolumes } from "@/utils/analytics/volume"; -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Admin Dashboard", -}; - -export const dynamic = "force-dynamic"; - -export default async function AdminPage() { - const currentDate = new Date(); - - const lastMonth = new Date(); - lastMonth.setDate(currentDate.getDate() - 31); - const printers = await db.query.printers.findMany({}); - const printJobs = await db.query.printJobs.findMany({ - where: (job, { gte }) => gte(job.startAt, lastMonth), - with: { - printer: true, - }, - }); - if (printJobs.length < 1) { - return ( - - - Druckaufträge - Zurzeit sind keine Druckaufträge verfügbar. - - -

Aktualisieren Sie die Seite oder prüfen Sie später erneut, ob neue Druckaufträge verfügbar sind.

-
-
- ); - } - - const currentPrintJobs = printJobs.filter((job) => { - if (job.aborted) return false; - - const endAt = job.startAt.getTime() + job.durationInMinutes * 1000 * 60; - - return endAt > currentDate.getTime(); - }); - const occupiedPrinters = currentPrintJobs.map((job) => job.printer.id); - const freePrinters = printers.filter((printer) => !occupiedPrinters.includes(printer.id)); - const printerUtilization = calculatePrinterUtilization(printJobs); - const printerVolume = calculatePrintVolumes(printJobs); - const printerAbortReasons = calculateAbortReasonsCount(printJobs); - const printerErrorRate = calculatePrinterErrorRate(printJobs); - const printerForecast = forecastPrinterUsage(printJobs); - - return ( - <> - - - Allgemein - Druckerauslastung - Fehlerberichte - Prognosen - - -
-
- -
- - -
-
- -
-
- -
- {printerUtilization.map((data) => ( - - ))} -
-
- -
-
- -
-
- -
-
-
- -
-
- ({ - day: index, - usageMinutes, - }))} - /> -
-
-
-
- - ); -} - -================ -File: src/app/admin/printers/columns.tsx -================ -"use client"; -import type { printers } from "@/server/db/schema"; -import type { ColumnDef } from "@tanstack/react-table"; -import type { InferSelectModel } from "drizzle-orm"; -import { ArrowUpDown, MoreHorizontal, PencilIcon } from "lucide-react"; - -import { EditPrinterDialogContent, EditPrinterDialogTrigger } from "@/app/admin/printers/dialogs/edit-printer"; -import { Button } from "@/components/ui/button"; -import { Dialog } from "@/components/ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { type PrinterStatus, translatePrinterStatus } from "@/utils/printers"; -import { useState } from "react"; - -// This type is used to define the shape of our data. -// You can use a Zod schema here if you want. - -export const columns: ColumnDef>[] = [ - { - accessorKey: "id", - header: ({ column }) => { - return ( - - ); - }, - }, - { - accessorKey: "name", - header: "Name", - }, - { - accessorKey: "description", - header: "Beschreibung", - }, - { - accessorKey: "status", - header: "Status", - cell: ({ row }) => { - const status = row.getValue("status"); - const translated = translatePrinterStatus(status as PrinterStatus); - - return translated; - }, - }, - { - id: "actions", - cell: ({ row }) => { - const printer = row.original; - const [open, setOpen] = useState(false); - - return ( - - - - - - - Aktionen - ABC - - -
- - Bearbeiten -
-
-
-
-
- -
- ); - }, - }, -]; - -================ -File: src/app/admin/printers/data-table.tsx -================ -"use client"; - -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { SlidersHorizontalIcon } from "lucide-react"; -import { useState } from "react"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; -} - -export function DataTable({ columns, data }: DataTableProps) { - const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState([]); - const [columnVisibility, setColumnVisibility] = useState({}); - - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onSortingChange: setSorting, - getSortedRowModel: getSortedRowModel(), - onColumnFiltersChange: setColumnFilters, - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - state: { - sorting, - columnFilters, - columnVisibility, - }, - }); - - return ( -
-
- table.getColumn("name")?.setFilterValue(event.target.value)} - className="max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - column.toggleVisibility(!!value)} - > - {column.id} - - ); - })} - - -
- -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - {flexRender(cell.column.columnDef.cell, cell.getContext())} - ))} - - )) - ) : ( - - - Keine Ergebnisse gefunden. - - - )} - -
-
-
- - -
-
- ); -} - -================ -File: src/app/admin/printers/dialogs/create-printer.tsx -================ -"use client"; - -import { PrinterForm } from "@/app/admin/printers/form"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { useState } from "react"; - -interface CreatePrinterDialogProps { - children: React.ReactNode; -} - -export function CreatePrinterDialog(props: CreatePrinterDialogProps) { - const { children } = props; - const [open, setOpen] = useState(false); - - return ( - - {children} - - - Drucker erstellen - - - - - ); -} - -================ -File: src/app/admin/printers/dialogs/delete-printer.tsx -================ -"use client"; - -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { useToast } from "@/components/ui/use-toast"; -import { deletePrinter } from "@/server/actions/printers"; -import { TrashIcon } from "lucide-react"; - -interface DeletePrinterDialogProps { - printerId: string; - setOpen: (state: boolean) => void; -} -export function DeletePrinterDialog(props: DeletePrinterDialogProps) { - const { printerId, setOpen } = props; - const { toast } = useToast(); - - async function onSubmit() { - toast({ - description: "Drucker wird gelöscht...", - }); - try { - const result = await deletePrinter(printerId); - if (result?.error) { - toast({ - description: result.error, - variant: "destructive", - }); - } - toast({ - description: "Drucker wurde gelöscht.", - }); - setOpen(false); - } catch (error) { - if (error instanceof Error) { - toast({ - description: error.message, - variant: "destructive", - }); - } else { - toast({ - description: "Ein unbekannter Fehler ist aufgetreten.", - variant: "destructive", - }); - } - } - } - - return ( - - - - - - - Bist Du dir sicher? - - Diese Aktion kann nicht rückgängig gemacht werden. Der Drucker und die damit verbundenen Daten werden - unwiderruflich gelöscht. - - - - Abbrechen - - Ja, löschen - - - - - ); -} - -================ -File: src/app/admin/printers/dialogs/edit-printer.tsx -================ -import { PrinterForm } from "@/app/admin/printers/form"; -import { DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import type { InferResultType } from "@/utils/drizzle"; - -interface EditPrinterDialogTriggerProps { - children: React.ReactNode; -} - -export function EditPrinterDialogTrigger(props: EditPrinterDialogTriggerProps) { - const { children } = props; - - return {children}; -} - -interface EditPrinterDialogContentProps { - printer: InferResultType<"printers">; - setOpen: (open: boolean) => void; -} -export function EditPrinterDialogContent(props: EditPrinterDialogContentProps) { - const { printer, setOpen } = props; - - return ( - - - Drucker bearbeiten - - - - ); -} - -================ -File: src/app/admin/printers/form.tsx -================ -"use client"; -import { DeletePrinterDialog } from "@/app/admin/printers/dialogs/delete-printer"; -import { Button } from "@/components/ui/button"; -import { DialogClose } from "@/components/ui/dialog"; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { useToast } from "@/components/ui/use-toast"; -import { createPrinter, updatePrinter } from "@/server/actions/printers"; -import type { InferResultType } from "@/utils/drizzle"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { SaveIcon, XCircleIcon } from "lucide-react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -export const formSchema = z.object({ - name: z - .string() - .min(2, { - message: "Der Name muss mindestens 2 Zeichen lang sein.", - }) - .max(50), - description: z - .string() - .min(2, { - message: "Die Beschreibung muss mindestens 2 Zeichen lang sein.", - }) - .max(50), - status: z.coerce.number().int().min(0).max(1), -}); - -interface PrinterFormProps { - printer?: InferResultType<"printers">; - setOpen: (state: boolean) => void; -} - -export function PrinterForm(props: PrinterFormProps) { - const { printer, setOpen } = props; - const { toast } = useToast(); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - name: printer?.name ?? "", - description: printer?.description ?? "", - status: printer?.status ?? 0, - }, - }); - - // 2. Define a submit handler. - async function onSubmit(values: z.infer) { - // TODO: create or update - if (printer) { - toast({ - description: "Drucker wird aktualisiert...", - }); - - // Update - try { - const result = await updatePrinter(printer.id, { - description: values.description, - name: values.name, - status: values.status, - }); - if (result?.error) { - toast({ - description: result.error, - variant: "destructive", - }); - } - - setOpen(false); - - toast({ - description: "Drucker wurde aktualisiert.", - variant: "default", - }); - } catch (error) { - if (error instanceof Error) { - toast({ - description: error.message, - variant: "destructive", - }); - } else { - toast({ - description: "Ein unbekannter Fehler ist aufgetreten.", - variant: "destructive", - }); - } - } - } else { - toast({ - description: "Drucker wird erstellt...", - variant: "default", - }); - - // Create - try { - const result = await createPrinter({ - description: values.description, - name: values.name, - status: values.status, - }); - if (result?.error) { - toast({ - description: result.error, - variant: "destructive", - }); - } - - setOpen(false); - - toast({ - description: "Drucker wurde erstellt.", - variant: "default", - }); - } catch (error) { - if (error instanceof Error) { - toast({ - description: error.message, - variant: "destructive", - }); - } else { - toast({ - description: "Ein unbekannter Fehler ist aufgetreten.", - variant: "destructive", - }); - } - } - } - } - - return ( -
- - ( - - Name - - - - Bitte gib einen eindeutigen Namen für den Drucker ein. - - - )} - /> - ( - - Beschreibung - - - - Füge eine kurze Beschreibung des Druckers hinzu. - - - )} - /> - ( - - Status - - Wähle den aktuellen Status des Druckers. - - - )} - /> -
- {printer && } - {!printer && ( - - - - )} - -
- - - ); -} - -================ -File: src/app/admin/printers/page.tsx -================ -import { columns } from "@/app/admin/printers/columns"; -import { DataTable } from "@/app/admin/printers/data-table"; -import { CreatePrinterDialog } from "@/app/admin/printers/dialogs/create-printer"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { db } from "@/server/db"; -import { PlusCircleIcon } from "lucide-react"; - -export default async function AdminPage() { - const data = await db.query.printers.findMany(); - - return ( - - -
- Druckerverwaltung - Suche, Bearbeite, Lösche und Erstelle Drucker -
- - - -
- - - -
- ); -} - -================ -File: src/app/admin/settings/download/route.ts -================ -import fs from "node:fs"; - -export const dynamic = 'force-dynamic'; - -export async function GET() { - return new Response(fs.readFileSync("./db/sqlite.db")); -} - -================ -File: src/app/admin/settings/page.tsx -================ -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import Link from "next/link"; - -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Systemeinstellungen", -}; - -export default function AdminPage() { - return ( - - - Einstellungen - Systemeinstellungen - - -
-

Datenbank herunterladen

- -
-
-
- ); -} - -================ -File: src/app/admin/users/columns.tsx -================ -"use client"; - -import { type UserRole, translateUserRole } from "@/server/auth/permissions"; -import type { users } from "@/server/db/schema"; -import type { ColumnDef } from "@tanstack/react-table"; -import type { InferSelectModel } from "drizzle-orm"; -import { - ArrowUpDown, - MailIcon, - MessageCircleIcon, - MoreHorizontal, -} from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import Link from "next/link"; -import { - EditUserDialogContent, - EditUserDialogRoot, - EditUserDialogTrigger, -} from "@/app/admin/users/dialog"; - -// This type is used to define the shape of our data. -// You can use a Zod schema here if you want. -export type User = { - id: string; - github_id: number; - username: string; - displayName: string; - email: string; - role: string; -}; - -export const columns: ColumnDef>[] = [ - { - accessorKey: "id", - header: ({ column }) => { - return ( - - ); - }, - }, - { - accessorKey: "github_id", - header: "GitHub ID", - }, - { - accessorKey: "username", - header: "Username", - }, - { - accessorKey: "displayName", - header: "Name", - }, - { - accessorKey: "email", - header: "E-Mail", - }, - { - accessorKey: "role", - header: "Rolle", - cell: ({ row }) => { - const role = row.getValue("role"); - const translated = translateUserRole(role as UserRole); - - return translated; - }, - }, - { - id: "actions", - cell: ({ row }) => { - const user = row.original; - - return ( - - - - - - - Aktionen - - - - Teams-Chat öffnen - - - - - - E-Mail schicken - - - - - - - - - - - ); - }, - }, -]; - -function generateTeamsChatURL(email: string) { - return `https://teams.microsoft.com/l/chat/0/0?users=${email}`; -} - -function generateEMailURL(email: string) { - return `mailto:${email}`; -} - -================ -File: src/app/admin/users/data-table.tsx -================ -"use client"; - -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { SlidersHorizontalIcon } from "lucide-react"; -import { useState } from "react"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; -} - -export function DataTable({ columns, data }: DataTableProps) { - const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState([]); - const [columnVisibility, setColumnVisibility] = useState({}); - - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onSortingChange: setSorting, - getSortedRowModel: getSortedRowModel(), - onColumnFiltersChange: setColumnFilters, - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - state: { - sorting, - columnFilters, - columnVisibility, - }, - }); - - return ( -
-
- table.getColumn("email")?.setFilterValue(event.target.value)} - className="max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - column.toggleVisibility(!!value)} - > - {column.id} - - ); - })} - - -
- -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - {flexRender(cell.column.columnDef.cell, cell.getContext())} - ))} - - )) - ) : ( - - - Keine Ergebnisse gefunden. - - - )} - -
-
-
- - -
-
- ); -} - -================ -File: src/app/admin/users/dialog.tsx -================ -import type { User } from "@/app/admin/users/columns"; -import { ProfileForm } from "@/app/admin/users/form"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { PencilIcon } from "lucide-react"; - -interface EditUserDialogRootProps { - children: React.ReactNode; -} - -export function EditUserDialogRoot(props: EditUserDialogRootProps) { - const { children } = props; - - return {children}; -} - -export function EditUserDialogTrigger() { - return ( - - - Benutzer bearbeiten - - ); -} - -interface EditUserDialogContentProps { - user: User; -} - -export function EditUserDialogContent(props: EditUserDialogContentProps) { - const { user } = props; - - if (!user) { - return; - } - - return ( - - - Benutzer bearbeiten - - Hinweis: In den seltensten Fällen sollten die Daten - eines Benutzers geändert werden. Dies kann zu unerwarteten Problemen - führen. - - - - - ); -} - -================ -File: src/app/admin/users/form.tsx -================ -"use client"; - -import type { User } from "@/app/admin/users/columns"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { DialogClose } from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { useToast } from "@/components/ui/use-toast"; -import { deleteUser, updateUser } from "@/server/actions/users"; -import type { UserRole } from "@/server/auth/permissions"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { SaveIcon, TrashIcon } from "lucide-react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -export const formSchema = z.object({ - username: z - .string() - .min(2, { - message: "Der Benutzername muss mindestens 2 Zeichen lang sein.", - }) - .max(50), - displayName: z - .string() - .min(2, { - message: "Der Anzeigename muss mindestens 2 Zeichen lang sein.", - }) - .max(50), - email: z.string().email(), - role: z.enum(["admin", "user", "guest"]), -}); - -interface ProfileFormProps { - user: User; -} - -export function ProfileForm(props: ProfileFormProps) { - const { user } = props; - const { toast } = useToast(); - - // 1. Define your form. - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - username: user.username, - displayName: user.displayName, - email: user.email, - role: user.role as UserRole, - }, - }); - - // 2. Define a submit handler. - async function onSubmit(values: z.infer) { - toast({ description: "Benutzerprofil wird aktualisiert..." }); - - await updateUser(user.id, values); - - toast({ description: "Benutzerprofil wurde aktualisiert." }); - } - - return ( -
- - ( - - Benutzername - - - - - Nur in Ausnahmefällen sollte der Benutzername geändert werden. - - - - )} - /> - ( - - Anzeigename - - - - - Der Anzeigename darf frei verändert werden. - - - - )} - /> - ( - - E-Mail Adresse - - - - - Nur in Ausnahmefällen sollte die E-Mail Adresse geändert werden. - - - - )} - /> - ( - - Benutzerrolle - - - Die Benutzerrolle bestimmt die Berechtigungen des Benutzers. - - - - )} - /> -
- - - - - - - Bist du dir sicher? - - Diese Aktion kann nicht rückgängig gemacht werden. Das - Benutzerprofil und die damit verbundenen Daten werden - unwiderruflich gelöscht. - - - - Abbrechen - { - toast({ description: "Benutzerprofil wird gelöscht..." }); - deleteUser(user.id); - toast({ description: "Benutzerprofil wurde gelöscht." }); - }} - > - Ja, löschen - - - - - - - -
- - - ); -} - -================ -File: src/app/admin/users/page.tsx -================ -import { columns } from "@/app/admin/users/columns"; -import { DataTable } from "@/app/admin/users/data-table"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { db } from "@/server/db"; - -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Alle Benutzer", -}; - -export default async function AdminPage() { - const data = await db.query.users.findMany(); - - return ( - - - Benutzerverwaltung - Suche, Bearbeite und Lösche Benutzer - - - - - - ); -} - -================ -File: src/app/api/job/[jobId]/remaining-time/route.ts -================ -import { db } from "@/server/db"; -import { printJobs } from "@/server/db/schema"; -import { eq } from "drizzle-orm"; - -export const dynamic = "force-dynamic"; - -interface RemainingTimeRouteProps { - params: { - jobId: string; - }; -} -export async function GET(request: Request, { params }: RemainingTimeRouteProps) { - // Trying to fix build error in container... - if (params.jobId === undefined) { - return Response.json({}); - } - - // Get the job details - const jobDetails = await db.query.printJobs.findFirst({ - where: eq(printJobs.id, params.jobId), - }); - - // Check if the job exists - if (!jobDetails) { - return Response.json({ - id: params.jobId, - error: "Job not found", - }); - } - - // Calculate the remaining time - const startAt = new Date(jobDetails.startAt).getTime(); - const endAt = startAt + jobDetails.durationInMinutes * 60 * 1000; - const remainingTime = Math.max(0, endAt - Date.now()); - - // Return the remaining time - return Response.json({ - id: params.jobId, - remainingTime, - }); -} - -================ -File: src/app/api/printers/route.ts -================ -import { getPrinters } from "@/server/actions/printers"; - -export const dynamic = "force-dynamic"; - -export async function GET() { - const printers = await getPrinters(); - - return Response.json(printers); -} - -================ -File: src/app/auth/login/callback/route.ts -================ -import { lucia } from "@/server/auth"; -import { type GitHubUserResult, github } from "@/server/auth/oauth"; -import { db } from "@/server/db"; -import { users } from "@/server/db/schema"; -import { OAuth2RequestError } from "arctic"; -import { eq } from "drizzle-orm"; -import { generateIdFromEntropySize } from "lucia"; -import { cookies } from "next/headers"; - -export const dynamic = "force-dynamic"; - -interface GithubEmailResponse { - email: string; - primary: boolean; - verified: boolean; - visibility: string; -} - -export async function GET(request: Request): Promise { - const url = new URL(request.url); - const code = url.searchParams.get("code"); - const state = url.searchParams.get("state"); - const storedState = cookies().get("github_oauth_state")?.value ?? null; - if (!code || !state || !storedState || state !== storedState) { - return new Response( - JSON.stringify({ - status_text: "Something is wrong", - data: { code, state, storedState }, - }), - { - status: 400, - }, - ); - } - - try { - const tokens = await github.validateAuthorizationCode(code); - const githubUserResponse = await fetch("https://git.i.mercedes-benz.com/api/v3/user", { - headers: { - Authorization: `Bearer ${tokens.accessToken}`, - }, - }); - const githubUser: GitHubUserResult = await githubUserResponse.json(); - - // Sometimes email can be null in the user query. - if (githubUser.email === null || githubUser.email === undefined) { - const githubEmailResponse = await fetch("https://git.i.mercedes-benz.com/api/v3/user/emails", { - headers: { - Authorization: `Bearer ${tokens.accessToken}`, - }, - }); - const githubUserEmail: GithubEmailResponse[] = await githubEmailResponse.json(); - githubUser.email = githubUserEmail[0].email; - } - const existingUser = await db.query.users.findFirst({ - where: eq(users.github_id, githubUser.id), - }); - - if (existingUser) { - const session = await lucia.createSession(existingUser.id, {}); - const sessionCookie = lucia.createSessionCookie(session.id); - cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); - return new Response(null, { - status: 302, - headers: { - Location: "/", - }, - }); - } - - const userId = generateIdFromEntropySize(10); // 16 characters long - - await db.insert(users).values({ - id: userId, - github_id: githubUser.id, - username: githubUser.login, - displayName: githubUser.name, - email: githubUser.email, - }); - - const session = await lucia.createSession(userId, {}); - const sessionCookie = lucia.createSessionCookie(session.id); - cookies().set(sessionCookie.name, sessionCookie.value, { - ...sessionCookie.attributes, - secure: false, // Else cookie does not get set cause IT has not provided us an SSL certificate yet - }); - return new Response(null, { - status: 302, - headers: { - Location: "/", - }, - }); - } catch (e) { - // the specific error message depends on the provider - if (e instanceof OAuth2RequestError) { - // invalid code - return new Response( - JSON.stringify({ - status_text: "Invalid code", - error: JSON.stringify(e), - }), - { - status: 400, - }, - ); - } - return new Response(null, { - status: 500, - }); - } -} - -================ -File: src/app/auth/login/route.ts -================ -import { github } from "@/server/auth/oauth"; -import { generateState } from "arctic"; -import { cookies } from "next/headers"; - -export const dynamic = "force-dynamic"; - -export async function GET(): Promise { - const state = generateState(); - const url = await github.createAuthorizationURL(state, { - scopes: ["user"], - }); - const ONE_HOUR = 60 * 60; - - cookies().set("github_oauth_state", state, { - path: "/", - secure: false, //process.env.NODE_ENV === "production", -- can't be used until SSL certificate is provided by IT - httpOnly: true, - maxAge: ONE_HOUR, - sameSite: "lax", - }); - - return Response.redirect(url); -} - -================ -File: src/app/globals.css -================ -@tailwind base; -@tailwind components; -@tailwind utilities; - - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; - --radius: 0.75rem; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - } - - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } -} - -================ -File: src/app/job/[jobId]/cancel-form.tsx -================ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { useToast } from "@/components/ui/use-toast"; -import { abortPrintJob } from "@/server/actions/printJobs"; -import { TriangleAlertIcon } from "lucide-react"; -import { useState } from "react"; - -const formSchema = z.object({ - abortReason: z - .string() - .min(1, { - message: "Bitte gebe einen Grund für den Abbruch an.", - }) - .max(255, { - message: "Der Grund darf maximal 255 Zeichen lang sein.", - }), -}); - -interface CancelFormProps { - jobId: string; -} - -export function CancelForm(props: CancelFormProps) { - const { jobId } = props; - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - abortReason: "", - }, - }); - const { toast } = useToast(); - const [open, setOpen] = useState(false); - - async function onSubmit(values: z.infer) { - toast({ - description: "Druckauftrag wird abgebrochen...", - }); - try { - const result = await abortPrintJob(jobId, values.abortReason); - if (result?.error) { - toast({ - description: result.error, - variant: "destructive", - }); - } - setOpen(false); - toast({ - description: "Druckauftrag wurde abgebrochen.", - }); - } catch (error) { - if (error instanceof Error) { - toast({ - description: error.message, - variant: "destructive", - }); - } else { - toast({ - description: "Ein unbekannter Fehler ist aufgetreten.", - variant: "destructive", - }); - } - } - } - - return ( - - - - - - - Druckauftrag abbrechen? - - Du bist dabei, den Druckauftrag abzubrechen. Bitte beachte, dass ein abgebrochener Druckauftrag nicht wieder - aufgenommen werden kann und der Drucker sich automatisch abschaltet. - - -
- - ( - - Grund für den Abbruch - - - - - Bitte teile uns den Grund für den Abbruch des Druckauftrags mit. Wenn der Drucker eine Fehlermeldung - anzeigt, gib bitte nur diese Fehlermeldung an. - - - - )} - /> -
- - - - -
- - -
-
- ); -} - -================ -File: src/app/job/[jobId]/edit-comments.tsx -================ -"use client"; - -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { useToast } from "@/components/ui/use-toast"; -import { updatePrintComments } from "@/server/actions/printJobs"; -import { useDebouncedCallback } from "use-debounce"; - -interface EditCommentsProps { - defaultValue: string | null; - jobId: string; - disabled?: boolean; -} -export function EditComments(props: EditCommentsProps) { - const { defaultValue, jobId, disabled } = props; - const { toast } = useToast(); - - const debounced = useDebouncedCallback(async (value) => { - try { - const result = await updatePrintComments(jobId, value); - if (result?.error) { - toast({ - description: result.error, - variant: "destructive", - }); - } - toast({ - description: "Anmerkungen wurden gespeichert.", - }); - } catch (error) { - if (error instanceof Error) { - toast({ - description: error.message, - variant: "destructive", - }); - } else { - toast({ - description: "Ein unbekannter Fehler ist aufgetreten.", - variant: "destructive", - }); - } - } - }, 1000); - - return ( -
- -