Remove deprecated backend files and documentation, including Docker configurations, environment variables, and various scripts, to streamline the project structure and eliminate unused components.
This commit is contained in:
parent
d2f23d589a
commit
ead75ae451
@ -1,47 +0,0 @@
|
|||||||
# Dokumentation MYP - Manage your Printer
|
|
||||||
|
|
||||||
## Projektbeschreibung
|
|
||||||
|
|
||||||
MYP (Manage your Printer) ist eine Plattform zur Reservierung von 3D-Druckern, die für die TBA im Werk 040, Berlin-Marienfelde, entwickelt wurde.
|
|
||||||
|
|
||||||
## Projektstruktur
|
|
||||||
|
|
||||||
- `backend/`: Flask-Backend für die API-Anbindung und Datenbankzugriff
|
|
||||||
- `frontend/`: Next.js Frontend für die Benutzeroberfläche
|
|
||||||
- `docs/`: Ausführliche Dokumentationen, Datenbankschema und Diagramme
|
|
||||||
- `scripts/`: Deployment- und Setup-Skripte
|
|
||||||
- `logs/`: Fehlerprotokolle und Logs
|
|
||||||
|
|
||||||
## Umfassende Dokumentation
|
|
||||||
|
|
||||||
Detaillierte Dokumentationen finden Sie in den folgenden Dateien:
|
|
||||||
|
|
||||||
- [Technische Dokumentation](docs/README.md)
|
|
||||||
- [Datenbankstruktur](docs/MYP.dbml)
|
|
||||||
- [Aktueller Projektstand](docs/Aktueller%20Stand.md)
|
|
||||||
- [IHK-Dokumentation](docs/Dokumentation_IHK.md)
|
|
||||||
|
|
||||||
## Herausforderungen und Komplikationen
|
|
||||||
|
|
||||||
- Netzwerkanbindung
|
|
||||||
- Ermitteln der Schnittstellen der Drucker
|
|
||||||
- Auswahl der Anbindung, Entwickeln eines Netzwerkkonzeptes
|
|
||||||
- Beschaffung der Hardware (beschränkte Auswahlmöglichkeiten)
|
|
||||||
- Welches Betriebssystem? OpenSuse, NixOS, Debian
|
|
||||||
- Frontend verstehen lernen
|
|
||||||
- Netzwerk einrichten, Frontend anbinden
|
|
||||||
|
|
||||||
## Verwendete Technologien
|
|
||||||
|
|
||||||
- Backend: Python, Flask
|
|
||||||
- Frontend: Next.js, React, TypeScript
|
|
||||||
- Datenbank: SQL
|
|
||||||
- Docker für Containerisierung
|
|
||||||
- Raspberry Pi für Druckersteuerung
|
|
||||||
|
|
||||||
## Installation und Einsatz
|
|
||||||
|
|
||||||
Installation und Einrichtung werden durch die Skripte im Verzeichnis `scripts/` unterstützt.
|
|
||||||
|
|
||||||
- `scripts/setup/`: Einrichtungsskripte für Backend, Docker und OAuth
|
|
||||||
- `scripts/deployment/`: Bereitstellungsskripte für Raspberry Pi
|
|
475
README.md
475
README.md
@ -1,475 +0,0 @@
|
|||||||
# 🖨️ MYP - Manage your Printer
|
|
||||||
|
|
||||||
[](https://docker.com)
|
|
||||||
[](https://linux.org)
|
|
||||||
[](https://windows.com)
|
|
||||||
[](LICENSE.md)
|
|
||||||
[](https://github.com)
|
|
||||||
|
|
||||||
MYP *(Manage your Printer)* ist eine moderne, containerbasierte Plattform zur Verwaltung und Reservierung von 3D-Druckern, entwickelt für die TBA im Werk 040, Berlin-Marienfelde.
|
|
||||||
|
|
||||||
## 🏗️ Architektur
|
|
||||||
|
|
||||||
Das System basiert auf einer **Microservice-Architektur** mit klarer Trennung zwischen Frontend und Backend:
|
|
||||||
|
|
||||||
- **🖥️ Backend**: Flask API Server (Python) - Port 5000
|
|
||||||
- **🌐 Frontend**: Next.js Web Interface (TypeScript/React) - Port 3000
|
|
||||||
- **🔄 Proxy**: Caddy Reverse Proxy - Port 80/443
|
|
||||||
- **📊 Monitoring**: Prometheus & Grafana (Entwicklung)
|
|
||||||
|
|
||||||
### 🔧 Technologie-Stack
|
|
||||||
|
|
||||||
**Backend:**
|
|
||||||
- Flask 3.0+ (Python Web Framework)
|
|
||||||
- SQLAlchemy (ORM)
|
|
||||||
- JWT Authentication
|
|
||||||
- TAPO Smart Plug Integration
|
|
||||||
- RESTful API Design
|
|
||||||
|
|
||||||
**Frontend:**
|
|
||||||
- Next.js 14+ (React Framework)
|
|
||||||
- TypeScript (Type Safety)
|
|
||||||
- Tailwind CSS (Styling)
|
|
||||||
- Drizzle ORM (Database)
|
|
||||||
- Modern UI Components
|
|
||||||
|
|
||||||
**Infrastructure:**
|
|
||||||
- Docker & Docker Compose
|
|
||||||
- Caddy (Reverse Proxy)
|
|
||||||
- Prometheus (Monitoring)
|
|
||||||
- Grafana (Dashboards)
|
|
||||||
|
|
||||||
## 🚀 Schnellstart
|
|
||||||
|
|
||||||
### Voraussetzungen
|
|
||||||
|
|
||||||
- **Docker** & **Docker Compose** installiert
|
|
||||||
- **Git** (empfohlen für Entwicklung)
|
|
||||||
- **4GB RAM** und **10GB freier Speicherplatz**
|
|
||||||
- **PowerShell 5.1+** (Windows) oder **Bash 4.0+** (Linux/macOS)
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
1. **Repository klonen**
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd Projektarbeit-MYP
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **System starten**
|
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
```powershell
|
|
||||||
# Einfacher Start (Entwicklungsumgebung)
|
|
||||||
.\start.ps1
|
|
||||||
|
|
||||||
# Mit Optionen
|
|
||||||
.\start.ps1 -Help # Hilfe anzeigen
|
|
||||||
.\start.ps1 prod # Produktionsumgebung
|
|
||||||
.\start.ps1 dev -Clean # Mit Bereinigung
|
|
||||||
```
|
|
||||||
|
|
||||||
**Linux/macOS:**
|
|
||||||
```bash
|
|
||||||
# Ausführungsrechte setzen (einmalig)
|
|
||||||
chmod +x start.sh cleanup.sh
|
|
||||||
|
|
||||||
# Einfacher Start (Entwicklungsumgebung)
|
|
||||||
./start.sh
|
|
||||||
|
|
||||||
# Mit Optionen
|
|
||||||
./start.sh --help # Hilfe anzeigen
|
|
||||||
./start.sh prod # Produktionsumgebung
|
|
||||||
./start.sh dev --clean # Mit Bereinigung
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Zugriff auf die Anwendung**
|
|
||||||
- 🌐 **Web Interface**: http://localhost
|
|
||||||
- 🔧 **Backend API**: http://localhost/api
|
|
||||||
- ⚛️ **Frontend Dev**: http://localhost:3000 (nur Entwicklung)
|
|
||||||
|
|
||||||
## 📋 Verfügbare Umgebungen
|
|
||||||
|
|
||||||
### 🛠️ Entwicklung (Standard)
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
.\start.ps1 dev
|
|
||||||
|
|
||||||
# Linux/macOS
|
|
||||||
./start.sh dev
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Hot Reload für Frontend und Backend
|
|
||||||
- Debug-Server auf Port 5555 (Backend) und 8081 (Frontend)
|
|
||||||
- Monitoring: Prometheus (9090), Grafana (3001)
|
|
||||||
- Datenbank-Viewer: Adminer (8080)
|
|
||||||
- Redis Cache (6379)
|
|
||||||
- Entwickler-Tools aktiviert
|
|
||||||
|
|
||||||
### 🚀 Produktion
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
.\start.ps1 prod
|
|
||||||
|
|
||||||
# Linux/macOS
|
|
||||||
./start.sh prod
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Optimierte Container-Images
|
|
||||||
- SSL-Verschlüsselung (Let's Encrypt)
|
|
||||||
- Performance-Optimierungen
|
|
||||||
- Sicherheitsheader
|
|
||||||
- Automatische Backups
|
|
||||||
- Health Checks
|
|
||||||
|
|
||||||
### 🧪 Test
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
.\start.ps1 test
|
|
||||||
|
|
||||||
# Linux/macOS
|
|
||||||
./start.sh test
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- In-Memory-Datenbank
|
|
||||||
- Mock-Services
|
|
||||||
- Test-Fixtures
|
|
||||||
- Coverage-Reports
|
|
||||||
- Automatisierte Tests
|
|
||||||
|
|
||||||
## 🛠️ Entwicklung
|
|
||||||
|
|
||||||
### Projektstruktur
|
|
||||||
|
|
||||||
```
|
|
||||||
Projektarbeit-MYP/
|
|
||||||
├── 🖥️ backend/ # Flask API Server
|
|
||||||
│ ├── app.py # Hauptanwendung
|
|
||||||
│ ├── config.py # Konfiguration
|
|
||||||
│ ├── security.py # Sicherheitsmodule
|
|
||||||
│ ├── monitoring.py # Monitoring & Logging
|
|
||||||
│ ├── requirements.txt # Python-Abhängigkeiten
|
|
||||||
│ ├── Dockerfile # Produktions-Container
|
|
||||||
│ ├── Dockerfile.dev # Entwicklungs-Container
|
|
||||||
│ ├── instance/ # Datenbank & Uploads
|
|
||||||
│ ├── logs/ # Anwendungslogs
|
|
||||||
│ ├── migrations/ # Datenbankmigrationen
|
|
||||||
│ ├── static/ # Statische Dateien
|
|
||||||
│ └── templates/ # HTML-Templates
|
|
||||||
│
|
|
||||||
├── 🌐 frontend/ # Next.js Web Interface
|
|
||||||
│ ├── src/ # Quellcode
|
|
||||||
│ │ ├── app/ # App Router (Next.js 14)
|
|
||||||
│ │ ├── components/ # React-Komponenten
|
|
||||||
│ │ ├── server/ # Server-seitige Logik
|
|
||||||
│ │ └── utils/ # Hilfsfunktionen
|
|
||||||
│ ├── public/ # Öffentliche Dateien
|
|
||||||
│ ├── package.json # Node.js-Abhängigkeiten
|
|
||||||
│ ├── Dockerfile # Produktions-Container
|
|
||||||
│ ├── Dockerfile.dev # Entwicklungs-Container
|
|
||||||
│ ├── tailwind.config.ts # Tailwind-Konfiguration
|
|
||||||
│ ├── tsconfig.json # TypeScript-Konfiguration
|
|
||||||
│ └── drizzle/ # Datenbank-Schema
|
|
||||||
│
|
|
||||||
├── 🔄 proxy/ # Caddy Reverse Proxy
|
|
||||||
│ └── Caddyfile # Proxy-Konfiguration
|
|
||||||
│
|
|
||||||
├── 📊 monitoring/ # Prometheus & Grafana
|
|
||||||
│ ├── prometheus/ # Monitoring-Konfiguration
|
|
||||||
│ └── grafana/ # Dashboard-Konfiguration
|
|
||||||
│
|
|
||||||
├── 🔧 infrastructure/ # Deployment & Konfiguration
|
|
||||||
│ ├── scripts/ # Deployment-Skripte
|
|
||||||
│ │ ├── start.ps1 # Windows-Startskript
|
|
||||||
│ │ ├── start.sh # Linux/macOS-Startskript
|
|
||||||
│ │ ├── cleanup.ps1 # Windows-Bereinigung
|
|
||||||
│ │ └── cleanup.sh # Linux/macOS-Bereinigung
|
|
||||||
│ └── environments/ # Umgebungskonfigurationen
|
|
||||||
│ ├── development.env # Entwicklungsumgebung
|
|
||||||
│ ├── production.env # Produktionsumgebung
|
|
||||||
│ └── test.env # Testumgebung
|
|
||||||
│
|
|
||||||
├── 🧪 tests/ # Übergreifende Tests
|
|
||||||
│ ├── e2e/ # End-to-End-Tests
|
|
||||||
│ └── integration/ # Integrationstests
|
|
||||||
│
|
|
||||||
├── 📚 docs/ # Projektdokumentation
|
|
||||||
├── 📝 logs/ # Systemlogs
|
|
||||||
├── 🐳 docker-compose.yml # Hauptkonfiguration
|
|
||||||
├── 🐳 docker-compose.dev.yml # Entwicklungskonfiguration
|
|
||||||
├── 📋 README.md # Diese Datei
|
|
||||||
├── 📋 PROJECT_STRUCTURE.md # Detaillierte Architektur
|
|
||||||
├── 📋 Dokumentation.md # Deutsche Dokumentation
|
|
||||||
└── 📄 LICENSE.md # Lizenzinformationen
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backend-Entwicklung
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
|
|
||||||
# Virtuelle Umgebung erstellen
|
|
||||||
python -m venv venv
|
|
||||||
|
|
||||||
# Aktivieren
|
|
||||||
source venv/bin/activate # Linux/macOS
|
|
||||||
venv\Scripts\activate # Windows
|
|
||||||
|
|
||||||
# Abhängigkeiten installieren
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Entwicklungsserver starten
|
|
||||||
flask run --debug --host=0.0.0.0 --port=5000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend-Entwicklung
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
|
|
||||||
# Abhängigkeiten installieren
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# Entwicklungsserver starten
|
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# Build für Produktion
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
# Linting & Formatierung
|
|
||||||
pnpm lint
|
|
||||||
pnpm format
|
|
||||||
```
|
|
||||||
|
|
||||||
### API-Dokumentation
|
|
||||||
|
|
||||||
Die API-Dokumentation ist verfügbar unter:
|
|
||||||
- **Swagger UI**: http://localhost/api/docs
|
|
||||||
- **OpenAPI Spec**: http://localhost/api/swagger.json
|
|
||||||
- **Redoc**: http://localhost/api/redoc
|
|
||||||
|
|
||||||
## 🔧 Konfiguration
|
|
||||||
|
|
||||||
### Umgebungsvariablen
|
|
||||||
|
|
||||||
Konfigurationsdateien befinden sich in `infrastructure/environments/`:
|
|
||||||
|
|
||||||
- `development.env` - Entwicklungsumgebung
|
|
||||||
- `production.env` - Produktionsumgebung
|
|
||||||
- `test.env` - Testumgebung
|
|
||||||
|
|
||||||
### Drucker-Konfiguration
|
|
||||||
|
|
||||||
Drucker werden über die `PRINTERS` Umgebungsvariable konfiguriert:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Drucker 1": {
|
|
||||||
"ip": "192.168.0.100",
|
|
||||||
"model": "UltiMaker S5",
|
|
||||||
"location": "Raum A.1.01"
|
|
||||||
},
|
|
||||||
"Drucker 2": {
|
|
||||||
"ip": "192.168.0.101",
|
|
||||||
"model": "UltiMaker S5",
|
|
||||||
"location": "Raum A.1.02"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### TAPO Smart Plug Integration
|
|
||||||
|
|
||||||
```env
|
|
||||||
TAPO_USERNAME=your-email@example.com
|
|
||||||
TAPO_PASSWORD=your-secure-password
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSL/TLS-Konfiguration (Produktion)
|
|
||||||
|
|
||||||
```env
|
|
||||||
CADDY_DOMAIN=your-domain.com
|
|
||||||
CADDY_EMAIL=admin@your-domain.com
|
|
||||||
SSL_ENABLED=true
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🧹 Wartung
|
|
||||||
|
|
||||||
### System bereinigen
|
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
```powershell
|
|
||||||
# Interaktive Bereinigung
|
|
||||||
.\cleanup.ps1
|
|
||||||
|
|
||||||
# Automatische Bereinigung
|
|
||||||
.\cleanup.ps1 -Force
|
|
||||||
|
|
||||||
# Vollständige Bereinigung (inkl. Volumes)
|
|
||||||
.\cleanup.ps1 -All -Force
|
|
||||||
```
|
|
||||||
|
|
||||||
**Linux/macOS:**
|
|
||||||
```bash
|
|
||||||
# Interaktive Bereinigung
|
|
||||||
./cleanup.sh
|
|
||||||
|
|
||||||
# Automatische Bereinigung
|
|
||||||
./cleanup.sh --force
|
|
||||||
|
|
||||||
# Vollständige Bereinigung (inkl. Volumes)
|
|
||||||
./cleanup.sh --all --force
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logs anzeigen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Alle Services
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Spezifische Services
|
|
||||||
docker-compose logs -f backend
|
|
||||||
docker-compose logs -f frontend
|
|
||||||
docker-compose logs -f caddy
|
|
||||||
|
|
||||||
# Mit Zeitstempel
|
|
||||||
docker-compose logs -f -t
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container-Status prüfen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Status aller Container
|
|
||||||
docker-compose ps
|
|
||||||
|
|
||||||
# Detaillierte Informationen
|
|
||||||
docker-compose top
|
|
||||||
|
|
||||||
# Ressourcenverbrauch
|
|
||||||
docker stats
|
|
||||||
|
|
||||||
# Health Checks
|
|
||||||
docker-compose exec backend curl -f http://localhost:5000/health
|
|
||||||
docker-compose exec frontend curl -f http://localhost:3000/api/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup & Restore
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Datenbank-Backup erstellen
|
|
||||||
docker-compose exec backend python -c "
|
|
||||||
import sqlite3
|
|
||||||
import shutil
|
|
||||||
shutil.copy('instance/myp.db', 'instance/backup_$(date +%Y%m%d_%H%M%S).db')
|
|
||||||
"
|
|
||||||
|
|
||||||
# Logs archivieren
|
|
||||||
tar -czf logs_backup_$(date +%Y%m%d_%H%M%S).tar.gz logs/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 Sicherheit
|
|
||||||
|
|
||||||
### Produktionsumgebung
|
|
||||||
|
|
||||||
- **SSL/TLS**: Automatische Let's Encrypt-Zertifikate
|
|
||||||
- **Security Headers**: HSTS, CSP, X-Frame-Options
|
|
||||||
- **Rate Limiting**: API-Endpunkt-Schutz
|
|
||||||
- **Authentication**: JWT-basierte Authentifizierung
|
|
||||||
- **Input Validation**: Umfassende Eingabevalidierung
|
|
||||||
- **CORS**: Konfigurierbare Cross-Origin-Richtlinien
|
|
||||||
|
|
||||||
### Geheimnisse verwalten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Sichere Umgebungsvariablen setzen
|
|
||||||
echo "SECRET_KEY=$(openssl rand -hex 32)" >> infrastructure/environments/production.env
|
|
||||||
echo "JWT_SECRET=$(openssl rand -hex 32)" >> infrastructure/environments/production.env
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚨 Fehlerbehebung
|
|
||||||
|
|
||||||
### Häufige Probleme
|
|
||||||
|
|
||||||
**Docker-Container starten nicht:**
|
|
||||||
```bash
|
|
||||||
# Docker-Status prüfen
|
|
||||||
docker info
|
|
||||||
|
|
||||||
# Container-Logs anzeigen
|
|
||||||
docker-compose logs
|
|
||||||
|
|
||||||
# Ports prüfen
|
|
||||||
netstat -tulpn | grep :80
|
|
||||||
netstat -tulpn | grep :3000
|
|
||||||
netstat -tulpn | grep :5000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend kann Backend nicht erreichen:**
|
|
||||||
```bash
|
|
||||||
# Netzwerk-Konfiguration prüfen
|
|
||||||
docker network ls
|
|
||||||
docker network inspect projektarbeit-myp_myp-network
|
|
||||||
|
|
||||||
# API-Erreichbarkeit testen
|
|
||||||
curl -f http://localhost/api/health
|
|
||||||
```
|
|
||||||
|
|
||||||
**Datenbank-Probleme:**
|
|
||||||
```bash
|
|
||||||
# Datenbank-Integrität prüfen
|
|
||||||
docker-compose exec backend python -c "
|
|
||||||
import sqlite3
|
|
||||||
conn = sqlite3.connect('instance/myp.db')
|
|
||||||
conn.execute('PRAGMA integrity_check;')
|
|
||||||
print(conn.fetchall())
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Support
|
|
||||||
|
|
||||||
Bei Problemen:
|
|
||||||
1. Prüfen Sie die Logs: `docker-compose logs`
|
|
||||||
2. Überprüfen Sie die Systemvoraussetzungen
|
|
||||||
3. Führen Sie eine Bereinigung durch: `./cleanup.sh --force`
|
|
||||||
4. Starten Sie das System neu: `./start.sh`
|
|
||||||
|
|
||||||
## 📈 Performance
|
|
||||||
|
|
||||||
### Monitoring
|
|
||||||
|
|
||||||
- **Prometheus**: http://localhost:9090 (nur Entwicklung)
|
|
||||||
- **Grafana**: http://localhost:3001 (nur Entwicklung)
|
|
||||||
- **Caddy Admin**: http://localhost:2019
|
|
||||||
|
|
||||||
### Optimierungen
|
|
||||||
|
|
||||||
- **Container-Images**: Multi-Stage-Builds für minimale Größe
|
|
||||||
- **Caching**: Redis für Session-Management
|
|
||||||
- **CDN**: Statische Assets über Caddy
|
|
||||||
- **Database**: SQLite mit WAL-Modus für bessere Performance
|
|
||||||
|
|
||||||
## 🤝 Beitragen
|
|
||||||
|
|
||||||
1. Fork des Repositories erstellen
|
|
||||||
2. Feature-Branch erstellen: `git checkout -b feature/neue-funktion`
|
|
||||||
3. Änderungen committen: `git commit -am 'Neue Funktion hinzufügen'`
|
|
||||||
4. Branch pushen: `git push origin feature/neue-funktion`
|
|
||||||
5. Pull Request erstellen
|
|
||||||
|
|
||||||
## 📄 Lizenz
|
|
||||||
|
|
||||||
Dieses Projekt steht unter der MIT-Lizenz. Siehe [LICENSE.md](LICENSE.md) für Details.
|
|
||||||
|
|
||||||
## 🙏 Danksagungen
|
|
||||||
|
|
||||||
- **TBA Werk 040** - Für die Unterstützung und Anforderungen
|
|
||||||
- **Open Source Community** - Für die verwendeten Technologien
|
|
||||||
- **Docker Team** - Für die Container-Technologie
|
|
||||||
- **Next.js Team** - Für das Frontend-Framework
|
|
||||||
- **Flask Team** - Für das Backend-Framework
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Entwickelt mit ❤️ für die TBA im Werk 040, Berlin-Marienfelde**
|
|
@ -1,436 +0,0 @@
|
|||||||
# 🏗️ MYP - Separate Server Architektur
|
|
||||||
|
|
||||||
## Übersicht
|
|
||||||
|
|
||||||
Das MYP-System wurde in **zwei vollständig unabhängige Server** aufgeteilt:
|
|
||||||
|
|
||||||
- **🏭 Backend-Server** (Port 5000): Flask-API für Geschäftslogik und Datenmanagement
|
|
||||||
- **🎨 Frontend-Server** (Port 3000): Next.js-Anwendung für Benutzeroberfläche
|
|
||||||
|
|
||||||
## 🔗 Server-Kommunikation
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐ HTTP/API ┌─────────────────┐
|
|
||||||
│ Frontend │◄───────────────►│ Backend │
|
|
||||||
│ (Next.js) │ │ (Flask) │
|
|
||||||
│ Port: 3000 │ │ Port: 5000 │
|
|
||||||
└─────────────────┘ └─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Separate Server starten
|
|
||||||
|
|
||||||
### Backend-Server (unabhängig)
|
|
||||||
|
|
||||||
#### **Windows (PowerShell)**
|
|
||||||
```powershell
|
|
||||||
cd backend
|
|
||||||
.\install.ps1 -Production # Installation
|
|
||||||
.\start-backend-server.ps1 -Development # Entwicklung
|
|
||||||
.\start-backend-server.ps1 -Production # Produktion
|
|
||||||
.\start-backend-server.ps1 -Development -Logs # Mit Live-Logs
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Linux/macOS (Bash)**
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
./install.sh --production # Installation
|
|
||||||
./start-backend-server.sh --development # Entwicklung
|
|
||||||
./start-backend-server.sh --production # Produktion
|
|
||||||
./start-backend-server.sh --development --logs # Mit Live-Logs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend-Server (unabhängig)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install # Dependencies installieren
|
|
||||||
npm run dev # Entwicklungsserver
|
|
||||||
npm run build && npm start # Produktionsserver
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Konfiguration
|
|
||||||
|
|
||||||
### Backend-Konfiguration (`backend/env.backend`)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# === FLASK KONFIGURATION ===
|
|
||||||
FLASK_APP=app.py
|
|
||||||
FLASK_ENV=production
|
|
||||||
PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
# === DATENBANK ===
|
|
||||||
DATABASE_PATH=instance/myp.db
|
|
||||||
|
|
||||||
# === SICHERHEIT ===
|
|
||||||
SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F
|
|
||||||
JWT_SECRET=secure-jwt-secret-backend-2024
|
|
||||||
|
|
||||||
# === CORS KONFIGURATION ===
|
|
||||||
CORS_ORIGINS=http://localhost:3000,https://frontend.myp.local
|
|
||||||
|
|
||||||
# === DRUCKER KONFIGURATION ===
|
|
||||||
PRINTERS={"Drucker 1": {"ip": "192.168.0.100"}, "Drucker 2": {"ip": "192.168.0.101"}}
|
|
||||||
|
|
||||||
# === TAPO SMART PLUG ===
|
|
||||||
TAPO_USERNAME=your.email@company.com
|
|
||||||
TAPO_PASSWORD=your_password
|
|
||||||
|
|
||||||
# === NETZWERK ===
|
|
||||||
HOST=0.0.0.0
|
|
||||||
PORT=5000
|
|
||||||
BACKEND_URL=http://localhost:5000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend-Konfiguration (`frontend/env.frontend`)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# === NODE.JS KONFIGURATION ===
|
|
||||||
NODE_ENV=production
|
|
||||||
NEXT_TELEMETRY_DISABLED=1
|
|
||||||
|
|
||||||
# === FRONTEND SERVER ===
|
|
||||||
PORT=3000
|
|
||||||
HOSTNAME=0.0.0.0
|
|
||||||
FRONTEND_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# === BACKEND API KONFIGURATION ===
|
|
||||||
BACKEND_API_URL=http://localhost:5000/api
|
|
||||||
BACKEND_HOST=localhost:5000
|
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:5000/api
|
|
||||||
NEXT_PUBLIC_BACKEND_HOST=localhost:5000
|
|
||||||
|
|
||||||
# === AUTHENTIFIZIERUNG ===
|
|
||||||
NEXTAUTH_URL=http://localhost:3000
|
|
||||||
NEXTAUTH_SECRET=frontend-auth-secret-2024
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Entwicklung
|
|
||||||
|
|
||||||
### Beide Server parallel starten
|
|
||||||
|
|
||||||
#### **Windows**
|
|
||||||
```powershell
|
|
||||||
# Terminal 1: Backend
|
|
||||||
cd backend
|
|
||||||
.\start-backend-server.ps1 -Development -Logs
|
|
||||||
|
|
||||||
# Terminal 2: Frontend
|
|
||||||
cd frontend
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Linux/macOS**
|
|
||||||
```bash
|
|
||||||
# Terminal 1: Backend
|
|
||||||
cd backend
|
|
||||||
./start-backend-server.sh --development --logs
|
|
||||||
|
|
||||||
# Terminal 2: Frontend
|
|
||||||
cd frontend
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### API-Endpoints testen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend Health-Check
|
|
||||||
curl http://localhost:5000/monitoring/health/simple
|
|
||||||
|
|
||||||
# Backend API Test
|
|
||||||
curl http://localhost:5000/api/test
|
|
||||||
|
|
||||||
# Frontend Health-Check
|
|
||||||
curl http://localhost:3000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🏭 Produktion
|
|
||||||
|
|
||||||
### Docker-Deployment
|
|
||||||
|
|
||||||
#### **Backend-Container**
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
docker-compose -f docker-compose.backend.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Frontend-Container**
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
docker-compose -f docker-compose.frontend.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Native Deployment
|
|
||||||
|
|
||||||
#### **Backend (Linux/Ubuntu)**
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
./install.sh --production
|
|
||||||
|
|
||||||
# Systemd-Service
|
|
||||||
sudo systemctl start myp-backend
|
|
||||||
sudo systemctl enable myp-backend
|
|
||||||
sudo systemctl status myp-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Frontend (mit PM2)**
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install -g pm2
|
|
||||||
npm run build
|
|
||||||
pm2 start npm --name "myp-frontend" -- start
|
|
||||||
pm2 save
|
|
||||||
pm2 startup
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 Sicherheit
|
|
||||||
|
|
||||||
### CORS-Konfiguration
|
|
||||||
|
|
||||||
Das Backend ist konfiguriert, um nur Anfragen von autorisierten Frontend-Domains zu akzeptieren:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# backend/app.py
|
|
||||||
CORS(app,
|
|
||||||
origins=['http://localhost:3000', 'https://frontend.myp.local'],
|
|
||||||
supports_credentials=True,
|
|
||||||
allow_headers=['Content-Type', 'Authorization', 'X-Requested-With'],
|
|
||||||
methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
|
|
||||||
```
|
|
||||||
|
|
||||||
### Umgebungsvariablen
|
|
||||||
|
|
||||||
- Verwenden Sie sichere, zufällige Secret Keys
|
|
||||||
- Lagern Sie Passwörter nie im Code
|
|
||||||
- Nutzen Sie unterschiedliche Secrets für verschiedene Umgebungen
|
|
||||||
|
|
||||||
## 📊 Monitoring
|
|
||||||
|
|
||||||
### Health-Checks
|
|
||||||
|
|
||||||
| Service | Endpoint | Port | Beschreibung |
|
|
||||||
|----------|----------|------|--------------|
|
|
||||||
| Backend | `/monitoring/health/simple` | 5000 | Einfacher Health-Check |
|
|
||||||
| Backend | `/monitoring/health` | 5000 | Detaillierter Health-Check |
|
|
||||||
| Frontend | `/health` | 3000 | Frontend-Health-Check |
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
|
|
||||||
#### **Backend-Logs**
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
Get-Content backend\logs\myp.log -Wait
|
|
||||||
|
|
||||||
# Linux/macOS
|
|
||||||
tail -f backend/logs/myp.log
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Frontend-Logs**
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
npm run dev # Logs in der Konsole
|
|
||||||
|
|
||||||
# Production mit PM2
|
|
||||||
pm2 logs myp-frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Troubleshooting
|
|
||||||
|
|
||||||
### Häufige Probleme
|
|
||||||
|
|
||||||
#### **Backend startet nicht**
|
|
||||||
```bash
|
|
||||||
# 1. Python-Version prüfen
|
|
||||||
python --version # Mindestens 3.8 erforderlich
|
|
||||||
|
|
||||||
# 2. Dependencies prüfen
|
|
||||||
cd backend
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# 3. Test-Skript ausführen
|
|
||||||
python test-backend-setup.py
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Frontend startet nicht**
|
|
||||||
```bash
|
|
||||||
# 1. Node-Version prüfen
|
|
||||||
node --version # Mindestens 18 erforderlich
|
|
||||||
|
|
||||||
# 2. Dependencies installieren
|
|
||||||
cd frontend
|
|
||||||
rm -rf node_modules package-lock.json
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 3. Build-Fehler beheben
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **CORS-Fehler**
|
|
||||||
```bash
|
|
||||||
# Backend CORS-Origins in env.backend prüfen
|
|
||||||
CORS_ORIGINS=http://localhost:3000,https://your-frontend-domain.com
|
|
||||||
|
|
||||||
# Frontend API-URL in env.frontend prüfen
|
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:5000/api
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug-Modus
|
|
||||||
|
|
||||||
#### **Backend Debug**
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
.\start-backend-server.ps1 -Development -Logs
|
|
||||||
|
|
||||||
# Linux/macOS
|
|
||||||
./start-backend-server.sh --development --logs
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Frontend Debug**
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm run dev # Automatisches Reload bei Änderungen
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 Reverse Proxy (Produktionsempfehlung)
|
|
||||||
|
|
||||||
### Nginx-Konfiguration
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# /etc/nginx/sites-available/myp
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name your-domain.com;
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backend API
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://localhost:5000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backend Health-Check
|
|
||||||
location /monitoring {
|
|
||||||
proxy_pass http://localhost:5000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSL-Konfiguration mit Certbot
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# SSL-Zertifikat installieren
|
|
||||||
sudo certbot --nginx -d your-domain.com
|
|
||||||
|
|
||||||
# Automatische Erneuerung
|
|
||||||
sudo crontab -e
|
|
||||||
# Füge hinzu: 0 12 * * * /usr/bin/certbot renew --quiet
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 Performance-Optimierung
|
|
||||||
|
|
||||||
### Backend-Optimierung
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Gunicorn-Worker anpassen (env.backend)
|
|
||||||
WORKERS=4 # CPU-Kerne * 2
|
|
||||||
TIMEOUT=30 # Request-Timeout
|
|
||||||
MAX_REQUESTS=1000 # Requests pro Worker
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend-Optimierung
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build-Optimierung
|
|
||||||
cd frontend
|
|
||||||
npm run build # Produktions-Build
|
|
||||||
npm run analyze # Bundle-Analyse
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 CI/CD Pipeline
|
|
||||||
|
|
||||||
### GitHub Actions Beispiel
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# .github/workflows/deploy.yml
|
|
||||||
name: Deploy MYP
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy-backend:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Deploy Backend
|
|
||||||
run: |
|
|
||||||
cd backend
|
|
||||||
./install.sh --production
|
|
||||||
sudo systemctl restart myp-backend
|
|
||||||
|
|
||||||
deploy-frontend:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Deploy Frontend
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
pm2 restart myp-frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Wartung
|
|
||||||
|
|
||||||
### Regelmäßige Aufgaben
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Logs rotieren
|
|
||||||
sudo logrotate -f /etc/logrotate.d/myp
|
|
||||||
|
|
||||||
# Dependencies aktualisieren
|
|
||||||
cd backend && pip install -r requirements.txt --upgrade
|
|
||||||
cd frontend && npm update
|
|
||||||
|
|
||||||
# Datenbank-Backup
|
|
||||||
cd backend && cp instance/myp.db backups/myp-$(date +%Y%m%d).db
|
|
||||||
|
|
||||||
# Health-Check-Monitoring
|
|
||||||
curl -f http://localhost:5000/monitoring/health/simple || echo "Backend down"
|
|
||||||
curl -f http://localhost:3000/health || echo "Frontend down"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
### Kontakt
|
|
||||||
- **Entwickler**: Till Tomczak
|
|
||||||
- **E-Mail**: till.tomczak@mercedes-benz.com
|
|
||||||
- **Repository**: https://github.com/your-org/myp
|
|
||||||
|
|
||||||
### Dokumentation
|
|
||||||
- **Backend-API**: http://localhost:5000/docs (falls Swagger aktiviert)
|
|
||||||
- **Frontend-Dokumentation**: Siehe `frontend/README.md`
|
|
||||||
- **System-Architektur**: Siehe `docs/ARCHITECTURE.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Hinweis**: Diese Dokumentation beschreibt die neue separate Server-Architektur. Die alte gekoppelte Architektur wird nicht mehr unterstützt.
|
|
@ -1,5 +0,0 @@
|
|||||||
SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F
|
|
||||||
DATABASE_PATH=instance/myp.db
|
|
||||||
TAPO_USERNAME=till.tomczak@mercedes-benz.com
|
|
||||||
TAPO_PASSWORD=744563017196A
|
|
||||||
PRINTERS={"Printer 1": {"ip": "192.168.0.100"}, "Printer 2": {"ip": "192.168.0.101"}, "Printer 3": {"ip": "192.168.0.102"}, "Printer 4": {"ip": "192.168.0.103"}, "Printer 5": {"ip": "192.168.0.104"}, "Printer 6": {"ip": "192.168.0.106"}}
|
|
48
backend/.gitignore
vendored
48
backend/.gitignore
vendored
@ -1,48 +0,0 @@
|
|||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
*.so
|
|
||||||
.Python
|
|
||||||
env/
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# Flask
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs/
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# SQLite Datenbank-Dateien
|
|
||||||
*.db
|
|
||||||
*.db-journal
|
|
||||||
|
|
||||||
# Virtuelle Umgebungen
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Betriebssystem
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
375
backend/COMMON_ERRORS.md
Normal file
375
backend/COMMON_ERRORS.md
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
# MYP V2 - Häufige Fehler und Lösungen
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
Diese Datei dokumentiert häufige Fehler, die während der Entwicklung und dem Betrieb von MYP V2 auftreten können, sowie deren Lösungen.
|
||||||
|
|
||||||
|
## 🔧 Setup und Installation
|
||||||
|
|
||||||
|
### Fehler: ModuleNotFoundError für PyP100
|
||||||
|
**Symptom**: `ModuleNotFoundError: No module named 'PyP100'`
|
||||||
|
|
||||||
|
**Ursache**: PyP100-Bibliothek ist nicht installiert
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
pip3.11 install PyP100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Datenbankverbindung fehlgeschlagen
|
||||||
|
**Symptom**: `sqlite3.OperationalError: unable to open database file`
|
||||||
|
|
||||||
|
**Ursache**:
|
||||||
|
- Fehlende Schreibberechtigung im Datenbank-Verzeichnis
|
||||||
|
- Verzeichnis existiert nicht
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Verzeichnis erstellen
|
||||||
|
mkdir -p /path/to/database/directory
|
||||||
|
# Berechtigungen setzen
|
||||||
|
chmod 755 /path/to/database/directory
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Log-Verzeichnis nicht gefunden
|
||||||
|
**Symptom**: `FileNotFoundError: [Errno 2] No such file or directory: 'logs/app/app.log'`
|
||||||
|
|
||||||
|
**Ursache**: Log-Verzeichnisse wurden nicht erstellt
|
||||||
|
|
||||||
|
**Lösung**: Die `ensure_log_directories()` Funktion in `logging_config.py` wird automatisch aufgerufen
|
||||||
|
|
||||||
|
## 🔐 Authentifizierung und Autorisierung
|
||||||
|
|
||||||
|
### Fehler: Session-Timeout zu kurz
|
||||||
|
**Symptom**: Benutzer werden zu häufig abgemeldet
|
||||||
|
|
||||||
|
**Ursache**: `SESSION_LIFETIME` in `settings.py` zu niedrig eingestellt
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```python
|
||||||
|
# In config/settings.py
|
||||||
|
SESSION_LIFETIME = timedelta(days=7) # Oder gewünschte Dauer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Admin-Rechte nicht erkannt
|
||||||
|
**Symptom**: `AttributeError: 'AnonymousUserMixin' object has no attribute 'is_admin'`
|
||||||
|
|
||||||
|
**Ursache**: Benutzer ist nicht angemeldet oder UserMixin-Objekt falsch erstellt
|
||||||
|
|
||||||
|
**Lösung**: Prüfung in Decorators verbessern:
|
||||||
|
```python
|
||||||
|
if not current_user.is_authenticated or not hasattr(current_user, 'is_admin') or not current_user.is_admin:
|
||||||
|
return jsonify({"error": "Keine Berechtigung"}), 403
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Passwort-Hash-Fehler
|
||||||
|
**Symptom**: `ValueError: Invalid salt`
|
||||||
|
|
||||||
|
**Ursache**: Inkonsistente Passwort-Hash-Methoden
|
||||||
|
|
||||||
|
**Lösung**: Einheitliche Verwendung von `werkzeug.security`:
|
||||||
|
```python
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🖨️ Drucker und Smart Plug-Steuerung
|
||||||
|
|
||||||
|
### Fehler: Tapo-Verbindung fehlgeschlagen
|
||||||
|
**Symptom**: `Exception: Failed to establish a new connection`
|
||||||
|
|
||||||
|
**Ursache**:
|
||||||
|
- Falsche IP-Adresse
|
||||||
|
- Netzwerkprobleme
|
||||||
|
- Falsche Credentials
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. IP-Adresse in `PRINTERS` Konfiguration prüfen
|
||||||
|
2. Netzwerkverbindung testen: `ping <ip-address>`
|
||||||
|
3. Credentials in `settings.py` überprüfen
|
||||||
|
|
||||||
|
### Fehler: Plug-Status kann nicht abgerufen werden
|
||||||
|
**Symptom**: `KeyError: 'device_on'`
|
||||||
|
|
||||||
|
**Ursache**: Unerwartete API-Antwort von Tapo-Gerät
|
||||||
|
|
||||||
|
**Lösung**: Defensive Programmierung:
|
||||||
|
```python
|
||||||
|
plug_state = plug_info.get("device_on", False)
|
||||||
|
power_consumption = plug_info.get("current_power", 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Drucker nicht in Konfiguration gefunden
|
||||||
|
**Symptom**: `Drucker nicht in Konfiguration gefunden`
|
||||||
|
|
||||||
|
**Ursache**: Drucker-Name stimmt nicht mit `PRINTERS` Dictionary überein
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Verfügbare Drucker in `settings.py` prüfen
|
||||||
|
2. Exakte Schreibweise verwenden
|
||||||
|
3. Neue Drucker zur Konfiguration hinzufügen
|
||||||
|
|
||||||
|
## 📅 Job-Management
|
||||||
|
|
||||||
|
### Fehler: Überlappende Jobs nicht erkannt
|
||||||
|
**Symptom**: Mehrere Jobs laufen gleichzeitig auf einem Drucker
|
||||||
|
|
||||||
|
**Ursache**: Fehlerhafte Überlappungsprüfung in der Datenbank-Abfrage
|
||||||
|
|
||||||
|
**Lösung**: Korrekte SQL-Abfrage verwenden:
|
||||||
|
```python
|
||||||
|
overlapping_jobs = db_session.query(Job).filter(
|
||||||
|
Job.printer_id == printer_id,
|
||||||
|
Job.status.in_(["scheduled", "active"]),
|
||||||
|
((Job.start_time <= start_time) & (Job.end_time > start_time)) |
|
||||||
|
((Job.start_time < end_time) & (Job.end_time >= end_time)) |
|
||||||
|
((Job.start_time >= start_time) & (Job.end_time <= end_time))
|
||||||
|
).count()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Datumsformat-Parsing fehlgeschlagen
|
||||||
|
**Symptom**: `ValueError: time data does not match format`
|
||||||
|
|
||||||
|
**Ursache**: Inkonsistente Datumsformate zwischen Frontend und Backend
|
||||||
|
|
||||||
|
**Lösung**: ISO-Format verwenden:
|
||||||
|
```python
|
||||||
|
start_time = datetime.fromisoformat(data["start_time"].replace("Z", "+00:00"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Job-Status nicht aktualisiert
|
||||||
|
**Symptom**: Jobs bleiben im "scheduled" Status obwohl sie laufen sollten
|
||||||
|
|
||||||
|
**Ursache**:
|
||||||
|
- Scheduler läuft nicht
|
||||||
|
- Fehler im Job-Monitor
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Scheduler-Status prüfen: `GET /api/scheduler/status`
|
||||||
|
2. Logs überprüfen: `logs/scheduler/scheduler.log`
|
||||||
|
3. Scheduler neu starten: `POST /api/scheduler/start`
|
||||||
|
|
||||||
|
## 🗄️ Datenbank-Probleme
|
||||||
|
|
||||||
|
### Fehler: Foreign Key Constraint
|
||||||
|
**Symptom**: `sqlite3.IntegrityError: FOREIGN KEY constraint failed`
|
||||||
|
|
||||||
|
**Ursache**: Versuch, referenzierte Datensätze zu löschen
|
||||||
|
|
||||||
|
**Lösung**: Abhängigkeiten vor dem Löschen prüfen:
|
||||||
|
```python
|
||||||
|
# Vor dem Löschen eines Druckers
|
||||||
|
active_jobs = db_session.query(Job).filter(
|
||||||
|
Job.printer_id == printer_id,
|
||||||
|
Job.status.in_(["scheduled", "active"])
|
||||||
|
).count()
|
||||||
|
|
||||||
|
if active_jobs > 0:
|
||||||
|
return jsonify({"error": "Es existieren aktive Jobs für diesen Drucker"}), 400
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Datenbank-Session nicht geschlossen
|
||||||
|
**Symptom**: `ResourceWarning: unclosed <sqlite3.Connection>`
|
||||||
|
|
||||||
|
**Ursache**: Vergessene `db_session.close()` Aufrufe
|
||||||
|
|
||||||
|
**Lösung**: Immer `try/finally` verwenden:
|
||||||
|
```python
|
||||||
|
db_session = get_db_session()
|
||||||
|
try:
|
||||||
|
# Datenbankoperationen
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
db_session.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Unique Constraint Violation
|
||||||
|
**Symptom**: `sqlite3.IntegrityError: UNIQUE constraint failed`
|
||||||
|
|
||||||
|
**Ursache**: Versuch, doppelte Einträge zu erstellen
|
||||||
|
|
||||||
|
**Lösung**: Vor dem Einfügen prüfen:
|
||||||
|
```python
|
||||||
|
existing = db_session.query(User).filter(User.email == email).first()
|
||||||
|
if existing:
|
||||||
|
return jsonify({"error": "E-Mail bereits registriert"}), 400
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Logging und Monitoring
|
||||||
|
|
||||||
|
### Fehler: Log-Rotation funktioniert nicht
|
||||||
|
**Symptom**: Log-Dateien werden sehr groß
|
||||||
|
|
||||||
|
**Ursache**: `RotatingFileHandler` nicht korrekt konfiguriert
|
||||||
|
|
||||||
|
**Lösung**: Konfiguration in `logging_config.py` prüfen:
|
||||||
|
```python
|
||||||
|
handler = RotatingFileHandler(
|
||||||
|
log_file,
|
||||||
|
maxBytes=10*1024*1024, # 10MB
|
||||||
|
backupCount=5
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Logger schreibt nicht in Datei
|
||||||
|
**Symptom**: Keine Log-Einträge in den Dateien
|
||||||
|
|
||||||
|
**Ursache**:
|
||||||
|
- Log-Level zu hoch eingestellt
|
||||||
|
- Handler nicht korrekt hinzugefügt
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Log-Level prüfen: `logger.setLevel(logging.INFO)`
|
||||||
|
2. Handler hinzufügen: `logger.addHandler(handler)`
|
||||||
|
|
||||||
|
### Fehler: Doppelte Log-Einträge
|
||||||
|
**Symptom**: Jeder Log-Eintrag erscheint mehrfach
|
||||||
|
|
||||||
|
**Ursache**: Logger-Vererbung oder mehrfache Handler-Registrierung
|
||||||
|
|
||||||
|
**Lösung**: `propagate` deaktivieren:
|
||||||
|
```python
|
||||||
|
logger.propagate = False
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Scheduler-Probleme
|
||||||
|
|
||||||
|
### Fehler: Scheduler startet nicht
|
||||||
|
**Symptom**: `scheduler.is_running()` gibt `False` zurück
|
||||||
|
|
||||||
|
**Ursache**:
|
||||||
|
- `SCHEDULER_ENABLED = False` in Konfiguration
|
||||||
|
- Fehler beim Task-Registrieren
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Konfiguration prüfen: `SCHEDULER_ENABLED = True`
|
||||||
|
2. Task-Registrierung überprüfen
|
||||||
|
3. Logs analysieren: `logs/scheduler/scheduler.log`
|
||||||
|
|
||||||
|
### Fehler: Tasks werden nicht ausgeführt
|
||||||
|
**Symptom**: Job-Monitor läuft nicht automatisch
|
||||||
|
|
||||||
|
**Ursache**:
|
||||||
|
- Task nicht korrekt registriert
|
||||||
|
- Fehler in der Task-Funktion
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Task-Status prüfen: `scheduler.get_task_info()`
|
||||||
|
2. Task-Funktion auf Fehler prüfen
|
||||||
|
3. Interval-Einstellungen überprüfen
|
||||||
|
|
||||||
|
### Fehler: Scheduler-Thread blockiert
|
||||||
|
**Symptom**: Anwendung reagiert nicht mehr
|
||||||
|
|
||||||
|
**Ursache**: Endlosschleife oder blockierende Operation in Task
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Timeout für externe Operationen setzen
|
||||||
|
2. Exception-Handling in Tasks verbessern
|
||||||
|
3. Scheduler neu starten
|
||||||
|
|
||||||
|
## 🌐 API und HTTP-Probleme
|
||||||
|
|
||||||
|
### Fehler: CORS-Probleme
|
||||||
|
**Symptom**: `Access-Control-Allow-Origin` Fehler im Browser
|
||||||
|
|
||||||
|
**Ursache**: Frontend und Backend auf verschiedenen Ports
|
||||||
|
|
||||||
|
**Lösung**: Flask-CORS installieren und konfigurieren:
|
||||||
|
```python
|
||||||
|
from flask_cors import CORS
|
||||||
|
CORS(app)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: 500 Internal Server Error
|
||||||
|
**Symptom**: Unspezifische Server-Fehler
|
||||||
|
|
||||||
|
**Ursache**: Unbehandelte Exceptions
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Debug-Modus aktivieren: `FLASK_DEBUG = True`
|
||||||
|
2. Logs überprüfen
|
||||||
|
3. Try-Catch-Blöcke erweitern
|
||||||
|
|
||||||
|
### Fehler: JSON-Serialisierung fehlgeschlagen
|
||||||
|
**Symptom**: `TypeError: Object of type datetime is not JSON serializable`
|
||||||
|
|
||||||
|
**Ursache**: Datetime-Objekte in JSON-Response
|
||||||
|
|
||||||
|
**Lösung**: `.isoformat()` verwenden:
|
||||||
|
```python
|
||||||
|
"created_at": obj.created_at.isoformat() if obj.created_at else None
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Performance-Probleme
|
||||||
|
|
||||||
|
### Fehler: Langsame Datenbankabfragen
|
||||||
|
**Symptom**: API-Responses dauern sehr lange
|
||||||
|
|
||||||
|
**Ursache**: Fehlende Indizes oder ineffiziente Abfragen
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Indizes auf häufig abgefragte Spalten erstellen
|
||||||
|
2. Query-Optimierung mit `EXPLAIN QUERY PLAN`
|
||||||
|
3. Eager Loading für Beziehungen verwenden
|
||||||
|
|
||||||
|
### Fehler: Speicher-Leaks
|
||||||
|
**Symptom**: Speicherverbrauch steigt kontinuierlich
|
||||||
|
|
||||||
|
**Ursache**:
|
||||||
|
- Nicht geschlossene Datenbankverbindungen
|
||||||
|
- Zirkuläre Referenzen
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Connection-Pooling implementieren
|
||||||
|
2. Regelmäßige Garbage Collection
|
||||||
|
3. Memory-Profiling mit `memory_profiler`
|
||||||
|
|
||||||
|
## 🛠️ Entwicklungsumgebung
|
||||||
|
|
||||||
|
### Fehler: Import-Fehler in Tests
|
||||||
|
**Symptom**: `ModuleNotFoundError` beim Ausführen von Tests
|
||||||
|
|
||||||
|
**Ursache**: PYTHONPATH nicht korrekt gesetzt
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
export PYTHONPATH="${PYTHONPATH}:$(pwd)"
|
||||||
|
python3.11 -m pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: Konfigurationsdateien nicht gefunden
|
||||||
|
**Symptom**: `FileNotFoundError` für `settings.py`
|
||||||
|
|
||||||
|
**Ursache**: Arbeitsverzeichnis stimmt nicht
|
||||||
|
|
||||||
|
**Lösung**: Relative Pfade verwenden oder Arbeitsverzeichnis setzen:
|
||||||
|
```bash
|
||||||
|
cd /path/to/MYP_V2
|
||||||
|
python3.11 app/app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Checkliste für Fehlerbehebung
|
||||||
|
|
||||||
|
### Vor jeder Änderung:
|
||||||
|
1. [ ] Aktuelle Logs überprüfen
|
||||||
|
2. [ ] Datenbank-Backup erstellen
|
||||||
|
3. [ ] Konfiguration validieren
|
||||||
|
4. [ ] Tests ausführen (falls vorhanden)
|
||||||
|
|
||||||
|
### Nach jeder Änderung:
|
||||||
|
1. [ ] Funktionalität testen
|
||||||
|
2. [ ] Logs auf neue Fehler prüfen
|
||||||
|
3. [ ] Performance-Impact bewerten
|
||||||
|
4. [ ] Dokumentation aktualisieren
|
||||||
|
|
||||||
|
### Bei kritischen Fehlern:
|
||||||
|
1. [ ] Service stoppen
|
||||||
|
2. [ ] Fehlerursache identifizieren
|
||||||
|
3. [ ] Rollback-Plan erstellen
|
||||||
|
4. [ ] Fix implementieren und testen
|
||||||
|
5. [ ] Service neu starten
|
||||||
|
6. [ ] Monitoring für 24h verstärken
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Letzte Aktualisierung**: Dezember 2024
|
||||||
|
**Hinweis**: Diese Datei sollte bei jedem neuen Fehler aktualisiert werden.
|
@ -1,52 +0,0 @@
|
|||||||
FROM python:slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install system dependencies (curl, sqlite3 for database, wget for healthcheck)
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
sqlite3 \
|
|
||||||
wget \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
# Install Python dependencies
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Copy application code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Create required directories
|
|
||||||
RUN mkdir -p logs instance
|
|
||||||
|
|
||||||
ENV FLASK_APP=app.py
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
# Add health check endpoint
|
|
||||||
RUN echo 'from flask import Blueprint\n\
|
|
||||||
health_bp = Blueprint("health", __name__)\n\
|
|
||||||
\n\
|
|
||||||
@health_bp.route("/health")\n\
|
|
||||||
def health_check():\n\
|
|
||||||
return {"status": "healthy"}, 200\n'\
|
|
||||||
> /app/health.py
|
|
||||||
|
|
||||||
# Add the health blueprint to app.py if it doesn't exist
|
|
||||||
RUN grep -q "health_bp" app.py || sed -i '/from flask import/a from health import health_bp' app.py
|
|
||||||
RUN grep -q "app.register_blueprint(health_bp)" app.py || sed -i '/app = Flask/a app.register_blueprint(health_bp)' app.py
|
|
||||||
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Add startup script to initialize database if needed
|
|
||||||
RUN echo '#!/bin/bash\n\
|
|
||||||
if [ ! -f "instance/myp.db" ] || [ ! -s "instance/myp.db" ]; then\n\
|
|
||||||
echo "Initializing database..."\n\
|
|
||||||
python -c "from app import init_db; init_db()"\n\
|
|
||||||
fi\n\
|
|
||||||
\n\
|
|
||||||
echo "Starting gunicorn server..."\n\
|
|
||||||
gunicorn --bind 0.0.0.0:5000 app:app\n'\
|
|
||||||
> /app/start.sh && chmod +x /app/start.sh
|
|
||||||
|
|
||||||
CMD ["/app/start.sh"]
|
|
@ -1,49 +0,0 @@
|
|||||||
# 🔧 MYP Backend - Entwicklungs-Container
|
|
||||||
# Optimiert für Hot Reload und Debugging
|
|
||||||
|
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
# Arbeitsverzeichnis setzen
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# System-Abhängigkeiten installieren
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
sqlite3 \
|
|
||||||
wget \
|
|
||||||
git \
|
|
||||||
build-essential \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Python-Abhängigkeiten installieren
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Entwicklungs-spezifische Pakete
|
|
||||||
RUN pip install --no-cache-dir \
|
|
||||||
watchdog \
|
|
||||||
flask-debugtoolbar \
|
|
||||||
pytest \
|
|
||||||
pytest-cov \
|
|
||||||
black \
|
|
||||||
flake8
|
|
||||||
|
|
||||||
# Verzeichnisse erstellen
|
|
||||||
RUN mkdir -p logs instance
|
|
||||||
|
|
||||||
# Umgebungsvariablen für Entwicklung
|
|
||||||
ENV FLASK_APP=app.py
|
|
||||||
ENV FLASK_ENV=development
|
|
||||||
ENV FLASK_DEBUG=1
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
|
||||||
|
|
||||||
# Port freigeben
|
|
||||||
EXPOSE 5000 5555
|
|
||||||
|
|
||||||
# Health Check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:5000/health || exit 1
|
|
||||||
|
|
||||||
# Entwicklungs-Startbefehl (wird durch docker-compose überschrieben)
|
|
||||||
CMD ["flask", "run", "--host=0.0.0.0", "--port=5000", "--reload", "--debugger"]
|
|
@ -1,327 +0,0 @@
|
|||||||
# MYP Backend - Produktions-Setup Anleitung
|
|
||||||
|
|
||||||
Diese Anleitung beschreibt die Installation und Konfiguration des MYP Backends für den Produktionsbetrieb.
|
|
||||||
|
|
||||||
## Voraussetzungen
|
|
||||||
|
|
||||||
- Linux-Server (Ubuntu 20.04 LTS oder höher empfohlen)
|
|
||||||
- Python 3.8 oder höher
|
|
||||||
- Nginx (optional, für Reverse Proxy)
|
|
||||||
- Systemd (für Service-Management)
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### 1. System-Updates und Abhängigkeiten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt update && sudo apt upgrade -y
|
|
||||||
sudo apt install python3 python3-pip python3-venv nginx sqlite3 -y
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Benutzer und Verzeichnisse erstellen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# MYP-Benutzer erstellen
|
|
||||||
sudo useradd --system --group --home /opt/myp myp
|
|
||||||
|
|
||||||
# Verzeichnisse erstellen
|
|
||||||
sudo mkdir -p /opt/myp/{backend,logs}
|
|
||||||
sudo chown -R myp:myp /opt/myp
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Anwendung installieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Als myp-Benutzer wechseln
|
|
||||||
sudo -u myp bash
|
|
||||||
|
|
||||||
# In das Backend-Verzeichnis wechseln
|
|
||||||
cd /opt/myp/backend
|
|
||||||
|
|
||||||
# Repository klonen (oder Dateien kopieren)
|
|
||||||
# git clone https://github.com/your-org/myp.git .
|
|
||||||
|
|
||||||
# Virtual Environment erstellen
|
|
||||||
python3 -m venv /opt/myp/venv
|
|
||||||
source /opt/myp/venv/bin/activate
|
|
||||||
|
|
||||||
# Dependencies installieren
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Konfiguration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Umgebungsvariablen konfigurieren
|
|
||||||
cp env.example .env
|
|
||||||
nano .env
|
|
||||||
|
|
||||||
# Wichtige Konfigurationen:
|
|
||||||
# - SECRET_KEY: Sicheren Schlüssel generieren
|
|
||||||
# - TAPO_USERNAME/TAPO_PASSWORD: Tapo-Anmeldedaten
|
|
||||||
# - PRINTERS: Drucker-Konfiguration als JSON
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Beispiel für sichere SECRET_KEY-Generierung:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -c "import secrets; print(secrets.token_hex(32))"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Datenbank initialisieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Datenbank-Verzeichnis erstellen
|
|
||||||
mkdir -p instance
|
|
||||||
|
|
||||||
# Flask-Anwendung starten, um Datenbank zu initialisieren
|
|
||||||
python3 app.py
|
|
||||||
# Ctrl+C nach erfolgreicher Initialisierung
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Systemd Service einrichten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Als root-Benutzer
|
|
||||||
sudo cp myp-backend.service /etc/systemd/system/
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable myp-backend
|
|
||||||
sudo systemctl start myp-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Service-Status überprüfen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl status myp-backend
|
|
||||||
sudo journalctl -u myp-backend -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## Nginx Reverse Proxy (Optional)
|
|
||||||
|
|
||||||
### Nginx-Konfiguration erstellen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/nginx/sites-available/myp-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name your-domain.com;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:5000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_connect_timeout 30;
|
|
||||||
proxy_send_timeout 30;
|
|
||||||
proxy_read_timeout 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Statische Dateien direkt ausliefern
|
|
||||||
location /static/ {
|
|
||||||
alias /opt/myp/backend/static/;
|
|
||||||
expires 1h;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Health Check
|
|
||||||
location /monitoring/health/simple {
|
|
||||||
access_log off;
|
|
||||||
proxy_pass http://127.0.0.1:5000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nginx aktivieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ln -s /etc/nginx/sites-available/myp-backend /etc/nginx/sites-enabled/
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
## SSL/HTTPS Setup (Empfohlen)
|
|
||||||
|
|
||||||
### Mit Let's Encrypt (Certbot)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install certbot python3-certbot-nginx -y
|
|
||||||
sudo certbot --nginx -d your-domain.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Umgebungsvariable für HTTPS setzen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In .env-Datei
|
|
||||||
FORCE_HTTPS=true
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring und Logs
|
|
||||||
|
|
||||||
### Log-Dateien
|
|
||||||
|
|
||||||
- Anwendungs-Logs: `/opt/myp/backend/logs/myp.log`
|
|
||||||
- Error-Logs: `/opt/myp/backend/logs/myp-errors.log`
|
|
||||||
- Security-Logs: `/opt/myp/backend/logs/security.log`
|
|
||||||
- Systemd-Logs: `journalctl -u myp-backend`
|
|
||||||
|
|
||||||
### Health Check-Endpunkte
|
|
||||||
|
|
||||||
- Einfacher Health Check: `http://your-domain.com/monitoring/health/simple`
|
|
||||||
- Detaillierter Health Check: `http://your-domain.com/monitoring/health`
|
|
||||||
- System-Metriken: `http://your-domain.com/monitoring/metrics`
|
|
||||||
- Anwendungsinfo: `http://your-domain.com/monitoring/info`
|
|
||||||
|
|
||||||
## Wartung und Updates
|
|
||||||
|
|
||||||
### Service neustarten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart myp-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logs rotieren
|
|
||||||
|
|
||||||
Die Log-Rotation ist automatisch konfiguriert. Bei Bedarf manuell:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo logrotate -f /etc/logrotate.d/myp-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
### Updates installieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo -u myp bash
|
|
||||||
cd /opt/myp/backend
|
|
||||||
source /opt/myp/venv/bin/activate
|
|
||||||
|
|
||||||
# Code aktualisieren
|
|
||||||
git pull
|
|
||||||
|
|
||||||
# Dependencies aktualisieren
|
|
||||||
pip install -r requirements.txt --upgrade
|
|
||||||
|
|
||||||
# Service neustarten
|
|
||||||
sudo systemctl restart myp-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
### Datenbank-Backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backup erstellen
|
|
||||||
sudo -u myp sqlite3 /opt/myp/backend/instance/myp.db ".backup /opt/myp/backup/myp_$(date +%Y%m%d_%H%M%S).db"
|
|
||||||
|
|
||||||
# Automatisches Backup via Cron
|
|
||||||
sudo -u myp crontab -e
|
|
||||||
# Füge hinzu: 0 2 * * * sqlite3 /opt/myp/backend/instance/myp.db ".backup /opt/myp/backup/myp_$(date +\%Y\%m\%d_\%H\%M\%S).db"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sicherheit
|
|
||||||
|
|
||||||
### Firewall konfigurieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ufw allow ssh
|
|
||||||
sudo ufw allow 'Nginx Full'
|
|
||||||
sudo ufw --force enable
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fail2Ban für zusätzlichen Schutz
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install fail2ban -y
|
|
||||||
sudo nano /etc/fail2ban/jail.local
|
|
||||||
```
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[DEFAULT]
|
|
||||||
bantime = 3600
|
|
||||||
findtime = 600
|
|
||||||
maxretry = 5
|
|
||||||
|
|
||||||
[nginx-http-auth]
|
|
||||||
enabled = true
|
|
||||||
port = http,https
|
|
||||||
logpath = /var/log/nginx/error.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Regelmäßige Security-Updates
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install unattended-upgrades -y
|
|
||||||
sudo dpkg-reconfigure -plow unattended-upgrades
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Service startet nicht
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Logs überprüfen
|
|
||||||
sudo journalctl -u myp-backend --no-pager
|
|
||||||
sudo -u myp cat /opt/myp/backend/logs/myp-errors.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hohe Speichernutzung
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Memory-Statistiken
|
|
||||||
sudo systemctl status myp-backend
|
|
||||||
sudo ps aux | grep gunicorn
|
|
||||||
```
|
|
||||||
|
|
||||||
### Datenbankprobleme
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Datenbank-Integrität prüfen
|
|
||||||
sudo -u myp sqlite3 /opt/myp/backend/instance/myp.db "PRAGMA integrity_check;"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Netzwerk-Konnektivität
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Port-Verfügbarkeit prüfen
|
|
||||||
sudo netstat -tlnp | grep :5000
|
|
||||||
curl -I http://localhost:5000/monitoring/health/simple
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance-Optimierung
|
|
||||||
|
|
||||||
### Gunicorn Worker anpassen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In .env oder Service-Datei
|
|
||||||
WORKERS=8 # 2 * CPU-Kerne + 1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nginx-Caching aktivieren
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# In Nginx-Konfiguration
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### SQLite-Optimierung
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Für bessere Performance (einmalig ausführen)
|
|
||||||
PRAGMA journal_mode=WAL;
|
|
||||||
PRAGMA synchronous=NORMAL;
|
|
||||||
PRAGMA cache_size=10000;
|
|
||||||
PRAGMA temp_store=memory;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Bei Problemen oder Fragen:
|
|
||||||
|
|
||||||
1. Überprüfen Sie die Log-Dateien
|
|
||||||
2. Testen Sie die Health Check-Endpunkte
|
|
||||||
3. Konsultieren Sie die Systemd-Logs
|
|
||||||
4. Kontaktieren Sie das Entwicklungsteam
|
|
120
backend/README.md
Normal file
120
backend/README.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# 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/<printerId>` - Einzelnen Drucker abrufen
|
||||||
|
- `POST /api/printers` - Neuen Drucker anlegen
|
||||||
|
- `DELETE /api/printers/<printerId>` - Drucker löschen
|
||||||
|
|
||||||
|
### Jobs/Reservierungen
|
||||||
|
|
||||||
|
- `GET /api/jobs` - Alle Reservierungen abrufen
|
||||||
|
- `POST /api/jobs` - Neue Reservierung anlegen
|
||||||
|
- `GET /api/jobs/<jobId>` - Reservierungsdetails abrufen
|
||||||
|
- `POST /api/jobs/<jobId>/finish` - Job beenden (Plug ausschalten)
|
||||||
|
- `POST /api/jobs/<jobId>/abort` - Job abbrechen (Plug ausschalten)
|
||||||
|
- `POST /api/jobs/<jobId>/extend` - Endzeit verschieben
|
||||||
|
- `GET /api/jobs/<jobId>/status` - Aktuellen Plug-Status abrufen
|
||||||
|
- `GET /api/jobs/<jobId>/remaining-time` - Restzeit in Sekunden abrufen
|
||||||
|
- `DELETE /api/jobs/<jobId>` - Job löschen
|
||||||
|
|
||||||
|
### Benutzer
|
||||||
|
|
||||||
|
- `GET /api/users` - Alle Benutzer auflisten (nur Admin)
|
||||||
|
- `GET /api/users/<userId>` - Einzelnen Benutzer abrufen
|
||||||
|
- `DELETE /api/users/<userId>` - 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
|
||||||
|
|
@ -1 +0,0 @@
|
|||||||
|
|
217
backend/ROADMAP.md
Normal file
217
backend/ROADMAP.md
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
# MYP V2 - Roadmap
|
||||||
|
|
||||||
|
## Projektübersicht
|
||||||
|
MYP V2 ist ein 3D-Drucker-Management-System mit automatischer Smart Plug-Steuerung für TP-Link Tapo P110 Geräte.
|
||||||
|
|
||||||
|
## Aktuelle Implementierung (Stand: Dezember 2024)
|
||||||
|
|
||||||
|
### ✅ Abgeschlossene Features
|
||||||
|
|
||||||
|
#### Backend-Infrastruktur
|
||||||
|
- **Flask-Anwendung** mit vollständiger REST-API
|
||||||
|
- **SQLite-Datenbank** mit SQLAlchemy ORM
|
||||||
|
- **Benutzerauthentifizierung** mit Flask-Login
|
||||||
|
- **Rollenbasierte Zugriffskontrolle** (Admin/User)
|
||||||
|
- **Job-Scheduler** für automatische Aufgabenausführung
|
||||||
|
- **Logging-System** mit konfigurierbaren Log-Levels
|
||||||
|
- **Konfigurationsmanagement** mit hardcodierten Credentials
|
||||||
|
|
||||||
|
#### Datenmodelle
|
||||||
|
- **User**: Benutzerverwaltung mit Rollen
|
||||||
|
- **Printer**: 3D-Drucker mit Smart Plug-Integration
|
||||||
|
- **Job**: Druckaufträge mit Zeitplanung
|
||||||
|
- **Stats**: Systemstatistiken und Metriken
|
||||||
|
|
||||||
|
#### API-Endpunkte
|
||||||
|
- **Authentifizierung**: Register, Login, Logout
|
||||||
|
- **Drucker-Management**: CRUD-Operationen
|
||||||
|
- **Job-Management**: Erstellen, Überwachen, Steuern von Druckaufträgen
|
||||||
|
- **Benutzer-Management**: Admin-Funktionen
|
||||||
|
- **Statistiken**: Systemmetriken und Berichte
|
||||||
|
- **Scheduler-Steuerung**: Start/Stop/Status des Job-Monitors
|
||||||
|
|
||||||
|
#### Smart Plug-Integration
|
||||||
|
- **TP-Link Tapo P110** Steuerung über PyP100
|
||||||
|
- **Automatisches Ein-/Ausschalten** basierend auf Job-Zeiten
|
||||||
|
- **Stromverbrauchsüberwachung**
|
||||||
|
- **Fehlerbehandlung** bei Verbindungsproblemen
|
||||||
|
|
||||||
|
#### Logging & Monitoring
|
||||||
|
- **Strukturiertes Logging** mit separaten Loggern für verschiedene Komponenten
|
||||||
|
- **Log-Rotation** und Archivierung
|
||||||
|
- **Startup-Informationen** und Systemstatus
|
||||||
|
- **Fehlerprotokollierung** mit Stack-Traces
|
||||||
|
|
||||||
|
### 🔧 Technische Architektur
|
||||||
|
|
||||||
|
#### Verzeichnisstruktur
|
||||||
|
```
|
||||||
|
MYP_V2/
|
||||||
|
├── app/
|
||||||
|
│ ├── blueprints/ # Flask Blueprints (leer)
|
||||||
|
│ ├── config/
|
||||||
|
│ │ └── settings.py # Konfiguration und Credentials
|
||||||
|
│ ├── models.py # Datenbankmodelle
|
||||||
|
│ ├── app.py # Haupt-Flask-Anwendung
|
||||||
|
│ ├── static/ # CSS, JS, Bilder (leer)
|
||||||
|
│ ├── templates/ # HTML-Templates (leer)
|
||||||
|
│ └── utils/
|
||||||
|
│ ├── job_scheduler.py # Background-Task-Scheduler
|
||||||
|
│ └── logging_config.py # Logging-Konfiguration
|
||||||
|
├── docs/ # Dokumentation
|
||||||
|
├── install/ # Installationsskripte
|
||||||
|
├── logs/ # Log-Dateien
|
||||||
|
│ ├── app/ # Anwendungs-Logs
|
||||||
|
│ ├── auth/ # Authentifizierungs-Logs
|
||||||
|
│ ├── jobs/ # Job-Logs
|
||||||
|
│ ├── printers/ # Drucker-Logs
|
||||||
|
│ └── scheduler/ # Scheduler-Logs
|
||||||
|
├── ROADMAP.md # Diese Datei
|
||||||
|
└── setup_myp.sh # Setup-Skript
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Konfiguration
|
||||||
|
- **Hardcodierte Credentials** für Tapo-Geräte
|
||||||
|
- **Drucker-Konfiguration** mit IP-Adressen
|
||||||
|
- **Flask-Einstellungen** (Host, Port, Debug-Modus)
|
||||||
|
- **Session-Management** mit konfigurierbarer Lebensdauer
|
||||||
|
- **Scheduler-Einstellungen** mit aktivierbarem/deaktivierbarem Modus
|
||||||
|
|
||||||
|
## 🚀 Geplante Features
|
||||||
|
|
||||||
|
### Phase 1: Frontend-Entwicklung
|
||||||
|
- [ ] **React/Vue.js Frontend** für Benutzeroberfläche
|
||||||
|
- [ ] **Dashboard** mit Echtzeit-Status der Drucker
|
||||||
|
- [ ] **Job-Kalender** für Terminplanung
|
||||||
|
- [ ] **Benutzer-Management-Interface** für Admins
|
||||||
|
- [ ] **Responsive Design** für mobile Geräte
|
||||||
|
|
||||||
|
### Phase 2: Erweiterte Features
|
||||||
|
- [ ] **Datei-Upload** für 3D-Modelle (.stl, .gcode)
|
||||||
|
- [ ] **Druckzeit-Schätzung** basierend auf Dateianalyse
|
||||||
|
- [ ] **Material-Tracking** mit Verbrauchsberechnung
|
||||||
|
- [ ] **Wartungsplanung** für Drucker
|
||||||
|
- [ ] **Benachrichtigungssystem** (E-Mail, Push)
|
||||||
|
|
||||||
|
### Phase 3: Integration & Automatisierung
|
||||||
|
- [ ] **Octoprint-Integration** für erweiterte Druckersteuerung
|
||||||
|
- [ ] **Kamera-Integration** für Live-Überwachung
|
||||||
|
- [ ] **Temperatur-Monitoring** über zusätzliche Sensoren
|
||||||
|
- [ ] **Automatische Qualitätskontrolle** mit KI-basierter Bilderkennung
|
||||||
|
- [ ] **Multi-Standort-Support** für verteilte Druckerfarms
|
||||||
|
|
||||||
|
### Phase 4: Enterprise Features
|
||||||
|
- [ ] **Kostenverfolgung** pro Job und Benutzer
|
||||||
|
- [ ] **Reporting & Analytics** mit erweiterten Metriken
|
||||||
|
- [ ] **API-Dokumentation** mit Swagger/OpenAPI
|
||||||
|
- [ ] **Backup & Recovery** System
|
||||||
|
- [ ] **LDAP/Active Directory** Integration
|
||||||
|
|
||||||
|
## 🔒 Sicherheit & Compliance
|
||||||
|
|
||||||
|
### Aktuelle Sicherheitsmaßnahmen
|
||||||
|
- ✅ **Session-basierte Authentifizierung**
|
||||||
|
- ✅ **Rollenbasierte Zugriffskontrolle**
|
||||||
|
- ✅ **Passwort-Hashing** mit Werkzeug
|
||||||
|
- ✅ **SQL-Injection-Schutz** durch SQLAlchemy ORM
|
||||||
|
|
||||||
|
### Geplante Sicherheitsverbesserungen
|
||||||
|
- [ ] **JWT-Token-Authentifizierung** für API-Zugriff
|
||||||
|
- [ ] **Rate Limiting** für API-Endpunkte
|
||||||
|
- [ ] **HTTPS-Erzwingung** in Produktionsumgebung
|
||||||
|
- [ ] **Audit-Logging** für kritische Aktionen
|
||||||
|
- [ ] **Verschlüsselung** sensibler Daten in der Datenbank
|
||||||
|
|
||||||
|
## 📊 Performance & Skalierung
|
||||||
|
|
||||||
|
### Aktuelle Architektur
|
||||||
|
- **SQLite-Datenbank** für einfache Bereitstellung
|
||||||
|
- **Single-Thread-Scheduler** für Job-Monitoring
|
||||||
|
- **Synchrone API-Verarbeitung**
|
||||||
|
|
||||||
|
### Geplante Verbesserungen
|
||||||
|
- [ ] **PostgreSQL/MySQL** Support für größere Installationen
|
||||||
|
- [ ] **Redis** für Session-Storage und Caching
|
||||||
|
- [ ] **Celery** für asynchrone Task-Verarbeitung
|
||||||
|
- [ ] **Load Balancing** für Multi-Instance-Deployments
|
||||||
|
- [ ] **Containerisierung** mit Docker
|
||||||
|
|
||||||
|
## 🧪 Testing & Qualitätssicherung
|
||||||
|
|
||||||
|
### Geplante Test-Infrastruktur
|
||||||
|
- [ ] **Unit Tests** für alle Komponenten
|
||||||
|
- [ ] **Integration Tests** für API-Endpunkte
|
||||||
|
- [ ] **End-to-End Tests** für kritische Workflows
|
||||||
|
- [ ] **Performance Tests** für Lastszenarien
|
||||||
|
- [ ] **Security Tests** für Penetrationstests
|
||||||
|
|
||||||
|
## 📚 Dokumentation
|
||||||
|
|
||||||
|
### Geplante Dokumentation
|
||||||
|
- [ ] **API-Dokumentation** mit interaktiven Beispielen
|
||||||
|
- [ ] **Benutzerhandbuch** für End-User
|
||||||
|
- [ ] **Administrator-Handbuch** für System-Setup
|
||||||
|
- [ ] **Entwickler-Dokumentation** für Beiträge
|
||||||
|
- [ ] **Deployment-Guide** für verschiedene Umgebungen
|
||||||
|
|
||||||
|
## 🔄 Deployment & DevOps
|
||||||
|
|
||||||
|
### Aktuelle Bereitstellung
|
||||||
|
- **Manuelles Setup** über setup_myp.sh
|
||||||
|
- **Lokale Entwicklungsumgebung**
|
||||||
|
|
||||||
|
### Geplante Verbesserungen
|
||||||
|
- [ ] **Docker-Container** für einfache Bereitstellung
|
||||||
|
- [ ] **CI/CD-Pipeline** mit GitHub Actions
|
||||||
|
- [ ] **Automatisierte Tests** bei Pull Requests
|
||||||
|
- [ ] **Staging-Umgebung** für Pre-Production-Tests
|
||||||
|
- [ ] **Monitoring & Alerting** mit Prometheus/Grafana
|
||||||
|
|
||||||
|
## 📈 Metriken & KPIs
|
||||||
|
|
||||||
|
### Zu verfolgende Metriken
|
||||||
|
- **Druckzeit-Effizienz**: Verhältnis geplante vs. tatsächliche Druckzeit
|
||||||
|
- **Systemverfügbarkeit**: Uptime der Drucker und Services
|
||||||
|
- **Benutzeraktivität**: Anzahl aktiver Benutzer und Jobs
|
||||||
|
- **Fehlerrate**: Anzahl fehlgeschlagener Jobs und Systemfehler
|
||||||
|
- **Ressourcenverbrauch**: CPU, Memory, Disk Usage
|
||||||
|
|
||||||
|
## 🎯 Meilensteine
|
||||||
|
|
||||||
|
### Q1 2025
|
||||||
|
- [ ] Frontend-Grundgerüst implementieren
|
||||||
|
- [ ] Basis-Dashboard mit Drucker-Status
|
||||||
|
- [ ] Job-Erstellung über Web-Interface
|
||||||
|
|
||||||
|
### Q2 2025
|
||||||
|
- [ ] Datei-Upload und -Management
|
||||||
|
- [ ] Erweiterte Job-Steuerung
|
||||||
|
- [ ] Benutzer-Management-Interface
|
||||||
|
|
||||||
|
### Q3 2025
|
||||||
|
- [ ] Mobile App (React Native/Flutter)
|
||||||
|
- [ ] Erweiterte Integrationen (Octoprint, Kameras)
|
||||||
|
- [ ] Performance-Optimierungen
|
||||||
|
|
||||||
|
### Q4 2025
|
||||||
|
- [ ] Enterprise-Features
|
||||||
|
- [ ] Multi-Tenant-Support
|
||||||
|
- [ ] Vollständige API-Dokumentation
|
||||||
|
|
||||||
|
## 🤝 Beitrag & Community
|
||||||
|
|
||||||
|
### Entwicklungsrichtlinien
|
||||||
|
- **Code-Qualität**: Einhaltung von PEP 8 für Python
|
||||||
|
- **Dokumentation**: Vollständige Docstrings für alle Funktionen
|
||||||
|
- **Testing**: Mindestens 80% Code-Coverage
|
||||||
|
- **Security**: Regelmäßige Sicherheitsüberprüfungen
|
||||||
|
|
||||||
|
### Lizenzierung
|
||||||
|
- **Open Source**: MIT-Lizenz für Community-Beiträge
|
||||||
|
- **Enterprise**: Kommerzielle Lizenz für erweiterte Features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Letzte Aktualisierung**: Dezember 2024
|
||||||
|
**Version**: 2.0.0-alpha
|
||||||
|
**Maintainer**: MYP Development Team
|
1958
backend/app.py
1958
backend/app.py
File diff suppressed because it is too large
Load Diff
1001
backend/app/app.py
Normal file
1001
backend/app/app.py
Normal file
File diff suppressed because it is too large
Load Diff
66
backend/app/config/settings.py
Normal file
66
backend/app/config/settings.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
# Hardcodierte Konfiguration
|
||||||
|
SECRET_KEY = "7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F"
|
||||||
|
DATABASE_PATH = "database/myp.db"
|
||||||
|
TAPO_USERNAME = "till.tomczak@mercedes-benz.com"
|
||||||
|
TAPO_PASSWORD = "744563017196A"
|
||||||
|
|
||||||
|
# Drucker-Konfiguration
|
||||||
|
PRINTERS = {
|
||||||
|
"Printer 1": {"ip": "192.168.0.100"},
|
||||||
|
"Printer 2": {"ip": "192.168.0.101"},
|
||||||
|
"Printer 3": {"ip": "192.168.0.102"},
|
||||||
|
"Printer 4": {"ip": "192.168.0.103"},
|
||||||
|
"Printer 5": {"ip": "192.168.0.104"},
|
||||||
|
"Printer 6": {"ip": "192.168.0.106"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging-Konfiguration
|
||||||
|
LOG_DIR = "logs"
|
||||||
|
LOG_SUBDIRS = ["app", "scheduler", "auth", "jobs", "printers", "errors"]
|
||||||
|
LOG_LEVEL = "INFO"
|
||||||
|
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
# Flask-Konfiguration
|
||||||
|
FLASK_HOST = "0.0.0.0"
|
||||||
|
FLASK_PORT = 5000
|
||||||
|
FLASK_DEBUG = True
|
||||||
|
SESSION_LIFETIME = timedelta(days=7)
|
||||||
|
|
||||||
|
# Scheduler-Konfiguration
|
||||||
|
SCHEDULER_INTERVAL = 60 # Sekunden
|
||||||
|
SCHEDULER_ENABLED = True
|
||||||
|
|
||||||
|
# Datenbank-Konfiguration
|
||||||
|
DB_ENGINE = f"sqlite:///{DATABASE_PATH}"
|
||||||
|
|
||||||
|
def get_log_file(category: str) -> str:
|
||||||
|
"""
|
||||||
|
Gibt den Pfad zur Log-Datei für eine bestimmte Kategorie zurück.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: Log-Kategorie (app, scheduler, auth, jobs, printers, errors)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Pfad zur Log-Datei
|
||||||
|
"""
|
||||||
|
if category not in LOG_SUBDIRS:
|
||||||
|
category = "app"
|
||||||
|
|
||||||
|
return os.path.join(LOG_DIR, category, f"{category}.log")
|
||||||
|
|
||||||
|
def ensure_log_directories():
|
||||||
|
"""Erstellt alle erforderlichen Log-Verzeichnisse."""
|
||||||
|
os.makedirs(LOG_DIR, exist_ok=True)
|
||||||
|
for subdir in LOG_SUBDIRS:
|
||||||
|
os.makedirs(os.path.join(LOG_DIR, subdir), exist_ok=True)
|
||||||
|
|
||||||
|
def ensure_database_directory():
|
||||||
|
"""Erstellt das Datenbank-Verzeichnis."""
|
||||||
|
db_dir = os.path.dirname(DATABASE_PATH)
|
||||||
|
if db_dir:
|
||||||
|
os.makedirs(db_dir, exist_ok=True)
|
180
backend/app/models.py
Normal file
180
backend/app/models.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey, Float
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import relationship, sessionmaker, Session
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
|
from config.settings import DATABASE_PATH, ensure_database_directory
|
||||||
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
logger = get_logger("app")
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
email = Column(String(120), unique=True, nullable=False)
|
||||||
|
password_hash = Column(String(128), nullable=False)
|
||||||
|
name = Column(String(100), nullable=False)
|
||||||
|
role = Column(String(20), default="user") # "admin" oder "user"
|
||||||
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
jobs = relationship("Job", back_populates="user", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def set_password(self, password: str) -> None:
|
||||||
|
password_bytes = password.encode('utf-8')
|
||||||
|
salt = bcrypt.gensalt()
|
||||||
|
self.password_hash = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
|
||||||
|
|
||||||
|
def check_password(self, password: str) -> bool:
|
||||||
|
password_bytes = password.encode('utf-8')
|
||||||
|
hash_bytes = self.password_hash.encode('utf-8')
|
||||||
|
return bcrypt.checkpw(password_bytes, hash_bytes)
|
||||||
|
|
||||||
|
def is_admin(self) -> bool:
|
||||||
|
return self.role == "admin"
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"email": self.email,
|
||||||
|
"name": self.name,
|
||||||
|
"role": self.role,
|
||||||
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Printer(Base):
|
||||||
|
__tablename__ = "printers"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String(100), nullable=False)
|
||||||
|
location = Column(String(100))
|
||||||
|
mac_address = Column(String(50), nullable=False, unique=True)
|
||||||
|
plug_ip = Column(String(50), nullable=False)
|
||||||
|
plug_username = Column(String(100), nullable=False)
|
||||||
|
plug_password = Column(String(100), nullable=False)
|
||||||
|
active = Column(Boolean, default=True)
|
||||||
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
jobs = relationship("Job", back_populates="printer", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"name": self.name,
|
||||||
|
"location": self.location,
|
||||||
|
"mac_address": self.mac_address,
|
||||||
|
"plug_ip": self.plug_ip,
|
||||||
|
"active": self.active,
|
||||||
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Job(Base):
|
||||||
|
__tablename__ = "jobs"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
title = Column(String(200), nullable=False)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
printer_id = Column(Integer, ForeignKey("printers.id"), nullable=False)
|
||||||
|
start_time = Column(DateTime, nullable=False)
|
||||||
|
end_time = Column(DateTime, nullable=False)
|
||||||
|
actual_end_time = Column(DateTime)
|
||||||
|
status = Column(String(20), default="scheduled") # scheduled, active, completed, aborted
|
||||||
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
|
notes = Column(String(500))
|
||||||
|
material_used = Column(Float) # in Gramm
|
||||||
|
|
||||||
|
user = relationship("User", back_populates="jobs")
|
||||||
|
printer = relationship("Printer", back_populates="jobs")
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"title": self.title,
|
||||||
|
"user_id": self.user_id,
|
||||||
|
"printer_id": self.printer_id,
|
||||||
|
"start_time": self.start_time.isoformat() if self.start_time else None,
|
||||||
|
"end_time": self.end_time.isoformat() if self.end_time else None,
|
||||||
|
"actual_end_time": self.actual_end_time.isoformat() if self.actual_end_time else None,
|
||||||
|
"status": self.status,
|
||||||
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
|
"notes": self.notes,
|
||||||
|
"material_used": self.material_used,
|
||||||
|
"user": self.user.to_dict() if self.user else None,
|
||||||
|
"printer": self.printer.to_dict() if self.printer else None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Stats(Base):
|
||||||
|
__tablename__ = "stats"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
total_print_time = Column(Integer, default=0) # in Sekunden
|
||||||
|
total_jobs_completed = Column(Integer, default=0)
|
||||||
|
total_material_used = Column(Float, default=0.0) # in Gramm
|
||||||
|
last_updated = Column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
def init_db() -> None:
|
||||||
|
"""Initialisiert die Datenbank und erstellt alle Tabellen."""
|
||||||
|
ensure_database_directory()
|
||||||
|
engine = create_engine(f"sqlite:///{DATABASE_PATH}")
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
logger.info("Datenbank initialisiert.")
|
||||||
|
|
||||||
|
|
||||||
|
def create_initial_admin(email: str, password: str, name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Erstellt einen initialen Admin-Benutzer, falls die Datenbank leer ist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
email: E-Mail-Adresse des Admins
|
||||||
|
password: Passwort des Admins
|
||||||
|
name: Name des Admins
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True, wenn der Admin erstellt wurde, False sonst
|
||||||
|
"""
|
||||||
|
engine = create_engine(f"sqlite:///{DATABASE_PATH}")
|
||||||
|
Session_class = sessionmaker(bind=engine)
|
||||||
|
session = Session_class()
|
||||||
|
|
||||||
|
# Prüfen, ob bereits Benutzer existieren
|
||||||
|
user_count = session.query(User).count()
|
||||||
|
if user_count > 0:
|
||||||
|
session.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ersten Admin anlegen
|
||||||
|
admin = User(
|
||||||
|
email=email,
|
||||||
|
name=name,
|
||||||
|
role="admin"
|
||||||
|
)
|
||||||
|
admin.set_password(password)
|
||||||
|
|
||||||
|
session.add(admin)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Statistik-Eintrag anlegen
|
||||||
|
stats = Stats()
|
||||||
|
session.add(stats)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
logger.info(f"Initialer Admin-Benutzer {email} wurde angelegt.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_db_session() -> Session:
|
||||||
|
"""Gibt eine neue Datenbank-Session zurück."""
|
||||||
|
engine = create_engine(f"sqlite:///{DATABASE_PATH}")
|
||||||
|
Session_class = sessionmaker(bind=engine)
|
||||||
|
return Session_class()
|
1
backend/app/utils/__init__.py
Normal file
1
backend/app/utils/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Utils package for MYP
|
230
backend/app/utils/job_scheduler.py
Normal file
230
backend/app/utils/job_scheduler.py
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Callable, Any, List, Optional, Union
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("scheduler")
|
||||||
|
|
||||||
|
class BackgroundTaskScheduler:
|
||||||
|
"""
|
||||||
|
Ein fortschrittlicher Hintergrund-Task-Scheduler, der registrierbare Worker-Funktionen unterstützt.
|
||||||
|
Tasks können als Platzhalter registriert und später konfiguriert werden.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._tasks: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self._thread: Optional[threading.Thread] = None
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def register_task(self,
|
||||||
|
task_id: str,
|
||||||
|
func: Callable,
|
||||||
|
interval: int = 60,
|
||||||
|
args: List = None,
|
||||||
|
kwargs: Dict = None,
|
||||||
|
enabled: bool = True) -> bool:
|
||||||
|
"""
|
||||||
|
Registriert eine neue Hintergrund-Task.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: Eindeutige ID für die Task
|
||||||
|
func: Die auszuführende Funktion
|
||||||
|
interval: Intervall in Sekunden zwischen den Ausführungen
|
||||||
|
args: Positionsargumente für die Funktion
|
||||||
|
kwargs: Schlüsselwortargumente für die Funktion
|
||||||
|
enabled: Ob die Task aktiviert sein soll
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn erfolgreich, False wenn die ID bereits existiert
|
||||||
|
"""
|
||||||
|
if task_id in self._tasks:
|
||||||
|
logger.error(f"Task mit ID {task_id} existiert bereits")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._tasks[task_id] = {
|
||||||
|
"func": func,
|
||||||
|
"interval": interval,
|
||||||
|
"args": args or [],
|
||||||
|
"kwargs": kwargs or {},
|
||||||
|
"enabled": enabled,
|
||||||
|
"last_run": None,
|
||||||
|
"next_run": datetime.now() if enabled else None
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Task {task_id} registriert: Intervall {interval}s, Enabled: {enabled}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_task(self,
|
||||||
|
task_id: str,
|
||||||
|
interval: Optional[int] = None,
|
||||||
|
args: Optional[List] = None,
|
||||||
|
kwargs: Optional[Dict] = None,
|
||||||
|
enabled: Optional[bool] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Aktualisiert die Konfiguration einer bestehenden Task.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: ID der zu aktualisierenden Task
|
||||||
|
interval: Neues Intervall in Sekunden
|
||||||
|
args: Neue Positionsargumente
|
||||||
|
kwargs: Neue Schlüsselwortargumente
|
||||||
|
enabled: Neuer Aktivierungsstatus
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn erfolgreich, False wenn die ID nicht existiert
|
||||||
|
"""
|
||||||
|
if task_id not in self._tasks:
|
||||||
|
logger.error(f"Task mit ID {task_id} existiert nicht")
|
||||||
|
return False
|
||||||
|
|
||||||
|
task = self._tasks[task_id]
|
||||||
|
|
||||||
|
if interval is not None:
|
||||||
|
task["interval"] = interval
|
||||||
|
|
||||||
|
if args is not None:
|
||||||
|
task["args"] = args
|
||||||
|
|
||||||
|
if kwargs is not None:
|
||||||
|
task["kwargs"] = kwargs
|
||||||
|
|
||||||
|
if enabled is not None and enabled != task["enabled"]:
|
||||||
|
task["enabled"] = enabled
|
||||||
|
if enabled:
|
||||||
|
task["next_run"] = datetime.now()
|
||||||
|
else:
|
||||||
|
task["next_run"] = None
|
||||||
|
|
||||||
|
logger.info(f"Task {task_id} aktualisiert: Intervall {task['interval']}s, Enabled: {task['enabled']}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def remove_task(self, task_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
Entfernt eine Task aus dem Scheduler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: ID der zu entfernenden Task
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn erfolgreich, False wenn die ID nicht existiert
|
||||||
|
"""
|
||||||
|
if task_id not in self._tasks:
|
||||||
|
logger.error(f"Task mit ID {task_id} existiert nicht")
|
||||||
|
return False
|
||||||
|
|
||||||
|
del self._tasks[task_id]
|
||||||
|
logger.info(f"Task {task_id} entfernt")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_task_info(self, task_id: Optional[str] = None) -> Union[Dict, List[Dict]]:
|
||||||
|
"""
|
||||||
|
Gibt Informationen zu einer Task oder allen Tasks zurück.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: ID der Task oder None für alle Tasks
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict oder List: Task-Informationen
|
||||||
|
"""
|
||||||
|
if task_id is not None:
|
||||||
|
if task_id not in self._tasks:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
task = self._tasks[task_id]
|
||||||
|
return {
|
||||||
|
"id": task_id,
|
||||||
|
"interval": task["interval"],
|
||||||
|
"enabled": task["enabled"],
|
||||||
|
"last_run": task["last_run"].isoformat() if task["last_run"] else None,
|
||||||
|
"next_run": task["next_run"].isoformat() if task["next_run"] else None
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": tid,
|
||||||
|
"interval": task["interval"],
|
||||||
|
"enabled": task["enabled"],
|
||||||
|
"last_run": task["last_run"].isoformat() if task["last_run"] else None,
|
||||||
|
"next_run": task["next_run"].isoformat() if task["next_run"] else None
|
||||||
|
}
|
||||||
|
for tid, task in self._tasks.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
def start(self) -> bool:
|
||||||
|
"""
|
||||||
|
Startet den Scheduler.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn erfolgreich gestartet, False wenn bereits läuft
|
||||||
|
"""
|
||||||
|
if self._running:
|
||||||
|
logger.warning("Scheduler läuft bereits")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._stop_event.clear()
|
||||||
|
self._thread = threading.Thread(target=self._run)
|
||||||
|
self._thread.daemon = True
|
||||||
|
self._thread.start()
|
||||||
|
self._running = True
|
||||||
|
|
||||||
|
logger.info("Scheduler gestartet")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def stop(self) -> bool:
|
||||||
|
"""
|
||||||
|
Stoppt den Scheduler.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn erfolgreich gestoppt, False wenn nicht läuft
|
||||||
|
"""
|
||||||
|
if not self._running:
|
||||||
|
logger.warning("Scheduler läuft nicht")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._stop_event.set()
|
||||||
|
if self._thread:
|
||||||
|
self._thread.join(timeout=5.0)
|
||||||
|
|
||||||
|
self._running = False
|
||||||
|
logger.info("Scheduler gestoppt")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft, ob der Scheduler läuft.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn der Scheduler läuft, sonst False
|
||||||
|
"""
|
||||||
|
return self._running
|
||||||
|
|
||||||
|
def _run(self) -> None:
|
||||||
|
"""Interne Methode zum Ausführen des Scheduler-Loops."""
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
for task_id, task in self._tasks.items():
|
||||||
|
if not task["enabled"] or not task["next_run"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if now >= task["next_run"]:
|
||||||
|
logger.info(f"Ausführung von Task {task_id}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
task["func"](*task["args"], **task["kwargs"])
|
||||||
|
logger.info(f"Task {task_id} erfolgreich ausgeführt")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei Ausführung von Task {task_id}: {str(e)}")
|
||||||
|
|
||||||
|
task["last_run"] = now
|
||||||
|
task["next_run"] = now + timedelta(seconds=task["interval"])
|
||||||
|
|
||||||
|
# 1 Sekunde warten und erneut prüfen
|
||||||
|
self._stop_event.wait(1)
|
||||||
|
|
||||||
|
# Singleton-Instanz
|
||||||
|
scheduler = BackgroundTaskScheduler()
|
101
backend/app/utils/logging_config.py
Normal file
101
backend/app/utils/logging_config.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
import os
|
||||||
|
from typing import Dict
|
||||||
|
from config.settings import (
|
||||||
|
LOG_DIR, LOG_SUBDIRS, LOG_LEVEL, LOG_FORMAT, LOG_DATE_FORMAT,
|
||||||
|
get_log_file, ensure_log_directories
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dictionary zur Speicherung der konfigurierten Logger
|
||||||
|
_loggers: Dict[str, logging.Logger] = {}
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
"""Initialisiert das Logging-System und erstellt alle erforderlichen Verzeichnisse."""
|
||||||
|
ensure_log_directories()
|
||||||
|
|
||||||
|
# Root-Logger konfigurieren
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(getattr(logging, LOG_LEVEL))
|
||||||
|
|
||||||
|
# Alle Handler entfernen
|
||||||
|
for handler in root_logger.handlers[:]:
|
||||||
|
root_logger.removeHandler(handler)
|
||||||
|
|
||||||
|
# Formatter erstellen
|
||||||
|
formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)
|
||||||
|
|
||||||
|
# Console Handler für alle Logs
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(getattr(logging, LOG_LEVEL))
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# File Handler für allgemeine App-Logs
|
||||||
|
app_log_file = get_log_file("app")
|
||||||
|
app_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
app_log_file, maxBytes=10*1024*1024, backupCount=5
|
||||||
|
)
|
||||||
|
app_handler.setLevel(getattr(logging, LOG_LEVEL))
|
||||||
|
app_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(app_handler)
|
||||||
|
|
||||||
|
def get_logger(category: str) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Gibt einen konfigurierten Logger für eine bestimmte Kategorie zurück.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: Log-Kategorie (app, scheduler, auth, jobs, printers, errors)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
logging.Logger: Konfigurierter Logger
|
||||||
|
"""
|
||||||
|
if category in _loggers:
|
||||||
|
return _loggers[category]
|
||||||
|
|
||||||
|
# Logger erstellen
|
||||||
|
logger = logging.getLogger(f"myp.{category}")
|
||||||
|
logger.setLevel(getattr(logging, LOG_LEVEL))
|
||||||
|
|
||||||
|
# Verhindere doppelte Logs durch Parent-Logger
|
||||||
|
logger.propagate = False
|
||||||
|
|
||||||
|
# Formatter erstellen
|
||||||
|
formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)
|
||||||
|
|
||||||
|
# Console Handler
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(getattr(logging, LOG_LEVEL))
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# File Handler für spezifische Kategorie
|
||||||
|
log_file = get_log_file(category)
|
||||||
|
file_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
log_file, maxBytes=10*1024*1024, backupCount=5
|
||||||
|
)
|
||||||
|
file_handler.setLevel(getattr(logging, LOG_LEVEL))
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# Error-Logs zusätzlich in errors.log schreiben
|
||||||
|
if category != "errors":
|
||||||
|
error_log_file = get_log_file("errors")
|
||||||
|
error_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
error_log_file, maxBytes=10*1024*1024, backupCount=5
|
||||||
|
)
|
||||||
|
error_handler.setLevel(logging.ERROR)
|
||||||
|
error_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(error_handler)
|
||||||
|
|
||||||
|
_loggers[category] = logger
|
||||||
|
return logger
|
||||||
|
|
||||||
|
def log_startup_info():
|
||||||
|
"""Loggt Startup-Informationen."""
|
||||||
|
app_logger = get_logger("app")
|
||||||
|
app_logger.info("=" * 50)
|
||||||
|
app_logger.info("MYP (Manage Your Printers) wird gestartet...")
|
||||||
|
app_logger.info(f"Log-Verzeichnis: {LOG_DIR}")
|
||||||
|
app_logger.info(f"Log-Level: {LOG_LEVEL}")
|
||||||
|
app_logger.info("=" * 50)
|
@ -1,218 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Raspberry Pi Bereinigungsskript für MYP-Projekt
|
|
||||||
# Dieses Skript bereinigt alte Docker-Installationen und installiert alle erforderlichen Abhängigkeiten
|
|
||||||
|
|
||||||
# Farbcodes für Ausgabe
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[0;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Funktion zur Ausgabe mit Zeitstempel
|
|
||||||
log() {
|
|
||||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log() {
|
|
||||||
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prüfen, ob das Skript mit Root-Rechten ausgeführt wird
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
|
||||||
error_log "Dieses Skript muss mit Root-Rechten ausgeführt werden (sudo)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "${YELLOW}=== MYP Raspberry Pi Bereinigung und Setup ===${NC}"
|
|
||||||
log "Diese Skript wird alle alten Docker-Installationen entfernen und die erforderlichen Abhängigkeiten neu installieren."
|
|
||||||
|
|
||||||
# Sicherstellen, dass apt funktioniert
|
|
||||||
log "Aktualisiere apt-Paketindex..."
|
|
||||||
apt-get update || {
|
|
||||||
error_log "Konnte apt-Paketindex nicht aktualisieren."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Installiere grundlegende Abhängigkeiten
|
|
||||||
log "Installiere grundlegende Abhängigkeiten..."
|
|
||||||
apt-get install -y \
|
|
||||||
apt-transport-https \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
gnupg \
|
|
||||||
lsb-release \
|
|
||||||
wget \
|
|
||||||
git \
|
|
||||||
jq \
|
|
||||||
|| {
|
|
||||||
error_log "Konnte grundlegende Abhängigkeiten nicht installieren."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Stoppe alle laufenden Docker-Container
|
|
||||||
log "${YELLOW}Stoppe alle laufenden Docker-Container...${NC}"
|
|
||||||
if command -v docker &> /dev/null; then
|
|
||||||
docker stop $(docker ps -aq) 2>/dev/null || true
|
|
||||||
log "Alle Docker-Container gestoppt."
|
|
||||||
else
|
|
||||||
log "Docker ist nicht installiert, keine Container zu stoppen."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Entferne alte Docker-Installation
|
|
||||||
log "${YELLOW}Entferne alte Docker-Installation...${NC}"
|
|
||||||
apt-get remove -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-compose || true
|
|
||||||
apt-get autoremove -y || true
|
|
||||||
rm -rf /var/lib/docker /var/lib/containerd /var/run/docker.sock /etc/docker /usr/local/bin/docker-compose 2>/dev/null || true
|
|
||||||
log "${GREEN}Alte Docker-Installation entfernt.${NC}"
|
|
||||||
|
|
||||||
# Entferne alte Projektcontainer und -Dateien
|
|
||||||
log "${YELLOW}Entferne alte MYP-Projektcontainer und -Dateien...${NC}"
|
|
||||||
if command -v docker &> /dev/null; then
|
|
||||||
# Entferne Container
|
|
||||||
docker rm -f myp-frontend myp-backend 2>/dev/null || true
|
|
||||||
# Entferne Images
|
|
||||||
docker rmi -f myp-frontend myp-backend 2>/dev/null || true
|
|
||||||
# Entferne unbenutzte Volumes und Netzwerke
|
|
||||||
docker system prune -af --volumes 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Erkennen der Raspberry Pi-Architektur
|
|
||||||
log "Erkenne Systemarchitektur..."
|
|
||||||
ARCH=$(dpkg --print-architecture)
|
|
||||||
log "Erkannte Architektur: ${ARCH}"
|
|
||||||
|
|
||||||
# Installiere Docker mit dem offiziellen Convenience-Skript
|
|
||||||
log "${YELLOW}Installiere Docker mit dem offiziellen Convenience-Skript...${NC}"
|
|
||||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
|
||||||
sh get-docker.sh --channel stable
|
|
||||||
|
|
||||||
# Überprüfen, ob Docker erfolgreich installiert wurde
|
|
||||||
if ! command -v docker &> /dev/null; then
|
|
||||||
error_log "Docker-Installation fehlgeschlagen!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "${GREEN}Docker erfolgreich installiert!${NC}"
|
|
||||||
|
|
||||||
# Füge den aktuellen Benutzer zur Docker-Gruppe hinzu
|
|
||||||
if [ "$SUDO_USER" ]; then
|
|
||||||
log "Füge Benutzer $SUDO_USER zur Docker-Gruppe hinzu..."
|
|
||||||
usermod -aG docker $SUDO_USER
|
|
||||||
log "${YELLOW}Hinweis: Eine Neuanmeldung ist erforderlich, damit die Gruppenänderung wirksam wird.${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Konfiguriere Docker mit DNS-Servern für bessere Netzwerkkompatibilität
|
|
||||||
log "Konfiguriere Docker mit Google DNS..."
|
|
||||||
mkdir -p /etc/docker
|
|
||||||
cat > /etc/docker/daemon.json << EOL
|
|
||||||
{
|
|
||||||
"dns": ["8.8.8.8", "8.8.4.4"]
|
|
||||||
}
|
|
||||||
EOL
|
|
||||||
|
|
||||||
# Starte Docker-Dienst neu
|
|
||||||
log "Starte Docker-Dienst neu..."
|
|
||||||
systemctl restart docker
|
|
||||||
systemctl enable docker
|
|
||||||
log "${GREEN}Docker-Dienst neu gestartet und für den Autostart aktiviert.${NC}"
|
|
||||||
|
|
||||||
# Installiere Docker Compose v2
|
|
||||||
log "${YELLOW}Installiere Docker Compose...${NC}"
|
|
||||||
|
|
||||||
# Bestimme die passende Docker Compose-Version für die Architektur
|
|
||||||
if [ "$ARCH" = "armhf" ]; then
|
|
||||||
log "Installiere Docker Compose für armhf (32-bit)..."
|
|
||||||
curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-armv7" -o /usr/local/bin/docker-compose
|
|
||||||
elif [ "$ARCH" = "arm64" ]; then
|
|
||||||
log "Installiere Docker Compose für arm64 (64-bit)..."
|
|
||||||
curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose
|
|
||||||
else
|
|
||||||
log "Unbekannte Architektur, verwende automatische Erkennung..."
|
|
||||||
curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod +x /usr/local/bin/docker-compose
|
|
||||||
|
|
||||||
# Überprüfe, ob Docker Compose installiert wurde
|
|
||||||
if ! command -v docker-compose &> /dev/null; then
|
|
||||||
error_log "Docker Compose-Installation fehlgeschlagen!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Installiere Docker Compose Plugin (neuere Methode)
|
|
||||||
log "Installiere Docker Compose Plugin..."
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y docker-compose-plugin
|
|
||||||
|
|
||||||
log "${GREEN}Docker Compose erfolgreich installiert!${NC}"
|
|
||||||
docker compose version || docker-compose --version
|
|
||||||
|
|
||||||
# Installiere zusätzliche Abhängigkeiten für die Projektunterstützung
|
|
||||||
log "${YELLOW}Installiere zusätzliche Projektabhängigkeiten...${NC}"
|
|
||||||
apt-get install -y \
|
|
||||||
python3 \
|
|
||||||
python3-pip \
|
|
||||||
sqlite3 \
|
|
||||||
build-essential \
|
|
||||||
libffi-dev \
|
|
||||||
libssl-dev \
|
|
||||||
|| {
|
|
||||||
error_log "Konnte zusätzliche Abhängigkeiten nicht installieren."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optimieren des Raspberry Pi für Docker-Workloads
|
|
||||||
log "${YELLOW}Optimiere Raspberry Pi für Docker-Workloads...${NC}"
|
|
||||||
|
|
||||||
# Swap erhöhen für bessere Performance bei begrenztem RAM
|
|
||||||
log "Konfiguriere Swap-Größe..."
|
|
||||||
CURRENT_SWAP=$(grep "CONF_SWAPSIZE" /etc/dphys-swapfile | cut -d= -f2)
|
|
||||||
log "Aktuelle Swap-Größe: ${CURRENT_SWAP}"
|
|
||||||
|
|
||||||
# Erhöhe Swap auf 2GB, wenn weniger
|
|
||||||
if [ "$CURRENT_SWAP" -lt 2048 ]; then
|
|
||||||
sed -i 's/^CONF_SWAPSIZE=.*/CONF_SWAPSIZE=2048/' /etc/dphys-swapfile
|
|
||||||
log "Swap-Größe auf 2048MB erhöht, Neustart des Swap-Dienstes erforderlich."
|
|
||||||
|
|
||||||
# Neustart des Swap-Dienstes
|
|
||||||
/etc/init.d/dphys-swapfile restart
|
|
||||||
else
|
|
||||||
log "Swap-Größe ist bereits ausreichend."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Konfiguriere cgroup für Docker
|
|
||||||
if ! grep -q "cgroup_enable=memory" /boot/cmdline.txt; then
|
|
||||||
log "Konfiguriere cgroup für Docker..."
|
|
||||||
CMDLINE=$(cat /boot/cmdline.txt)
|
|
||||||
echo "$CMDLINE cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1" > /boot/cmdline.txt
|
|
||||||
log "${YELLOW}WICHTIG: Ein Systemneustart ist erforderlich, damit die cgroup-Änderungen wirksam werden.${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prüfe, ob Backend-Installationsdateien vorhanden sind
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
BACKEND_DIR="$SCRIPT_DIR"
|
|
||||||
|
|
||||||
if [ -d "$BACKEND_DIR" ] && [ -f "$BACKEND_DIR/docker-compose.yml" ]; then
|
|
||||||
log "${GREEN}Backend-Projektdateien gefunden in $BACKEND_DIR${NC}"
|
|
||||||
else
|
|
||||||
log "${YELLOW}Warnung: Backend-Projektdateien nicht gefunden in $BACKEND_DIR${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Abschlussmeldung
|
|
||||||
log "${GREEN}=== Bereinigung und Setup abgeschlossen ===${NC}"
|
|
||||||
log "${YELLOW}WICHTIGE HINWEISE:${NC}"
|
|
||||||
log "1. Ein ${RED}SYSTEMNEUSTART${NC} ist ${RED}DRINGEND ERFORDERLICH${NC}, damit alle Änderungen wirksam werden."
|
|
||||||
log "2. Nach dem Neustart können Sie das Backend-Installationsskript ausführen:"
|
|
||||||
log " cd $BACKEND_DIR && ./install.sh"
|
|
||||||
log "3. Bei Problemen mit Docker-Berechtigungen stellen Sie sicher, dass Sie sich neu angemeldet haben."
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
read -p "Möchten Sie das System jetzt neu starten? (j/n): " REBOOT_CHOICE
|
|
||||||
if [[ "$REBOOT_CHOICE" == "j" ]]; then
|
|
||||||
log "System wird neu gestartet..."
|
|
||||||
reboot
|
|
||||||
else
|
|
||||||
log "Bitte starten Sie das System manuell neu, bevor Sie die Installationsskripte ausführen."
|
|
||||||
fi
|
|
@ -1,235 +0,0 @@
|
|||||||
"""
|
|
||||||
Konfigurationsklassen für die MYP Flask-Anwendung.
|
|
||||||
Definiert verschiedene Konfigurationen für Development, Production und Testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from datetime import timedelta
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Basis-Konfigurationsklasse mit gemeinsamen Einstellungen."""
|
|
||||||
|
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY') or secrets.token_hex(32)
|
|
||||||
DATABASE = os.environ.get('DATABASE_PATH', 'instance/myp.db')
|
|
||||||
|
|
||||||
# Session-Konfiguration
|
|
||||||
SESSION_COOKIE_HTTPONLY = True
|
|
||||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
|
||||||
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
|
|
||||||
|
|
||||||
# Job-Konfiguration
|
|
||||||
JOB_CHECK_INTERVAL = int(os.environ.get('JOB_CHECK_INTERVAL', '60')) # Sekunden
|
|
||||||
|
|
||||||
# Tapo-Konfiguration
|
|
||||||
TAPO_USERNAME = os.environ.get('TAPO_USERNAME')
|
|
||||||
TAPO_PASSWORD = os.environ.get('TAPO_PASSWORD')
|
|
||||||
|
|
||||||
# Logging-Konfiguration
|
|
||||||
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')
|
|
||||||
LOG_MAX_BYTES = int(os.environ.get('LOG_MAX_BYTES', '10485760')) # 10MB
|
|
||||||
LOG_BACKUP_COUNT = int(os.environ.get('LOG_BACKUP_COUNT', '10'))
|
|
||||||
|
|
||||||
# Drucker-Konfiguration
|
|
||||||
PRINTERS = os.environ.get('PRINTERS', '{}')
|
|
||||||
|
|
||||||
# JSON-Konfiguration parsen
|
|
||||||
@property
|
|
||||||
def PRINTERS_DICT(self):
|
|
||||||
"""Parse PRINTERS configuration as JSON"""
|
|
||||||
import json
|
|
||||||
printers_str = self.PRINTERS
|
|
||||||
if isinstance(printers_str, dict):
|
|
||||||
return printers_str
|
|
||||||
try:
|
|
||||||
return json.loads(printers_str) if printers_str else {}
|
|
||||||
except (json.JSONDecodeError, TypeError):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# API-Konfiguration
|
|
||||||
API_KEY = os.environ.get('API_KEY')
|
|
||||||
|
|
||||||
# Rate Limiting
|
|
||||||
RATE_LIMIT_ENABLED = True
|
|
||||||
MAX_REQUESTS_PER_MINUTE = int(os.environ.get('MAX_REQUESTS_PER_MINUTE', '100'))
|
|
||||||
RATE_LIMIT_WINDOW_MINUTES = int(os.environ.get('RATE_LIMIT_WINDOW_MINUTES', '15'))
|
|
||||||
|
|
||||||
# Security
|
|
||||||
SECURITY_ENABLED = True
|
|
||||||
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def init_app(app):
|
|
||||||
"""Initialisierung der Anwendung mit der Konfiguration."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class DevelopmentConfig(Config):
|
|
||||||
"""Konfiguration für die Entwicklungsumgebung."""
|
|
||||||
|
|
||||||
DEBUG = True
|
|
||||||
TESTING = False
|
|
||||||
|
|
||||||
# Session-Cookies in Development weniger strikt
|
|
||||||
SESSION_COOKIE_SECURE = False
|
|
||||||
|
|
||||||
# Kürzere Job-Check-Intervalle für schnellere Entwicklung
|
|
||||||
JOB_CHECK_INTERVAL = int(os.environ.get('JOB_CHECK_INTERVAL', '30'))
|
|
||||||
|
|
||||||
# Weniger strikte Sicherheit in Development
|
|
||||||
SECURITY_ENABLED = False
|
|
||||||
RATE_LIMIT_ENABLED = False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def init_app(app):
|
|
||||||
Config.init_app(app)
|
|
||||||
|
|
||||||
# Development-spezifische Initialisierung
|
|
||||||
import logging
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
class ProductionConfig(Config):
|
|
||||||
"""Konfiguration für die Produktionsumgebung."""
|
|
||||||
|
|
||||||
DEBUG = False
|
|
||||||
TESTING = False
|
|
||||||
|
|
||||||
# Sichere Session-Cookies in Production
|
|
||||||
SESSION_COOKIE_SECURE = True
|
|
||||||
SESSION_COOKIE_HTTPONLY = True
|
|
||||||
SESSION_COOKIE_SAMESITE = 'Strict'
|
|
||||||
|
|
||||||
# Strengere Sicherheitseinstellungen
|
|
||||||
WTF_CSRF_ENABLED = True
|
|
||||||
WTF_CSRF_TIME_LIMIT = None
|
|
||||||
|
|
||||||
# Längere Job-Check-Intervalle für bessere Performance
|
|
||||||
JOB_CHECK_INTERVAL = int(os.environ.get('JOB_CHECK_INTERVAL', '60'))
|
|
||||||
|
|
||||||
# Produktions-Sicherheit
|
|
||||||
SECURITY_ENABLED = True
|
|
||||||
RATE_LIMIT_ENABLED = True
|
|
||||||
MAX_REQUESTS_PER_MINUTE = int(os.environ.get('MAX_REQUESTS_PER_MINUTE', '60'))
|
|
||||||
|
|
||||||
# HTTPS-Enforcement (wenn verfügbar)
|
|
||||||
FORCE_HTTPS = os.environ.get('FORCE_HTTPS', 'False').lower() == 'true'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def init_app(app):
|
|
||||||
Config.init_app(app)
|
|
||||||
|
|
||||||
# Production-spezifische Initialisierung
|
|
||||||
import logging
|
|
||||||
from logging.handlers import RotatingFileHandler, SysLogHandler
|
|
||||||
|
|
||||||
# Datei-Logging
|
|
||||||
if not os.path.exists('logs'):
|
|
||||||
os.mkdir('logs')
|
|
||||||
|
|
||||||
# Prüfe ob Datei-Logging deaktiviert ist (für Tests)
|
|
||||||
if app.config.get('DISABLE_FILE_LOGGING', False):
|
|
||||||
app.logger.info('Datei-Logging deaktiviert (Test-Modus)')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Windows-kompatibles Logging
|
|
||||||
import platform
|
|
||||||
if platform.system() == 'Windows':
|
|
||||||
# Windows: Verwende TimedRotatingFileHandler statt RotatingFileHandler
|
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
|
||||||
|
|
||||||
file_handler = TimedRotatingFileHandler(
|
|
||||||
'logs/myp.log',
|
|
||||||
when='midnight',
|
|
||||||
interval=1,
|
|
||||||
backupCount=Config.LOG_BACKUP_COUNT
|
|
||||||
)
|
|
||||||
|
|
||||||
error_handler = TimedRotatingFileHandler(
|
|
||||||
'logs/myp-errors.log',
|
|
||||||
when='midnight',
|
|
||||||
interval=1,
|
|
||||||
backupCount=Config.LOG_BACKUP_COUNT
|
|
||||||
)
|
|
||||||
|
|
||||||
security_handler = TimedRotatingFileHandler(
|
|
||||||
'logs/security.log',
|
|
||||||
when='midnight',
|
|
||||||
interval=1,
|
|
||||||
backupCount=Config.LOG_BACKUP_COUNT
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Linux/Unix: Verwende RotatingFileHandler
|
|
||||||
file_handler = RotatingFileHandler(
|
|
||||||
'logs/myp.log',
|
|
||||||
maxBytes=Config.LOG_MAX_BYTES,
|
|
||||||
backupCount=Config.LOG_BACKUP_COUNT
|
|
||||||
)
|
|
||||||
|
|
||||||
error_handler = RotatingFileHandler(
|
|
||||||
'logs/myp-errors.log',
|
|
||||||
maxBytes=Config.LOG_MAX_BYTES,
|
|
||||||
backupCount=Config.LOG_BACKUP_COUNT
|
|
||||||
)
|
|
||||||
|
|
||||||
security_handler = RotatingFileHandler(
|
|
||||||
'logs/security.log',
|
|
||||||
maxBytes=Config.LOG_MAX_BYTES,
|
|
||||||
backupCount=Config.LOG_BACKUP_COUNT
|
|
||||||
)
|
|
||||||
|
|
||||||
file_handler.setFormatter(logging.Formatter(
|
|
||||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
|
||||||
))
|
|
||||||
file_handler.setLevel(logging.INFO)
|
|
||||||
app.logger.addHandler(file_handler)
|
|
||||||
|
|
||||||
error_handler.setFormatter(logging.Formatter(
|
|
||||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
|
||||||
))
|
|
||||||
error_handler.setLevel(logging.ERROR)
|
|
||||||
app.logger.addHandler(error_handler)
|
|
||||||
|
|
||||||
security_handler.setFormatter(logging.Formatter(
|
|
||||||
'%(asctime)s SECURITY %(levelname)s: %(message)s [%(name)s]'
|
|
||||||
))
|
|
||||||
security_handler.setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
# Security-Logger
|
|
||||||
security_logger = logging.getLogger('security')
|
|
||||||
security_logger.addHandler(security_handler)
|
|
||||||
security_logger.setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
app.logger.setLevel(logging.INFO)
|
|
||||||
app.logger.info('MYP Backend starting in production mode')
|
|
||||||
|
|
||||||
# Sicherheits-Middleware registrieren (optional)
|
|
||||||
if app.config.get('SECURITY_ENABLED', True):
|
|
||||||
try:
|
|
||||||
from security import security_middleware
|
|
||||||
security_middleware.init_app(app)
|
|
||||||
except ImportError:
|
|
||||||
app.logger.warning('Security module not found, skipping security middleware')
|
|
||||||
|
|
||||||
class TestingConfig(Config):
|
|
||||||
"""Konfiguration für Tests"""
|
|
||||||
TESTING = True
|
|
||||||
DEBUG = True
|
|
||||||
WTF_CSRF_ENABLED = False
|
|
||||||
DATABASE_PATH = ':memory:' # In-Memory-Datenbank für Tests
|
|
||||||
|
|
||||||
# Deaktiviere Datei-Logging für Tests (Windows-Kompatibilität)
|
|
||||||
DISABLE_FILE_LOGGING = True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def init_app(app):
|
|
||||||
"""Initialisierung für Test-Umgebung"""
|
|
||||||
# Nur Console-Logging für Tests
|
|
||||||
import logging
|
|
||||||
app.logger.setLevel(logging.WARNING) # Reduziere Log-Level für Tests
|
|
||||||
|
|
||||||
# Konfigurationsmapping
|
|
||||||
config = {
|
|
||||||
'development': DevelopmentConfig,
|
|
||||||
'production': ProductionConfig,
|
|
||||||
'testing': TestingConfig,
|
|
||||||
'default': DevelopmentConfig
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,15 +0,0 @@
|
|||||||
flask==2.2.3
|
|
||||||
flask-cors==3.0.10
|
|
||||||
psutil==5.9.4
|
|
||||||
matplotlib==3.7.1
|
|
||||||
pandas==1.5.3
|
|
||||||
requests==2.28.2
|
|
||||||
netifaces==0.11.0
|
|
||||||
Werkzeug==2.2.3
|
|
||||||
docker==6.1.2
|
|
||||||
pillow==9.4.0
|
|
||||||
send_file==0.2.0
|
|
||||||
python-dotenv>=1.0.0
|
|
||||||
python-logging-loki>=0.3.1
|
|
||||||
colorama>=0.4.6
|
|
||||||
pygal>=3.0.0
|
|
@ -1,617 +0,0 @@
|
|||||||
/* Debug-Dashboard CSS */
|
|
||||||
|
|
||||||
/* Reset und Basis-Stile */
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout */
|
|
||||||
.page-header {
|
|
||||||
background-color: #2c3e50;
|
|
||||||
color: white;
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header h1 {
|
|
||||||
color: white;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-update {
|
|
||||||
font-size: 0.9em;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-section {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Karten */
|
|
||||||
.card {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
padding: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Statistik-Karten */
|
|
||||||
.stats-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background-color: #f8fafc;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 15px;
|
|
||||||
min-width: 150px;
|
|
||||||
text-align: center;
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 5px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #64748b;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 1.5em;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Charts */
|
|
||||||
.chart-container {
|
|
||||||
position: relative;
|
|
||||||
height: 200px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-container.small {
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Formularelemente */
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group input {
|
|
||||||
flex: 1;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group-append {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group-append .btn {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
background-color: #3b82f6;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
background-color: #2563eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 5px 10px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background-color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success:hover {
|
|
||||||
background-color: #059669;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning {
|
|
||||||
background-color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning:hover {
|
|
||||||
background-color: #d97706;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background-color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background-color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status-Anzeigen */
|
|
||||||
.status {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-good {
|
|
||||||
background-color: #d1fae5;
|
|
||||||
color: #064e3b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-warning {
|
|
||||||
background-color: #fff7ed;
|
|
||||||
color: #7c2d12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-error {
|
|
||||||
background-color: #fee2e2;
|
|
||||||
color: #7f1d1d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nachrichten */
|
|
||||||
.message {
|
|
||||||
display: none;
|
|
||||||
padding: 15px;
|
|
||||||
margin: 15px 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-success {
|
|
||||||
background-color: #d1fae5;
|
|
||||||
color: #064e3b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-error {
|
|
||||||
background-color: #fee2e2;
|
|
||||||
color: #7f1d1d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Systemstatus-Banner */
|
|
||||||
.system-health-banner {
|
|
||||||
display: none;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 20px;
|
|
||||||
color: white;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-health-banner.checking {
|
|
||||||
background-color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-health-banner.healthy {
|
|
||||||
background-color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-health-banner.warning {
|
|
||||||
background-color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-health-banner.critical {
|
|
||||||
background-color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-icon {
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-status {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-status-title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-details {
|
|
||||||
font-size: 0.9em;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-good {
|
|
||||||
color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-warning {
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-critical {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tabellen */
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table th,
|
|
||||||
table td {
|
|
||||||
padding: 12px 15px;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table th {
|
|
||||||
background-color: #f8fafc;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
table tbody tr:hover {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tabs */
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
padding: 10px 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab:hover {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab.active {
|
|
||||||
border-bottom-color: #3b82f6;
|
|
||||||
color: #3b82f6;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Docker-Container */
|
|
||||||
.container-row {
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-row.running {
|
|
||||||
border-left: 3px solid #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-row.exited {
|
|
||||||
border-left: 3px solid #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-image {
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-running {
|
|
||||||
color: #064e3b;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logs und Terminal-Ausgabe */
|
|
||||||
.logs-container {
|
|
||||||
background-color: #1e293b;
|
|
||||||
color: #e2e8f0;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-top: 15px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-placeholder {
|
|
||||||
color: #94a3b8;
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #334155;
|
|
||||||
margin: -15px -15px 10px -15px;
|
|
||||||
border-top-left-radius: 5px;
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-line {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
padding: 2px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-error {
|
|
||||||
color: #f87171;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-warning {
|
|
||||||
color: #fbbf24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-info {
|
|
||||||
color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-debug {
|
|
||||||
color: #a3e635;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-timestamp {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre.ping-output,
|
|
||||||
pre.traceroute-output,
|
|
||||||
pre.dns-output {
|
|
||||||
background-color: #1e293b;
|
|
||||||
color: #e2e8f0;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Netzwerkschnittstellen */
|
|
||||||
.interface-item {
|
|
||||||
background-color: #f8fafc;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.interface-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interface-mac {
|
|
||||||
font-family: monospace;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interface-ips h4,
|
|
||||||
.interface-stats h4 {
|
|
||||||
color: #475569;
|
|
||||||
font-size: 1em;
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ip-item {
|
|
||||||
padding: 8px;
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ergebnisse-Container */
|
|
||||||
.results-container {
|
|
||||||
margin-top: 15px;
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading-Anzeige */
|
|
||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading::before {
|
|
||||||
content: "";
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
margin-right: 10px;
|
|
||||||
border: 2px solid #cbd5e1;
|
|
||||||
border-top-color: #3b82f6;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spinner 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spinner {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Error-Anzeige */
|
|
||||||
.error {
|
|
||||||
color: #ef4444;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #fee2e2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Log-Analyse */
|
|
||||||
.error-list,
|
|
||||||
.warning-list {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-item,
|
|
||||||
.warning-item {
|
|
||||||
background-color: #fee2e2;
|
|
||||||
border-left: 3px solid #ef4444;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning-item {
|
|
||||||
background-color: #fff7ed;
|
|
||||||
border-left-color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-time,
|
|
||||||
.warning-time {
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #64748b;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.dashboard-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-row {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-group {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,291 +0,0 @@
|
|||||||
/* Allgemeine Stile */
|
|
||||||
:root {
|
|
||||||
--primary-color: #3498db;
|
|
||||||
--secondary-color: #2c3e50;
|
|
||||||
--background-color: #f5f7fa;
|
|
||||||
--card-color: #ffffff;
|
|
||||||
--text-color: #333333;
|
|
||||||
--border-color: #e0e0e0;
|
|
||||||
--success-color: #2ecc71;
|
|
||||||
--warning-color: #f39c12;
|
|
||||||
--danger-color: #e74c3c;
|
|
||||||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: var(--background-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
header {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-content {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-info {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation */
|
|
||||||
nav {
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button {
|
|
||||||
background: none;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.75rem 1.25rem;
|
|
||||||
margin: 0 0.25rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button.active {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hauptbereich */
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
padding: 2rem;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: var(--secondary-color);
|
|
||||||
border-bottom: 2px solid var(--primary-color);
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: var(--card-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card.full-width {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: var(--secondary-color);
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fortschrittsbalken */
|
|
||||||
.progress-container {
|
|
||||||
margin-top: 1rem;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border-radius: 4px;
|
|
||||||
height: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
width: 0%;
|
|
||||||
transition: width 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tool-Karten */
|
|
||||||
.tool-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-input {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-input input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-input button {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-input button:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-box {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1rem;
|
|
||||||
overflow: auto;
|
|
||||||
min-height: 150px;
|
|
||||||
max-height: 300px;
|
|
||||||
font-family: 'Consolas', 'Courier New', monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tabellen */
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
padding: 0.75rem;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:hover {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status-Anzeigen */
|
|
||||||
.status {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-online {
|
|
||||||
background-color: var(--success-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-offline {
|
|
||||||
background-color: var(--danger-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-warning {
|
|
||||||
background-color: var(--warning-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
footer {
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-top: auto;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer p {
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
header {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-content {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-info {
|
|
||||||
margin-top: 1rem;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
}
|
|
13
backend/debug-server/static/js/chart.min.js
vendored
13
backend/debug-server/static/js/chart.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,470 +0,0 @@
|
|||||||
/*
|
|
||||||
* Debug-Charts.js
|
|
||||||
* JavaScript-Funktionen für das Rendering von Diagrammen und Visualisierungen
|
|
||||||
* im MYP Debug-Server.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Globale Variablen für Charts
|
|
||||||
let cpuUsageChart = null;
|
|
||||||
let memoryUsageChart = null;
|
|
||||||
let networkTrafficChart = null;
|
|
||||||
let diskUsageChart = null;
|
|
||||||
let containerStatusChart = null;
|
|
||||||
|
|
||||||
// Hilfsfunktion zum Formatieren von Bytes
|
|
||||||
function formatBytes(bytes, decimals = 2) {
|
|
||||||
if (bytes === 0) return '0 Bytes';
|
|
||||||
|
|
||||||
const k = 1024;
|
|
||||||
const dm = decimals < 0 ? 0 : decimals;
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiere Systemdiagramme
|
|
||||||
function updateSystemCharts() {
|
|
||||||
fetch('/api/system/metrics')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
// CPU-Nutzung aktualisieren
|
|
||||||
if (cpuUsageChart) {
|
|
||||||
cpuUsageChart.data.labels.push(new Date().toLocaleTimeString());
|
|
||||||
cpuUsageChart.data.datasets[0].data.push(data.cpu_percent);
|
|
||||||
|
|
||||||
// Behalte nur die letzten 30 Datenpunkte
|
|
||||||
if (cpuUsageChart.data.labels.length > 30) {
|
|
||||||
cpuUsageChart.data.labels.shift();
|
|
||||||
cpuUsageChart.data.datasets[0].data.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
cpuUsageChart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speichernutzung aktualisieren
|
|
||||||
if (memoryUsageChart) {
|
|
||||||
memoryUsageChart.data.datasets[0].data = [
|
|
||||||
data.memory.used,
|
|
||||||
data.memory.available
|
|
||||||
];
|
|
||||||
memoryUsageChart.update();
|
|
||||||
|
|
||||||
// Aktualisiere die Speicherinfo-Texte
|
|
||||||
document.getElementById('memory-used').textContent = formatBytes(data.memory.used);
|
|
||||||
document.getElementById('memory-available').textContent = formatBytes(data.memory.available);
|
|
||||||
document.getElementById('memory-total').textContent = formatBytes(data.memory.total);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Festplattennutzung aktualisieren
|
|
||||||
if (diskUsageChart && data.disk_usage) {
|
|
||||||
const diskLabels = [];
|
|
||||||
const diskUsed = [];
|
|
||||||
const diskFree = [];
|
|
||||||
|
|
||||||
for (const disk of data.disk_usage) {
|
|
||||||
diskLabels.push(disk.mountpoint);
|
|
||||||
diskUsed.push(disk.used);
|
|
||||||
diskFree.push(disk.free);
|
|
||||||
}
|
|
||||||
|
|
||||||
diskUsageChart.data.labels = diskLabels;
|
|
||||||
diskUsageChart.data.datasets[0].data = diskUsed;
|
|
||||||
diskUsageChart.data.datasets[1].data = diskFree;
|
|
||||||
diskUsageChart.update();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Fehler beim Abrufen der Systemmetriken:', error));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialisiere CPU-Nutzungsdiagramm
|
|
||||||
function initCpuUsageChart() {
|
|
||||||
const ctx = document.getElementById('cpu-usage-chart').getContext('2d');
|
|
||||||
|
|
||||||
cpuUsageChart = new Chart(ctx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: [],
|
|
||||||
datasets: [{
|
|
||||||
label: 'CPU-Auslastung (%)',
|
|
||||||
data: [],
|
|
||||||
borderColor: 'rgb(75, 192, 192)',
|
|
||||||
tension: 0.1,
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: 'rgba(75, 192, 192, 0.2)'
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
max: 100,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Auslastung (%)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Zeit'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'CPU-Auslastung',
|
|
||||||
font: {
|
|
||||||
size: 16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialisiere Speichernutzungsdiagramm
|
|
||||||
function initMemoryUsageChart() {
|
|
||||||
const ctx = document.getElementById('memory-usage-chart').getContext('2d');
|
|
||||||
|
|
||||||
memoryUsageChart = new Chart(ctx, {
|
|
||||||
type: 'doughnut',
|
|
||||||
data: {
|
|
||||||
labels: ['Verwendet', 'Verfügbar'],
|
|
||||||
datasets: [{
|
|
||||||
data: [0, 0],
|
|
||||||
backgroundColor: [
|
|
||||||
'rgba(255, 99, 132, 0.7)',
|
|
||||||
'rgba(75, 192, 192, 0.7)'
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Speichernutzung',
|
|
||||||
font: {
|
|
||||||
size: 16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialisiere Festplattennutzungsdiagramm
|
|
||||||
function initDiskUsageChart() {
|
|
||||||
const ctx = document.getElementById('disk-usage-chart').getContext('2d');
|
|
||||||
|
|
||||||
diskUsageChart = new Chart(ctx, {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: [],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: 'Belegt',
|
|
||||||
data: [],
|
|
||||||
backgroundColor: 'rgba(255, 99, 132, 0.7)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Frei',
|
|
||||||
data: [],
|
|
||||||
backgroundColor: 'rgba(75, 192, 192, 0.7)'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Laufwerk'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Speicherplatz (Bytes)'
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
callback: function(value) {
|
|
||||||
return formatBytes(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Festplattennutzung',
|
|
||||||
font: {
|
|
||||||
size: 16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiere Docker-Container-Status
|
|
||||||
function updateContainerStatus() {
|
|
||||||
fetch('/api/docker/status')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
const containerTable = document.getElementById('container-table');
|
|
||||||
if (!containerTable) return;
|
|
||||||
|
|
||||||
// Tabelle leeren
|
|
||||||
containerTable.innerHTML = '';
|
|
||||||
|
|
||||||
// Überschriftenzeile
|
|
||||||
const headerRow = document.createElement('tr');
|
|
||||||
['Name', 'Status', 'CPU', 'Speicher', 'Netzwerk', 'Aktionen'].forEach(header => {
|
|
||||||
const th = document.createElement('th');
|
|
||||||
th.textContent = header;
|
|
||||||
headerRow.appendChild(th);
|
|
||||||
});
|
|
||||||
containerTable.appendChild(headerRow);
|
|
||||||
|
|
||||||
// Containerdaten
|
|
||||||
data.containers.forEach(container => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
|
|
||||||
// Name
|
|
||||||
const nameCell = document.createElement('td');
|
|
||||||
nameCell.textContent = container.name;
|
|
||||||
row.appendChild(nameCell);
|
|
||||||
|
|
||||||
// Status
|
|
||||||
const statusCell = document.createElement('td');
|
|
||||||
const statusBadge = document.createElement('span');
|
|
||||||
statusBadge.textContent = container.status;
|
|
||||||
statusBadge.className = container.running ? 'status-badge running' : 'status-badge stopped';
|
|
||||||
statusCell.appendChild(statusBadge);
|
|
||||||
row.appendChild(statusCell);
|
|
||||||
|
|
||||||
// CPU
|
|
||||||
const cpuCell = document.createElement('td');
|
|
||||||
cpuCell.textContent = container.cpu_percent ? `${container.cpu_percent.toFixed(2)}%` : 'N/A';
|
|
||||||
row.appendChild(cpuCell);
|
|
||||||
|
|
||||||
// Speicher
|
|
||||||
const memoryCell = document.createElement('td');
|
|
||||||
memoryCell.textContent = container.memory_usage ? formatBytes(container.memory_usage) : 'N/A';
|
|
||||||
row.appendChild(memoryCell);
|
|
||||||
|
|
||||||
// Netzwerk
|
|
||||||
const networkCell = document.createElement('td');
|
|
||||||
if (container.network_io) {
|
|
||||||
networkCell.innerHTML = `↓ ${formatBytes(container.network_io.rx_bytes)}<br>↑ ${formatBytes(container.network_io.tx_bytes)}`;
|
|
||||||
} else {
|
|
||||||
networkCell.textContent = 'N/A';
|
|
||||||
}
|
|
||||||
row.appendChild(networkCell);
|
|
||||||
|
|
||||||
// Aktionen
|
|
||||||
const actionsCell = document.createElement('td');
|
|
||||||
const actionsDiv = document.createElement('div');
|
|
||||||
actionsDiv.className = 'container-actions';
|
|
||||||
|
|
||||||
// Restart-Button
|
|
||||||
const restartBtn = document.createElement('button');
|
|
||||||
restartBtn.className = 'btn btn-warning btn-sm';
|
|
||||||
restartBtn.innerHTML = '<i class="fas fa-sync"></i>';
|
|
||||||
restartBtn.title = 'Container neustarten';
|
|
||||||
restartBtn.onclick = () => restartContainer(container.id);
|
|
||||||
actionsDiv.appendChild(restartBtn);
|
|
||||||
|
|
||||||
// Logs-Button
|
|
||||||
const logsBtn = document.createElement('button');
|
|
||||||
logsBtn.className = 'btn btn-info btn-sm';
|
|
||||||
logsBtn.innerHTML = '<i class="fas fa-list-alt"></i>';
|
|
||||||
logsBtn.title = 'Container-Logs anzeigen';
|
|
||||||
logsBtn.onclick = () => showContainerLogs(container.id);
|
|
||||||
actionsDiv.appendChild(logsBtn);
|
|
||||||
|
|
||||||
actionsCell.appendChild(actionsDiv);
|
|
||||||
row.appendChild(actionsCell);
|
|
||||||
|
|
||||||
containerTable.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Container-Status-Diagramm aktualisieren
|
|
||||||
updateContainerStatusChart(data.containers);
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Fehler beim Abrufen der Docker-Informationen:', error));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialisiere Container-Status-Diagramm
|
|
||||||
function initContainerStatusChart() {
|
|
||||||
const ctx = document.getElementById('container-status-chart').getContext('2d');
|
|
||||||
|
|
||||||
containerStatusChart = new Chart(ctx, {
|
|
||||||
type: 'pie',
|
|
||||||
data: {
|
|
||||||
labels: ['Aktiv', 'Inaktiv'],
|
|
||||||
datasets: [{
|
|
||||||
data: [0, 0],
|
|
||||||
backgroundColor: [
|
|
||||||
'rgba(75, 192, 192, 0.7)',
|
|
||||||
'rgba(255, 99, 132, 0.7)'
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Container-Status',
|
|
||||||
font: {
|
|
||||||
size: 16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiere Container-Status-Diagramm
|
|
||||||
function updateContainerStatusChart(containers) {
|
|
||||||
if (!containerStatusChart) return;
|
|
||||||
|
|
||||||
const running = containers.filter(c => c.running).length;
|
|
||||||
const stopped = containers.filter(c => !c.running).length;
|
|
||||||
|
|
||||||
containerStatusChart.data.datasets[0].data = [running, stopped];
|
|
||||||
containerStatusChart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container neustarten
|
|
||||||
function restartContainer(containerId) {
|
|
||||||
fetch(`/api/docker/restart/${containerId}`, {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
showMessage('Container wird neugestartet...', false);
|
|
||||||
// Nach kurzer Verzögerung aktualisieren
|
|
||||||
setTimeout(updateContainerStatus, 2000);
|
|
||||||
} else {
|
|
||||||
showMessage('Fehler beim Neustarten des Containers: ' + data.message, true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showMessage('Fehler beim Neustarten des Containers', true);
|
|
||||||
console.error('Fehler beim Neustarten des Containers:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container-Logs anzeigen
|
|
||||||
function showContainerLogs(containerId) {
|
|
||||||
fetch(`/api/docker/logs/${containerId}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.logs) {
|
|
||||||
// Modal erstellen und anzeigen
|
|
||||||
const modal = document.createElement('div');
|
|
||||||
modal.className = 'modal';
|
|
||||||
modal.innerHTML = `
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>Container-Logs: ${data.container_name}</h3>
|
|
||||||
<span class="close">×</span>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<pre>${data.logs}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
|
|
||||||
// Modal schließen, wenn auf X geklickt wird
|
|
||||||
modal.querySelector('.close').onclick = function() {
|
|
||||||
document.body.removeChild(modal);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Modal schließen, wenn außerhalb geklickt wird
|
|
||||||
window.onclick = function(event) {
|
|
||||||
if (event.target === modal) {
|
|
||||||
document.body.removeChild(modal);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Modal anzeigen
|
|
||||||
modal.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
showMessage('Keine Logs verfügbar', true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showMessage('Fehler beim Abrufen der Container-Logs', true);
|
|
||||||
console.error('Fehler beim Abrufen der Container-Logs:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zeige Fehlermeldung oder Erfolgsmeldung
|
|
||||||
function showMessage(message, isError = false) {
|
|
||||||
const messageEl = document.getElementById('message');
|
|
||||||
if (messageEl) {
|
|
||||||
messageEl.textContent = message;
|
|
||||||
messageEl.className = isError ? 'message message-error' : 'message message-success';
|
|
||||||
messageEl.style.display = 'block';
|
|
||||||
|
|
||||||
// Verstecke Nachricht nach 5 Sekunden
|
|
||||||
setTimeout(() => {
|
|
||||||
messageEl.style.display = 'none';
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialisiere alle Diagramme
|
|
||||||
function initAllCharts() {
|
|
||||||
// Überprüfe, ob Chart.js geladen ist
|
|
||||||
if (typeof Chart !== 'undefined') {
|
|
||||||
if (document.getElementById('cpu-usage-chart')) {
|
|
||||||
initCpuUsageChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.getElementById('memory-usage-chart')) {
|
|
||||||
initMemoryUsageChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.getElementById('disk-usage-chart')) {
|
|
||||||
initDiskUsageChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.getElementById('container-status-chart')) {
|
|
||||||
initContainerStatusChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialen Datenabruf starten
|
|
||||||
updateSystemCharts();
|
|
||||||
updateContainerStatus();
|
|
||||||
|
|
||||||
// Regelmäßige Aktualisierung der Diagramme
|
|
||||||
setInterval(updateSystemCharts, 5000); // Alle 5 Sekunden aktualisieren
|
|
||||||
setInterval(updateContainerStatus, 10000); // Alle 10 Sekunden aktualisieren
|
|
||||||
} else {
|
|
||||||
console.error('Chart.js konnte nicht geladen werden.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Automatischer Start beim Laden der Seite
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
initAllCharts();
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
@ -1,306 +0,0 @@
|
|||||||
// DOM-Element-Referenzen
|
|
||||||
const navButtons = document.querySelectorAll('.nav-button');
|
|
||||||
const panels = document.querySelectorAll('.panel');
|
|
||||||
|
|
||||||
// Panel-Navigation
|
|
||||||
navButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
const targetPanel = button.dataset.target;
|
|
||||||
|
|
||||||
// Aktiven Button und Panel wechseln
|
|
||||||
navButtons.forEach(btn => btn.classList.remove('active'));
|
|
||||||
panels.forEach(panel => panel.classList.remove('active'));
|
|
||||||
|
|
||||||
button.classList.add('active');
|
|
||||||
document.getElementById(targetPanel).classList.add('active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Automatische Aktualisierung der Daten
|
|
||||||
const updateInterval = 10000; // 10 Sekunden
|
|
||||||
|
|
||||||
// Initialisierung und erste Datenladung
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// Systemdaten laden
|
|
||||||
loadSystemInfo();
|
|
||||||
|
|
||||||
// Netzwerkdaten laden
|
|
||||||
loadNetworkInfo();
|
|
||||||
|
|
||||||
// Docker-Daten laden
|
|
||||||
loadDockerInfo();
|
|
||||||
|
|
||||||
// Backend-Status laden
|
|
||||||
loadBackendStatus();
|
|
||||||
|
|
||||||
// Event-Listener für die Netzwerk-Tools
|
|
||||||
document.getElementById('ping-button').addEventListener('click', performPing);
|
|
||||||
document.getElementById('traceroute-button').addEventListener('click', performTraceroute);
|
|
||||||
document.getElementById('nslookup-button').addEventListener('click', performNSLookup);
|
|
||||||
|
|
||||||
// Automatische Aktualisierung einrichten
|
|
||||||
setInterval(() => {
|
|
||||||
if (document.getElementById('system-panel').classList.contains('active')) {
|
|
||||||
loadSystemInfo();
|
|
||||||
} else if (document.getElementById('network-panel').classList.contains('active')) {
|
|
||||||
loadNetworkInfo();
|
|
||||||
} else if (document.getElementById('docker-panel').classList.contains('active')) {
|
|
||||||
loadDockerInfo();
|
|
||||||
} else if (document.getElementById('backend-panel').classList.contains('active')) {
|
|
||||||
loadBackendStatus();
|
|
||||||
}
|
|
||||||
}, updateInterval);
|
|
||||||
});
|
|
||||||
|
|
||||||
// API-Anfragen
|
|
||||||
async function fetchData(endpoint) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(endpoint);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP-Fehler! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Fehler beim Abrufen von ${endpoint}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// System-Informationen laden
|
|
||||||
async function loadSystemInfo() {
|
|
||||||
const data = await fetchData('/systeminfo');
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
// Betriebssystem-Info aktualisieren
|
|
||||||
document.getElementById('platform-info').innerHTML = `
|
|
||||||
<p><strong>Plattform:</strong> ${data.platform}</p>
|
|
||||||
<p><strong>Python Version:</strong> ${data.python_version}</p>
|
|
||||||
<p><strong>Betriebszeit:</strong> ${data.uptime}</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Hardware-Info aktualisieren
|
|
||||||
document.getElementById('hardware-info').innerHTML = `
|
|
||||||
<p><strong>Prozessor:</strong> ${data.processor}</p>
|
|
||||||
<p><strong>Architektur:</strong> ${data.architecture}</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Speicher-Info aktualisieren
|
|
||||||
document.getElementById('memory-info').innerHTML = `
|
|
||||||
<p><strong>Gesamt:</strong> ${data.memory.total}</p>
|
|
||||||
<p><strong>Verwendet:</strong> ${data.memory.used} (${data.memory.percent}%)</p>
|
|
||||||
<p><strong>Frei:</strong> ${data.memory.free}</p>
|
|
||||||
`;
|
|
||||||
document.getElementById('memory-bar').style.width = `${data.memory.percent}%`;
|
|
||||||
document.getElementById('memory-bar').style.backgroundColor = getColorByPercentage(data.memory.percent);
|
|
||||||
|
|
||||||
// Festplatten-Info aktualisieren
|
|
||||||
document.getElementById('disk-info').innerHTML = `
|
|
||||||
<p><strong>Gesamt:</strong> ${data.disk.total}</p>
|
|
||||||
<p><strong>Verwendet:</strong> ${data.disk.used} (${data.disk.percent}%)</p>
|
|
||||||
<p><strong>Frei:</strong> ${data.disk.free}</p>
|
|
||||||
`;
|
|
||||||
document.getElementById('disk-bar').style.width = `${data.disk.percent}%`;
|
|
||||||
document.getElementById('disk-bar').style.backgroundColor = getColorByPercentage(data.disk.percent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Netzwerk-Informationen laden
|
|
||||||
async function loadNetworkInfo() {
|
|
||||||
const data = await fetchData('/network');
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
// Netzwerkschnittstellen aktualisieren
|
|
||||||
let interfacesHTML = '<table><tr><th>Schnittstelle</th><th>IP-Adresse</th><th>Netzmaske</th><th>Broadcast</th></tr>';
|
|
||||||
for (const [name, info] of Object.entries(data.interfaces)) {
|
|
||||||
interfacesHTML += `
|
|
||||||
<tr>
|
|
||||||
<td>${name}</td>
|
|
||||||
<td>${info.ip}</td>
|
|
||||||
<td>${info.netmask}</td>
|
|
||||||
<td>${info.broadcast}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
interfacesHTML += '</table>';
|
|
||||||
document.getElementById('network-interfaces').innerHTML = interfacesHTML;
|
|
||||||
|
|
||||||
// DNS-Server aktualisieren
|
|
||||||
let dnsHTML = '<ul>';
|
|
||||||
for (const server of data.dns_servers) {
|
|
||||||
dnsHTML += `<li>${server}</li>`;
|
|
||||||
}
|
|
||||||
dnsHTML += '</ul>';
|
|
||||||
document.getElementById('dns-servers').innerHTML = dnsHTML;
|
|
||||||
|
|
||||||
// Gateway aktualisieren
|
|
||||||
document.getElementById('default-gateway').innerHTML = `<p>${data.gateway}</p>`;
|
|
||||||
|
|
||||||
// Aktive Verbindungen aktualisieren
|
|
||||||
if (data.connections && data.connections.length > 0) {
|
|
||||||
let connectionsHTML = '<table><tr><th>Lokale Adresse</th><th>Remote-Adresse</th><th>Status</th><th>PID</th></tr>';
|
|
||||||
for (const conn of data.connections) {
|
|
||||||
connectionsHTML += `
|
|
||||||
<tr>
|
|
||||||
<td>${conn.local_address}</td>
|
|
||||||
<td>${conn.remote_address}</td>
|
|
||||||
<td>${conn.status}</td>
|
|
||||||
<td>${conn.pid}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
connectionsHTML += '</table>';
|
|
||||||
document.getElementById('active-connections').innerHTML = connectionsHTML;
|
|
||||||
} else {
|
|
||||||
document.getElementById('active-connections').innerHTML = '<p>Keine aktiven Verbindungen gefunden.</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Docker-Informationen laden
|
|
||||||
async function loadDockerInfo() {
|
|
||||||
const data = await fetchData('/docker');
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
// Docker-Status aktualisieren
|
|
||||||
if (data.installed) {
|
|
||||||
document.getElementById('docker-status').innerHTML = `
|
|
||||||
<p><span class="status status-online">Installiert</span></p>
|
|
||||||
<p><strong>Version:</strong> ${data.version}</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Container aktualisieren
|
|
||||||
if (data.containers && data.containers.length > 0) {
|
|
||||||
let containersHTML = '<table><tr><th>ID</th><th>Name</th><th>Image</th><th>Status</th><th>Ports</th></tr>';
|
|
||||||
for (const container of data.containers) {
|
|
||||||
containersHTML += `
|
|
||||||
<tr>
|
|
||||||
<td>${container.id}</td>
|
|
||||||
<td>${container.name}</td>
|
|
||||||
<td>${container.image}</td>
|
|
||||||
<td>${container.status}</td>
|
|
||||||
<td>${container.ports}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
containersHTML += '</table>';
|
|
||||||
document.getElementById('docker-containers').innerHTML = containersHTML;
|
|
||||||
} else {
|
|
||||||
document.getElementById('docker-containers').innerHTML = '<p>Keine laufenden Container gefunden.</p>';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.getElementById('docker-status').innerHTML = '<p><span class="status status-offline">Nicht installiert</span></p>';
|
|
||||||
document.getElementById('docker-containers').innerHTML = '<p>Docker ist nicht installiert oder nicht zugänglich.</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backend-Status laden
|
|
||||||
async function loadBackendStatus() {
|
|
||||||
const data = await fetchData('/backend-status');
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
let statusHTML = '';
|
|
||||||
if (data.status === 'online') {
|
|
||||||
statusHTML = `
|
|
||||||
<p>Status: <span class="status status-online">Online</span></p>
|
|
||||||
<p><strong>Port:</strong> ${data.port}</p>
|
|
||||||
`;
|
|
||||||
} else if (data.status === 'offline') {
|
|
||||||
statusHTML = `
|
|
||||||
<p>Status: <span class="status status-offline">Offline</span></p>
|
|
||||||
<p><strong>Port:</strong> ${data.port}</p>
|
|
||||||
<p><strong>Fehlercode:</strong> ${data.error_code}</p>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
statusHTML = `
|
|
||||||
<p>Status: <span class="status status-warning">Fehler</span></p>
|
|
||||||
<p><strong>Nachricht:</strong> ${data.message}</p>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
document.getElementById('main-backend-status').innerHTML = statusHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Netzwerk-Tools
|
|
||||||
async function performPing() {
|
|
||||||
const hostInput = document.getElementById('ping-host');
|
|
||||||
const resultElement = document.getElementById('ping-result');
|
|
||||||
const host = hostInput.value.trim();
|
|
||||||
|
|
||||||
if (!host) {
|
|
||||||
resultElement.textContent = 'Bitte geben Sie einen Hostnamen oder eine IP-Adresse ein.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resultElement.textContent = 'Ping wird ausgeführt...';
|
|
||||||
|
|
||||||
const data = await fetchData(`/ping/${encodeURIComponent(host)}`);
|
|
||||||
if (!data) {
|
|
||||||
resultElement.textContent = 'Fehler beim Ausführen des Ping-Befehls.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
resultElement.textContent = data.output;
|
|
||||||
} else {
|
|
||||||
resultElement.textContent = `Fehler: ${data.error || 'Unbekannter Fehler'}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function performTraceroute() {
|
|
||||||
const hostInput = document.getElementById('traceroute-host');
|
|
||||||
const resultElement = document.getElementById('traceroute-result');
|
|
||||||
const host = hostInput.value.trim();
|
|
||||||
|
|
||||||
if (!host) {
|
|
||||||
resultElement.textContent = 'Bitte geben Sie einen Hostnamen oder eine IP-Adresse ein.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resultElement.textContent = 'Traceroute wird ausgeführt...';
|
|
||||||
|
|
||||||
const data = await fetchData(`/traceroute/${encodeURIComponent(host)}`);
|
|
||||||
if (!data) {
|
|
||||||
resultElement.textContent = 'Fehler beim Ausführen des Traceroute-Befehls.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
resultElement.textContent = data.output;
|
|
||||||
} else {
|
|
||||||
resultElement.textContent = `Fehler: ${data.error || 'Unbekannter Fehler'}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function performNSLookup() {
|
|
||||||
const hostInput = document.getElementById('nslookup-host');
|
|
||||||
const resultElement = document.getElementById('nslookup-result');
|
|
||||||
const host = hostInput.value.trim();
|
|
||||||
|
|
||||||
if (!host) {
|
|
||||||
resultElement.textContent = 'Bitte geben Sie einen Hostnamen oder eine IP-Adresse ein.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resultElement.textContent = 'DNS-Abfrage wird ausgeführt...';
|
|
||||||
|
|
||||||
const data = await fetchData(`/nslookup/${encodeURIComponent(host)}`);
|
|
||||||
if (!data) {
|
|
||||||
resultElement.textContent = 'Fehler beim Ausführen des NSLookup-Befehls.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
resultElement.textContent = data.output;
|
|
||||||
} else {
|
|
||||||
resultElement.textContent = `Fehler: ${data.error || 'Unbekannter Fehler'}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hilfsfunktionen
|
|
||||||
function getColorByPercentage(percent) {
|
|
||||||
// Farbverlauf von Grün über Gelb nach Rot
|
|
||||||
if (percent < 70) {
|
|
||||||
return 'var(--success-color)';
|
|
||||||
} else if (percent < 90) {
|
|
||||||
return 'var(--warning-color)';
|
|
||||||
} else {
|
|
||||||
return 'var(--danger-color)';
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,444 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>MYP Debug Dashboard</title>
|
|
||||||
|
|
||||||
<!-- CSS Stylesheets -->
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/debug-dashboard.css') }}">
|
|
||||||
|
|
||||||
<!-- JavaScript Libraries -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="message" class="message"></div>
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>MYP Debug Dashboard</h1>
|
|
||||||
<div class="last-update">
|
|
||||||
Letzte Aktualisierung: {{ last_check }}
|
|
||||||
</div>
|
|
||||||
<div class="header-actions">
|
|
||||||
<button class="btn btn-refresh" onclick="refreshPage()"><i class="fas fa-sync-alt"></i> Aktualisieren</button>
|
|
||||||
<button class="btn btn-health" onclick="checkHealth()"><i class="fas fa-heartbeat"></i> Systemstatus prüfen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="system-health-banner" id="systemHealthBanner" style="display: none;">
|
|
||||||
<div class="health-icon"><i class="fas fa-spinner fa-spin"></i></div>
|
|
||||||
<div class="health-status">Systemstatus wird geprüft...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-container">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<i class="fas fa-server"></i> Systemübersicht
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="stats-row">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">CPU-Auslastung</div>
|
|
||||||
<div class="stat-value" id="cpu-percent">-</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">RAM-Auslastung</div>
|
|
||||||
<div class="stat-value" id="memory-percent">-</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Aktive Container</div>
|
|
||||||
<div class="stat-value" id="active-containers">-</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="chart-container">
|
|
||||||
<canvas id="cpu-usage-chart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<i class="fas fa-microchip"></i> Speichernutzung
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="chart-container">
|
|
||||||
<canvas id="memory-usage-chart"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats-row">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Verwendet</div>
|
|
||||||
<div class="stat-value" id="memory-used">-</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Verfügbar</div>
|
|
||||||
<div class="stat-value" id="memory-available">-</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Gesamt</div>
|
|
||||||
<div class="stat-value" id="memory-total">-</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-section">
|
|
||||||
<h2><i class="fas fa-hdd"></i> Festplattennutzung</h2>
|
|
||||||
|
|
||||||
<div class="chart-container">
|
|
||||||
<canvas id="disk-usage-chart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-section">
|
|
||||||
<h2><i class="fas fa-network-wired"></i> Netzwerkkonfiguration</h2>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
Verbindungsstatus
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<h3>Backend</h3>
|
|
||||||
<div class="status {{ 'status-good' if 'Verbunden' in backend_status else 'status-error' }}">
|
|
||||||
{{ backend_status }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Frontend</h3>
|
|
||||||
<div class="status {{ 'status-good' if 'Verbunden' in frontend_status else 'status-error' }}">
|
|
||||||
{{ frontend_status }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<i class="fas fa-cogs"></i> Netzwerkkonfiguration
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form id="configForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="backend_hostname">Backend Hostname/IP:</label>
|
|
||||||
<input type="text" id="backend_hostname" name="backend_hostname" value="{{ config.backend_hostname }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="backend_port">Backend Port:</label>
|
|
||||||
<input type="text" id="backend_port" name="backend_port" value="{{ config.backend_port }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="frontend_hostname">Frontend Hostname/IP:</label>
|
|
||||||
<input type="text" id="frontend_hostname" name="frontend_hostname" value="{{ config.frontend_hostname }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="frontend_port">Frontend Port:</label>
|
|
||||||
<input type="text" id="frontend_port" name="frontend_port" value="{{ config.frontend_port }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="btn-group">
|
|
||||||
<button type="button" class="btn" onclick="testConnection()">Verbindung testen</button>
|
|
||||||
<button type="button" class="btn btn-success" onclick="saveConfig()">Konfiguration speichern</button>
|
|
||||||
<button type="button" class="btn btn-warning" onclick="syncFrontend()">Frontend synchronisieren</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-section">
|
|
||||||
<h2><i class="fab fa-docker"></i> Docker-Container</h2>
|
|
||||||
|
|
||||||
<div class="dashboard-container">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Container-Status</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="chart-container small">
|
|
||||||
<canvas id="container-status-chart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Docker-Info</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>Version</th>
|
|
||||||
<td id="docker-version">-</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>API-Version</th>
|
|
||||||
<td id="docker-api-version">-</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>OS</th>
|
|
||||||
<td id="docker-os">-</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Status</th>
|
|
||||||
<td id="docker-status">-</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Container-Liste</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="filter-bar">
|
|
||||||
<input type="text" id="container-filter" placeholder="Container filtern..." oninput="filterContainers()">
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn" onclick="refreshContainers()"><i class="fas fa-sync-alt"></i> Aktualisieren</button>
|
|
||||||
<button class="btn" onclick="expandAllContainers()"><i class="fas fa-expand-alt"></i> Alle erweitern</button>
|
|
||||||
<button class="btn" onclick="collapseAllContainers()"><i class="fas fa-compress-alt"></i> Alle einklappen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table id="container-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>CPU</th>
|
|
||||||
<th>Speicher</th>
|
|
||||||
<th>Netzwerk</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td colspan="6">Lade Container-Informationen...</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">Docker-Logs Analyse</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="log-container-select">Container auswählen:</label>
|
|
||||||
<select id="log-container-select">
|
|
||||||
<option value="">-- Container auswählen --</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="log-filter">Log-Filter:</label>
|
|
||||||
<input type="text" id="log-filter" placeholder="Nach Text filtern...">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn" onclick="loadContainerLogs()"><i class="fas fa-search"></i> Logs laden</button>
|
|
||||||
<button class="btn" onclick="downloadContainerLogs()"><i class="fas fa-download"></i> Logs herunterladen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="logs-container" id="container-logs">
|
|
||||||
<div class="log-placeholder">Container auswählen, um Logs anzuzeigen</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-section">
|
|
||||||
<h2><i class="fas fa-network-wired"></i> Netzwerkschnittstellen</h2>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
Netzwerkschnittstellen
|
|
||||||
<button class="btn btn-sm" onclick="refreshNetworkInterfaces()"><i class="fas fa-sync-alt"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="network-interfaces">
|
|
||||||
<div class="loading">Lade Netzwerkschnittstellen...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
Aktive Verbindungen
|
|
||||||
<button class="btn btn-sm" onclick="refreshActiveConnections()"><i class="fas fa-sync-alt"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<table id="connections-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Lokale Adresse</th>
|
|
||||||
<th>Remote-Adresse</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Prozess</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td colspan="4">Lade aktive Verbindungen...</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
Routing-Tabelle
|
|
||||||
<button class="btn btn-sm" onclick="loadRouteTable()"><i class="fas fa-sync-alt"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<pre id="route-table">Lade Routing-Tabelle...</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-section">
|
|
||||||
<h2><i class="fas fa-exclamation-triangle"></i> Diagnose-Tools</h2>
|
|
||||||
|
|
||||||
<div class="tabs">
|
|
||||||
<div class="tab active" data-tab="ping">Ping-Test</div>
|
|
||||||
<div class="tab" data-tab="traceroute">Traceroute</div>
|
|
||||||
<div class="tab" data-tab="dns">DNS-Abfrage</div>
|
|
||||||
<div class="tab" data-tab="port-scan">Port-Scan</div>
|
|
||||||
<div class="tab" data-tab="logs">Log-Analyse</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content active" id="ping-tab">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="ping-host">Host:</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" id="ping-host" placeholder="z.B. example.com oder 192.168.1.1">
|
|
||||||
<div class="input-group-append">
|
|
||||||
<button class="btn" onclick="pingHost()">Ping</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ping-result" class="status">
|
|
||||||
Führen Sie einen Ping-Test durch, um Ergebnisse zu sehen.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="traceroute-tab">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="traceroute-host">Host:</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" id="traceroute-host" placeholder="z.B. example.com oder 192.168.1.1">
|
|
||||||
<div class="input-group-append">
|
|
||||||
<button class="btn" onclick="tracerouteHost()">Traceroute</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="traceroute-result" class="status">
|
|
||||||
Führen Sie einen Traceroute-Test durch, um Ergebnisse zu sehen.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="dns-tab">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dns-host">Host:</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" id="dns-host" placeholder="z.B. example.com">
|
|
||||||
<div class="input-group-append">
|
|
||||||
<button class="btn" onclick="dnsLookup()">DNS-Abfrage</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dns-result" class="status">
|
|
||||||
Führen Sie eine DNS-Abfrage durch, um Ergebnisse zu sehen.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="port-scan-tab">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="port-scan-host">Host:</label>
|
|
||||||
<input type="text" id="port-scan-host" placeholder="z.B. example.com oder 192.168.1.1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="port-scan-range">Port-Bereich:</label>
|
|
||||||
<input type="text" id="port-scan-range" placeholder="z.B. 1-1024" value="1-1024">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn" onclick="startPortScan()">Port-Scan starten</button>
|
|
||||||
<button class="btn" onclick="checkPortScanStatus()">Status prüfen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="port-scan-status" class="status">
|
|
||||||
Kein Port-Scan aktiv.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="port-scan-results" class="results-container">
|
|
||||||
<table id="port-scan-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Port</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Dienst</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="logs-tab">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="log-type">Log-Typ:</label>
|
|
||||||
<select id="log-type">
|
|
||||||
<option value="backend">Backend-Logs</option>
|
|
||||||
<option value="frontend">Frontend-Logs</option>
|
|
||||||
<option value="debug">Debug-Server-Logs</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="log-lines">Anzahl Zeilen:</label>
|
|
||||||
<input type="number" id="log-lines" value="100" min="10" max="1000">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn" onclick="loadLogs()">Logs laden</button>
|
|
||||||
<button class="btn" onclick="analyzeLogs()">Logs analysieren</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="logs-container" id="log-content">
|
|
||||||
<div class="log-placeholder">Wählen Sie einen Log-Typ und klicken Sie auf "Logs laden"</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- JavaScript Code -->
|
|
||||||
<script src="{{ url_for('static', filename='js/debug-dashboard.js') }}"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,261 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>MYP Debug Server</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
h1, h2, h3 {
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
.section {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #34495e;
|
|
||||||
}
|
|
||||||
input[type="text"] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
.btn:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
}
|
|
||||||
.btn-success {
|
|
||||||
background-color: #2ecc71;
|
|
||||||
}
|
|
||||||
.btn-success:hover {
|
|
||||||
background-color: #27ae60;
|
|
||||||
}
|
|
||||||
.btn-warning {
|
|
||||||
background-color: #f39c12;
|
|
||||||
}
|
|
||||||
.btn-warning:hover {
|
|
||||||
background-color: #e67e22;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
.status-good {
|
|
||||||
background-color: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
.status-warning {
|
|
||||||
background-color: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
.status-error {
|
|
||||||
background-color: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
.interface-item {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-left: 4px solid #3498db;
|
|
||||||
}
|
|
||||||
.message {
|
|
||||||
display: none;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 10px 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.message-success {
|
|
||||||
background-color: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
.message-error {
|
|
||||||
background-color: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>MYP Debug Server</h1>
|
|
||||||
<p>Letzte Aktualisierung: {{ last_check }}</p>
|
|
||||||
|
|
||||||
<div id="message" class="message"></div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Netzwerkkonfiguration</h2>
|
|
||||||
<form id="configForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="backend_hostname">Backend Hostname/IP:</label>
|
|
||||||
<input type="text" id="backend_hostname" name="backend_hostname" value="{{ config.backend_hostname }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="backend_port">Backend Port:</label>
|
|
||||||
<input type="text" id="backend_port" name="backend_port" value="{{ config.backend_port }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="frontend_hostname">Frontend Hostname/IP:</label>
|
|
||||||
<input type="text" id="frontend_hostname" name="frontend_hostname" value="{{ config.frontend_hostname }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="frontend_port">Frontend Port:</label>
|
|
||||||
<input type="text" id="frontend_port" name="frontend_port" value="{{ config.frontend_port }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="btn" onclick="testConnection()">Verbindung testen</button>
|
|
||||||
<button type="button" class="btn btn-success" onclick="saveConfig()">Konfiguration speichern</button>
|
|
||||||
<button type="button" class="btn btn-warning" onclick="syncFrontend()">Frontend synchronisieren</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Verbindungsstatus</h2>
|
|
||||||
|
|
||||||
<h3>Backend</h3>
|
|
||||||
<div class="status {{ 'status-good' if 'Verbunden' in backend_status else 'status-error' }}">
|
|
||||||
{{ backend_status }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Frontend</h3>
|
|
||||||
<div class="status {{ 'status-good' if 'Verbunden' in frontend_status else 'status-error' }}">
|
|
||||||
{{ frontend_status }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Netzwerkschnittstellen</h2>
|
|
||||||
{% for interface in interfaces %}
|
|
||||||
<div class="interface-item">
|
|
||||||
<strong>{{ interface.name }}</strong>: {{ interface.address }}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function showMessage(message, isError = false) {
|
|
||||||
const messageEl = document.getElementById('message');
|
|
||||||
messageEl.textContent = message;
|
|
||||||
messageEl.className = isError ? 'message message-error' : 'message message-success';
|
|
||||||
messageEl.style.display = 'block';
|
|
||||||
|
|
||||||
// Verstecke Nachricht nach 5 Sekunden
|
|
||||||
setTimeout(() => {
|
|
||||||
messageEl.style.display = 'none';
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormData() {
|
|
||||||
return {
|
|
||||||
backend_hostname: document.getElementById('backend_hostname').value,
|
|
||||||
backend_port: document.getElementById('backend_port').value,
|
|
||||||
frontend_hostname: document.getElementById('frontend_hostname').value,
|
|
||||||
frontend_port: document.getElementById('frontend_port').value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function testConnection() {
|
|
||||||
const formData = getFormData();
|
|
||||||
const data = new FormData();
|
|
||||||
|
|
||||||
for (const key in formData) {
|
|
||||||
data.append(key, formData[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch('/test-connection', {
|
|
||||||
method: 'POST',
|
|
||||||
body: data
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
let message = 'Backend: ';
|
|
||||||
message += data.results.backend.ping ? 'Ping OK' : 'Ping fehlgeschlagen';
|
|
||||||
message += ', ';
|
|
||||||
message += data.results.backend.connection ? 'Verbindung OK' : 'Keine Verbindung';
|
|
||||||
|
|
||||||
message += ' | Frontend: ';
|
|
||||||
message += data.results.frontend.ping ? 'Ping OK' : 'Ping fehlgeschlagen';
|
|
||||||
message += ', ';
|
|
||||||
message += data.results.frontend.connection ? 'Verbindung OK' : 'Keine Verbindung';
|
|
||||||
|
|
||||||
showMessage(message, !(data.results.backend.connection && data.results.frontend.connection));
|
|
||||||
} else {
|
|
||||||
showMessage(data.message, true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showMessage('Fehler bei der Verbindungsprüfung: ' + error, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveConfig() {
|
|
||||||
const formData = getFormData();
|
|
||||||
const data = new FormData();
|
|
||||||
|
|
||||||
for (const key in formData) {
|
|
||||||
data.append(key, formData[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch('/save-config', {
|
|
||||||
method: 'POST',
|
|
||||||
body: data
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
showMessage(data.message, !data.success);
|
|
||||||
if (data.success) {
|
|
||||||
// Aktualisiere die Seite nach erfolgreicher Speicherung
|
|
||||||
setTimeout(() => {
|
|
||||||
location.reload();
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showMessage('Fehler beim Speichern: ' + error, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncFrontend() {
|
|
||||||
fetch('/sync-frontend', {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
showMessage(data.message, !data.success);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showMessage('Fehler bei der Frontend-Synchronisierung: ' + error, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,150 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>MYP Backend Debug-Server</title>
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔍</text></svg>">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<div class="header-content">
|
|
||||||
<h1>MYP Backend Debug-Server</h1>
|
|
||||||
<div class="server-info">
|
|
||||||
<span id="hostname"><strong>Hostname:</strong> {{ hostname }}</span>
|
|
||||||
<span id="ip-address"><strong>IP-Adresse:</strong> {{ ip_address }}</span>
|
|
||||||
<span id="timestamp"><strong>Timestamp:</strong> {{ timestamp }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<button class="nav-button active" data-target="system-panel">System</button>
|
|
||||||
<button class="nav-button" data-target="network-panel">Netzwerk</button>
|
|
||||||
<button class="nav-button" data-target="docker-panel">Docker</button>
|
|
||||||
<button class="nav-button" data-target="tools-panel">Tools</button>
|
|
||||||
<button class="nav-button" data-target="backend-panel">Backend-Status</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<!-- System-Informationen -->
|
|
||||||
<section id="system-panel" class="panel active">
|
|
||||||
<h2>Systeminformationen</h2>
|
|
||||||
<div class="card-container">
|
|
||||||
<div class="card">
|
|
||||||
<h3>Betriebssystem</h3>
|
|
||||||
<div id="platform-info">Wird geladen...</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Hardware</h3>
|
|
||||||
<div id="hardware-info">Wird geladen...</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Speicher</h3>
|
|
||||||
<div id="memory-info">Wird geladen...</div>
|
|
||||||
<div class="progress-container">
|
|
||||||
<div id="memory-bar" class="progress-bar"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Festplatte</h3>
|
|
||||||
<div id="disk-info">Wird geladen...</div>
|
|
||||||
<div class="progress-container">
|
|
||||||
<div id="disk-bar" class="progress-bar"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Netzwerk-Informationen -->
|
|
||||||
<section id="network-panel" class="panel">
|
|
||||||
<h2>Netzwerkinformationen</h2>
|
|
||||||
<div class="card-container">
|
|
||||||
<div class="card full-width">
|
|
||||||
<h3>Netzwerkschnittstellen</h3>
|
|
||||||
<div id="network-interfaces">Wird geladen...</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>DNS Server</h3>
|
|
||||||
<div id="dns-servers">Wird geladen...</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Standard-Gateway</h3>
|
|
||||||
<div id="default-gateway">Wird geladen...</div>
|
|
||||||
</div>
|
|
||||||
<div class="card full-width">
|
|
||||||
<h3>Aktive Verbindungen</h3>
|
|
||||||
<div id="active-connections">Wird geladen...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Docker-Informationen -->
|
|
||||||
<section id="docker-panel" class="panel">
|
|
||||||
<h2>Docker-Informationen</h2>
|
|
||||||
<div class="card-container">
|
|
||||||
<div class="card">
|
|
||||||
<h3>Docker-Status</h3>
|
|
||||||
<div id="docker-status">Wird geladen...</div>
|
|
||||||
</div>
|
|
||||||
<div class="card full-width">
|
|
||||||
<h3>Container</h3>
|
|
||||||
<div id="docker-containers">Wird geladen...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Netzwerk-Tools -->
|
|
||||||
<section id="tools-panel" class="panel">
|
|
||||||
<h2>Netzwerk-Tools</h2>
|
|
||||||
<div class="card-container">
|
|
||||||
<div class="card tool-card">
|
|
||||||
<h3>Ping</h3>
|
|
||||||
<div class="tool-input">
|
|
||||||
<input type="text" id="ping-host" placeholder="Hostname oder IP-Adresse">
|
|
||||||
<button id="ping-button">Ping</button>
|
|
||||||
</div>
|
|
||||||
<pre id="ping-result" class="result-box">Geben Sie einen Hostnamen oder eine IP-Adresse ein...</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card tool-card">
|
|
||||||
<h3>Traceroute</h3>
|
|
||||||
<div class="tool-input">
|
|
||||||
<input type="text" id="traceroute-host" placeholder="Hostname oder IP-Adresse">
|
|
||||||
<button id="traceroute-button">Traceroute</button>
|
|
||||||
</div>
|
|
||||||
<pre id="traceroute-result" class="result-box">Geben Sie einen Hostnamen oder eine IP-Adresse ein...</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card tool-card">
|
|
||||||
<h3>DNS-Lookup</h3>
|
|
||||||
<div class="tool-input">
|
|
||||||
<input type="text" id="nslookup-host" placeholder="Hostname oder IP-Adresse">
|
|
||||||
<button id="nslookup-button">NSLookup</button>
|
|
||||||
</div>
|
|
||||||
<pre id="nslookup-result" class="result-box">Geben Sie einen Hostnamen oder eine IP-Adresse ein...</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Backend-Status -->
|
|
||||||
<section id="backend-panel" class="panel">
|
|
||||||
<h2>Backend-Status</h2>
|
|
||||||
<div class="card-container">
|
|
||||||
<div class="card">
|
|
||||||
<h3>Haupt-Backend</h3>
|
|
||||||
<div id="main-backend-status">Wird geladen...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>© 2025 MYP (Manage your Printer) | Debug-Server v1.0.0</p>
|
|
||||||
<p>Netzwerk- und Systemdiagnose-Tool</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,8 +0,0 @@
|
|||||||
# MYP Backend Cron-Jobs
|
|
||||||
# Installiere mit: crontab crontab-example
|
|
||||||
|
|
||||||
# Prüfe alle 5 Minuten auf abgelaufene Reservierungen und schalte Steckdosen aus
|
|
||||||
*/5 * * * * cd /pfad/zum/projektarbeit-myp/backend && /pfad/zur/venv/bin/flask check-jobs >> /pfad/zum/projektarbeit-myp/backend/logs/cron.log 2>&1
|
|
||||||
|
|
||||||
# Tägliche Sicherung der Datenbank um 3:00 Uhr
|
|
||||||
0 3 * * * cd /pfad/zum/projektarbeit-myp/backend && cp instance/myp.db instance/backups/myp-$(date +\%Y\%m\%d).db
|
|
@ -1,84 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# MYP Datenbank Initialisierungs-Skript
|
|
||||||
# Dieses Skript erstellt die erforderlichen Datenbanktabellen für das MYP Backend
|
|
||||||
|
|
||||||
echo "=== MYP Datenbank Initialisierung ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Prüfe, ob sqlite3 installiert ist
|
|
||||||
if ! command -v sqlite3 &> /dev/null; then
|
|
||||||
echo "FEHLER: sqlite3 ist nicht installiert."
|
|
||||||
echo "Bitte installiere sqlite3 mit deinem Paketmanager, z.B. 'apt install sqlite3'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Erstelle Instance-Ordner, falls nicht vorhanden
|
|
||||||
echo "Erstelle instance-Ordner, falls nicht vorhanden..."
|
|
||||||
mkdir -p instance/backups
|
|
||||||
|
|
||||||
# Prüfen, ob die Datenbank bereits existiert
|
|
||||||
if [ -f "instance/myp.db" ]; then
|
|
||||||
echo "Datenbank existiert bereits."
|
|
||||||
echo "Erstelle Backup in instance/backups..."
|
|
||||||
cp instance/myp.db "instance/backups/myp_$(date '+%Y%m%d_%H%M%S').db"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Erstelle die Datenbank und ihre Tabellen
|
|
||||||
echo "Erstelle neue Datenbank..."
|
|
||||||
sqlite3 instance/myp.db <<EOF
|
|
||||||
PRAGMA foreign_keys = ON;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
username TEXT UNIQUE NOT NULL,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
display_name TEXT,
|
|
||||||
email TEXT UNIQUE,
|
|
||||||
role TEXT DEFAULT 'user'
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS session (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
expires_at TIMESTAMP NOT NULL,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS socket (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
status INTEGER DEFAULT 0,
|
|
||||||
ip_address TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS job (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
socket_id TEXT NOT NULL,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
start_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
duration_in_minutes INTEGER NOT NULL,
|
|
||||||
comments TEXT,
|
|
||||||
aborted INTEGER DEFAULT 0,
|
|
||||||
abort_reason TEXT,
|
|
||||||
FOREIGN KEY (socket_id) REFERENCES socket (id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Setze Berechtigungen für die Datenbankdatei
|
|
||||||
chmod 644 instance/myp.db
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Datenbank-Initialisierung abgeschlossen ==="
|
|
||||||
echo ""
|
|
||||||
echo "Du kannst jetzt einen Admin-Benutzer über die Web-Oberfläche registrieren."
|
|
||||||
echo "Der erste registrierte Benutzer wird automatisch zum Admin."
|
|
||||||
echo ""
|
|
||||||
echo "Starte den Server mit:"
|
|
||||||
echo "python app.py"
|
|
||||||
echo ""
|
|
||||||
echo "Alternativ kannst du einen Admin-Benutzer über die API erstellen mit:"
|
|
||||||
echo "curl -X POST http://localhost:5000/api/create-initial-admin -H \"Content-Type: application/json\" -d '{\"username\":\"admin\",\"password\":\"password\",\"displayName\":\"Administrator\"}'"
|
|
||||||
echo ""
|
|
@ -1,95 +0,0 @@
|
|||||||
import requests
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Basis-URL inkl. Token
|
|
||||||
url = "http://192.168.0.102:80/app?token=9DFAC92C53CEC92E67A9CB2E00B3CB2F"
|
|
||||||
|
|
||||||
# HTTP-Header wie in der Originalanfrage
|
|
||||||
headers = {
|
|
||||||
"Referer": "http://192.168.0.102:80",
|
|
||||||
"Accept": "application/json",
|
|
||||||
"requestByApp": "true",
|
|
||||||
"Content-Type": "application/json; charset=UTF-8",
|
|
||||||
"Host": "192.168.0.102",
|
|
||||||
"Connection": "Keep-Alive",
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"User-Agent": "okhttp/3.14.9"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Liste der Payloads (als Python-Dictionaries)
|
|
||||||
payloads = [
|
|
||||||
{
|
|
||||||
"method": "securePassthrough",
|
|
||||||
"params": {
|
|
||||||
"request": (
|
|
||||||
"ZC4CHp6bbfBO1rtmuH6I+TStBIiFRfQpayYPwet5NBmL35dib5xXHeEeLM7c0OSQSyxO6fnbXrC1\n"
|
|
||||||
"gXdfowwwq4Fum9ispgt8yT7cgbDcqnoVrhxEtHIDfuwLh8YAGmDSfTMo/JlsGspWPYMKd1EWXtb5\n"
|
|
||||||
"gP9FA9LHnV2kxKsNSPQ=\n"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "securePassthrough",
|
|
||||||
"params": {
|
|
||||||
"request": (
|
|
||||||
"k111EbfCcfVzAouNbu1vyos9Ltsg+a97n4xUUQMviQVJfhqxvKOhv1SrvEk2LvpD0LwNVUNPZdwU\n"
|
|
||||||
"6pH5E/NOwdc1WzTPeqHiY760GpUuqn0tToHEHEyO2HaSKdrAYnw2gN410bvHb0pM3gYWS43eOA==\n"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "securePassthrough",
|
|
||||||
"params": {
|
|
||||||
"request": (
|
|
||||||
"7/uYVDwyNfFhg9y7rHyp+4AGKBYQPyaBN6cFMl9j4ER/JpJTcGBdaUteSmx8P8Fkz+b2kkNLjYa2\n"
|
|
||||||
"wQr2gA3m6vEq9jpnAF2V3fv9c4Yg9gja9MlTIZqM6EdMi7YbfbhLme34Bh8kMcohDR3u1F4DwFDz\n"
|
|
||||||
"hNZPckf/CegbY9KGFeGwT4rWyX3BTk9+FE7ldtJn\n"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "securePassthrough",
|
|
||||||
"params": {
|
|
||||||
"request": (
|
|
||||||
"EjWZb+YYS9tihgLdX4x+Wwx7q+e5X/ZHicr4jOnYmpFToDANzpm5ZpzD49BITcTCdQMOHlJBis85\n"
|
|
||||||
"9GX6Hv8j66OITyH0XmfG9dQo2tgIykyagCZIofr/BpAWYX4aRaOkU4z14mVa2XpDtHJQjc+pXYkh\n"
|
|
||||||
"JuWvLE+h01U5RoyPtvE=\n"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "securePassthrough",
|
|
||||||
"params": {
|
|
||||||
"request": (
|
|
||||||
"OwyTsm5HdB/ReJMhVRrkjnV0NLTanw6iXOxVrDDexT456edWuwKiBOsZUyBHmUyJKgiPQzOXqyWWi220bX8IjLX4q8YNgPwRlj+7nRbfzpC/I57wBZBTWIt626pSdIH0vpiuPq84KMfPD5BB2p78/LjsqlzyeLGYzkSsGRBMT8TnLMDFzZE864nfDUZ9muH2kk8NRMN9l6xoCXBJqGA9q8XxIWRTpsl0kTx52kUszY69hYlfFSrrCDIls1ykul14/T1NtOVF8KOgiwaSGOZf7L4QlbhYvRj9kkVVkrxhlwt8jtMqfJKEqq+CIPh3Mp4440WYMLRo6VNIEJ3pWjplkJmc+htnYC4FwVgT7mHZ8eeGGKBvsJz+78gTaHnGBnwZ26I8UdFparyp6QXpOhK9zFmGVh0yapiTHo6jOOI+4Q3Ru+aPnidX/ZASPmR7CZO70CUpvv9zIKJnrAaoTMmH7A6+kcmCRLgLFaTaM+4DFmiz6JGP+4W7MmVPJxxvn0IFlo1P/xwNDuL3T6GLUIEVNk89JG5roBm7AdchUZJO38dGZ0eFiiTK/NhKPvjj+fk9A4FGh7EDshXZhL2u50cdLcdUtcP/CAMDjgWlMm4Kk3vxMQO+UGE+jsB7NkaulmTW1jcl+PSnAE5P71oqVVQ0ng==\n"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "securePassthrough",
|
|
||||||
"params": {
|
|
||||||
"request": (
|
|
||||||
"7/uYVDwyNfFhg9y7rHyp+4AGKBYQPyaBN6cFMl9j4ER/JpJTcGBdaUteSmx8P8FkURmv/LWV1FpO\n"
|
|
||||||
"M3RWvsiC5UAsei2G+vwTVuQpOPjKKAx+qwftr9Qs2mSkPNjNLpWHK68EZkIw+h04TQkt0Q99Dirg\n"
|
|
||||||
"0BcrPgHTVKjiK8mdZ6w6gcld/h/FOKYMqJrP0Z+2\n"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "securePassthrough",
|
|
||||||
"params": {
|
|
||||||
"request": (
|
|
||||||
"ZE/+XlUmTA9D3DFfp4x3xhS3vdsQ+60tz4TOodtZDby/4DPoqk9EBvJZ1JtUCr5c0AHuv/sfwcvN\n"
|
|
||||||
"Vx1zJP9RkltrAKVTWoaESAeewLozpXt/x0s/jkYC1rh7eTrxm+nYTZ5LJgNtcQq8yJxhEPez1w==\n"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Sende die Payloads sequenziell per POST-Anfrage
|
|
||||||
for idx, payload in enumerate(payloads, start=1):
|
|
||||||
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
|
||||||
print(f"Anfrage {idx}:")
|
|
||||||
print("Status Code:", response.status_code)
|
|
||||||
print("Response Text:", response.text)
|
|
||||||
print("-" * 60)
|
|
Binary file not shown.
@ -1,128 +0,0 @@
|
|||||||
import requests
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Constants from the Wireshark capture
|
|
||||||
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
|
|
||||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCMl89OZsjqE8yZ9TQhUb9h539WTX3U8Y5YCNdp
|
|
||||||
OhuXvLFYcAT5mvC074VFROmD0xhvw5hrwESOisqpPPU9r78JpLuYUKd+/aidvykqBT8OW5rDLb6d
|
|
||||||
O9FO6Gc+bV8L8ttHVlBFoX69EqiRhcreGPG6FQz4JqGJF4T1nFi0EvALXwIDAQAB
|
|
||||||
-----END PUBLIC KEY-----"""
|
|
||||||
|
|
||||||
# Vorbereitete verschlüsselte Befehle (aus Wireshark extrahiert)
|
|
||||||
COMMAND_ON = """ps0Puxc37EK4PhfcevceL3lyyDrjwLT1+443DDXNbcNRsltlgCQ6+oXgsrE2Pl5OhV73ZI/oM5Nj
|
|
||||||
37cWEaHpXPiHdr1W0cD3aJ5qJ55TfTRkHP9xcMNQJHCn6aWPEHpR7xvvXW9WbJWfShnE2Xdvmw==
|
|
||||||
"""
|
|
||||||
|
|
||||||
COMMAND_OFF = """FlO5i3DRcrUmu2ZwIIv8b68EisGu8VCuqfGOydaR+xCA0n3f2W/EcqVj8MurRBFXYTrZ/uwa1W26
|
|
||||||
ftCfvhdXNebBRwHr9Rj3id4bVfltJ8eT5/R3xY8kputklW2mrw9UfdISzAJqOPp9KZcU4K9p8g==
|
|
||||||
"""
|
|
||||||
|
|
||||||
class TapoP115Controller:
|
|
||||||
def __init__(self, device_ip):
|
|
||||||
self.device_ip = device_ip
|
|
||||||
self.session_id = None
|
|
||||||
self.token = None
|
|
||||||
|
|
||||||
def perform_handshake(self):
|
|
||||||
"""Führt den ersten Handshake durch und speichert die Session-ID"""
|
|
||||||
handshake_data = {
|
|
||||||
"method": "handshake",
|
|
||||||
"params": {
|
|
||||||
"key": PUBLIC_KEY
|
|
||||||
},
|
|
||||||
"requestTimeMils": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Referer": f"http://{self.device_ip}:80",
|
|
||||||
"Accept": "application/json",
|
|
||||||
"requestByApp": "true",
|
|
||||||
"Content-Type": "application/json; charset=UTF-8"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
f"http://{self.device_ip}/app",
|
|
||||||
json=handshake_data,
|
|
||||||
headers=headers
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
if data["error_code"] == 0:
|
|
||||||
# Session-ID aus dem Cookie extrahieren
|
|
||||||
self.session_id = response.cookies.get("TP_SESSIONID")
|
|
||||||
print(f"Handshake erfolgreich, Session-ID: {self.session_id}")
|
|
||||||
|
|
||||||
# In einem echten Szenario würden wir hier den verschlüsselten Schlüssel entschlüsseln
|
|
||||||
# Da wir keinen privaten Schlüssel haben, speichern wir nur die Antwort
|
|
||||||
encrypted_key = data["result"]["key"]
|
|
||||||
print(f"Verschlüsselter Schlüssel: {encrypted_key}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
print("Handshake fehlgeschlagen")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def send_command(self, encrypted_command):
|
|
||||||
"""Sendet einen vorbereiteten verschlüsselten Befehl"""
|
|
||||||
if not self.session_id:
|
|
||||||
print("Keine Session-ID. Bitte zuerst Handshake durchführen.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Token aus der Wireshark-Aufnahme (könnte sich ändern, oder vom Gerät abhängen)
|
|
||||||
token = "9DFAC92C53CEC92E67A9CB2E00B3CB2F"
|
|
||||||
|
|
||||||
secure_data = {
|
|
||||||
"method": "securePassthrough",
|
|
||||||
"params": {
|
|
||||||
"request": encrypted_command
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Referer": f"http://{self.device_ip}:80",
|
|
||||||
"Accept": "application/json",
|
|
||||||
"requestByApp": "true",
|
|
||||||
"Content-Type": "application/json; charset=UTF-8",
|
|
||||||
"Cookie": f"TP_SESSIONID={self.session_id}"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
f"http://{self.device_ip}/app?token={token}",
|
|
||||||
json=secure_data,
|
|
||||||
headers=headers
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
if data["error_code"] == 0:
|
|
||||||
# In einem echten Szenario würden wir die Antwort entschlüsseln
|
|
||||||
encrypted_response = data["result"]["response"]
|
|
||||||
print("Befehl erfolgreich gesendet")
|
|
||||||
return encrypted_response
|
|
||||||
|
|
||||||
print("Fehler beim Senden des Befehls")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def turn_on(self):
|
|
||||||
"""Schaltet die Steckdose ein"""
|
|
||||||
return self.send_command(COMMAND_ON)
|
|
||||||
|
|
||||||
def turn_off(self):
|
|
||||||
"""Schaltet die Steckdose aus"""
|
|
||||||
return self.send_command(COMMAND_OFF)
|
|
||||||
|
|
||||||
# Verwendungsbeispiel
|
|
||||||
if __name__ == "__main__":
|
|
||||||
controller = TapoP115Controller("192.168.0.102")
|
|
||||||
|
|
||||||
# Handshake durchführen
|
|
||||||
if controller.perform_handshake():
|
|
||||||
# Steckdose einschalten
|
|
||||||
controller.turn_on()
|
|
||||||
|
|
||||||
# Kurze Pause (im echten Code mit time.sleep)
|
|
||||||
print("Steckdose ist eingeschaltet")
|
|
||||||
|
|
||||||
# Steckdose ausschalten
|
|
||||||
controller.turn_off()
|
|
||||||
print("Steckdose ist ausgeschaltet")
|
|
@ -1,9 +0,0 @@
|
|||||||
from PyP100 import PyP100
|
|
||||||
|
|
||||||
p100 = PyP100.P100("192.168.0.102", "till.tomczak@mercedes-benz.com", "Agent045") #Creates a P100 plug object
|
|
||||||
|
|
||||||
p100.handshake() #Creates the cookies required for further methods
|
|
||||||
p100.login() #Sends credentials to the plug and creates AES Key and IV for further methods
|
|
||||||
|
|
||||||
p100.turnOn() #Turns the connected plug on
|
|
||||||
p100.turnOff() #Turns the connected plug off
|
|
@ -1,253 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from app import app, db, User, Printer, PrintJob
|
|
||||||
|
|
||||||
class MYPBackendTestCase(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
# Temporäre Datenbank für Tests
|
|
||||||
self.db_fd, app.config['DATABASE'] = tempfile.mkstemp()
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE']
|
|
||||||
app.config['TESTING'] = True
|
|
||||||
self.app = app.test_client()
|
|
||||||
|
|
||||||
# Datenbank-Tabellen erstellen und Test-Daten einfügen
|
|
||||||
with app.app_context():
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
# Admin-Benutzer erstellen
|
|
||||||
admin = User(username='admin_test', email='admin@test.com', role='admin')
|
|
||||||
admin.set_password('admin')
|
|
||||||
db.session.add(admin)
|
|
||||||
|
|
||||||
# Normaler Benutzer erstellen
|
|
||||||
user = User(username='user_test', email='user@test.com', role='user')
|
|
||||||
user.set_password('user')
|
|
||||||
db.session.add(user)
|
|
||||||
|
|
||||||
# Drucker erstellen
|
|
||||||
printer1 = Printer(name='Printer 1', location='Room A', type='3D',
|
|
||||||
status='available', description='Test printer 1')
|
|
||||||
printer2 = Printer(name='Printer 2', location='Room B', type='3D',
|
|
||||||
status='busy', description='Test printer 2')
|
|
||||||
db.session.add(printer1)
|
|
||||||
db.session.add(printer2)
|
|
||||||
|
|
||||||
# Job erstellen
|
|
||||||
start_time = datetime.utcnow()
|
|
||||||
end_time = start_time + timedelta(minutes=60)
|
|
||||||
job = PrintJob(title='Test Job', start_time=start_time, end_time=end_time,
|
|
||||||
duration=60, status='active', comments='Test job',
|
|
||||||
user_id=2, printer_id=2)
|
|
||||||
db.session.add(job)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
# Aufräumen nach dem Test
|
|
||||||
os.close(self.db_fd)
|
|
||||||
os.unlink(app.config['DATABASE'])
|
|
||||||
|
|
||||||
def get_token(self, username, password):
|
|
||||||
response = self.app.post('/api/auth/login',
|
|
||||||
data=json.dumps({'username': username, 'password': password}),
|
|
||||||
content_type='application/json')
|
|
||||||
data = json.loads(response.data)
|
|
||||||
return data.get('token')
|
|
||||||
|
|
||||||
def test_login(self):
|
|
||||||
# Test: Erfolgreicher Login
|
|
||||||
response = self.app.post('/api/auth/login',
|
|
||||||
data=json.dumps({'username': 'admin_test', 'password': 'admin'}),
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertIn('token', data)
|
|
||||||
self.assertIn('user', data)
|
|
||||||
|
|
||||||
# Test: Fehlgeschlagener Login (falsches Passwort)
|
|
||||||
response = self.app.post('/api/auth/login',
|
|
||||||
data=json.dumps({'username': 'admin_test', 'password': 'wrong'}),
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 401)
|
|
||||||
|
|
||||||
def test_register(self):
|
|
||||||
# Test: Erfolgreiche Registrierung
|
|
||||||
response = self.app.post('/api/auth/register',
|
|
||||||
data=json.dumps({
|
|
||||||
'username': 'new_user',
|
|
||||||
'email': 'new@test.com',
|
|
||||||
'password': 'password'
|
|
||||||
}),
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
|
|
||||||
# Test: Doppelte Registrierung
|
|
||||||
response = self.app.post('/api/auth/register',
|
|
||||||
data=json.dumps({
|
|
||||||
'username': 'new_user',
|
|
||||||
'email': 'another@test.com',
|
|
||||||
'password': 'password'
|
|
||||||
}),
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 400)
|
|
||||||
|
|
||||||
def test_get_printers(self):
|
|
||||||
# Test: Drucker abrufen
|
|
||||||
response = self.app.get('/api/printers')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(len(data), 2)
|
|
||||||
|
|
||||||
def test_get_single_printer(self):
|
|
||||||
# Test: Einzelnen Drucker abrufen
|
|
||||||
response = self.app.get('/api/printers/1')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(data['name'], 'Printer 1')
|
|
||||||
|
|
||||||
def test_create_printer(self):
|
|
||||||
# Als Admin einen Drucker erstellen
|
|
||||||
token = self.get_token('admin_test', 'admin')
|
|
||||||
response = self.app.post('/api/printers',
|
|
||||||
headers={'Authorization': f'Bearer {token}'},
|
|
||||||
data=json.dumps({
|
|
||||||
'name': 'New Printer',
|
|
||||||
'location': 'Room C',
|
|
||||||
'type': '3D',
|
|
||||||
'description': 'New test printer'
|
|
||||||
}),
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(data['name'], 'New Printer')
|
|
||||||
|
|
||||||
def test_update_printer(self):
|
|
||||||
# Als Admin einen Drucker aktualisieren
|
|
||||||
token = self.get_token('admin_test', 'admin')
|
|
||||||
response = self.app.put('/api/printers/1',
|
|
||||||
headers={'Authorization': f'Bearer {token}'},
|
|
||||||
data=json.dumps({
|
|
||||||
'name': 'Updated Printer',
|
|
||||||
'location': 'Room D'
|
|
||||||
}),
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(data['name'], 'Updated Printer')
|
|
||||||
self.assertEqual(data['location'], 'Room D')
|
|
||||||
|
|
||||||
def test_delete_printer(self):
|
|
||||||
# Als Admin einen Drucker löschen
|
|
||||||
token = self.get_token('admin_test', 'admin')
|
|
||||||
response = self.app.delete('/api/printers/1',
|
|
||||||
headers={'Authorization': f'Bearer {token}'})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
# Überprüfen, ob der Drucker wirklich gelöscht wurde
|
|
||||||
response = self.app.get('/api/printers/1')
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
def test_get_jobs_as_admin(self):
|
|
||||||
# Als Admin alle Jobs abrufen
|
|
||||||
token = self.get_token('admin_test', 'admin')
|
|
||||||
response = self.app.get('/api/jobs',
|
|
||||||
headers={'Authorization': f'Bearer {token}'})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(len(data), 1)
|
|
||||||
|
|
||||||
def test_get_jobs_as_user(self):
|
|
||||||
# Als normaler Benutzer nur eigene Jobs abrufen
|
|
||||||
token = self.get_token('user_test', 'user')
|
|
||||||
response = self.app.get('/api/jobs',
|
|
||||||
headers={'Authorization': f'Bearer {token}'})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(len(data), 1) # Der Benutzer hat einen Job
|
|
||||||
|
|
||||||
def test_create_job(self):
|
|
||||||
# Als Benutzer einen Job erstellen
|
|
||||||
token = self.get_token('user_test', 'user')
|
|
||||||
response = self.app.post('/api/jobs',
|
|
||||||
headers={'Authorization': f'Bearer {token}'},
|
|
||||||
data=json.dumps({
|
|
||||||
'title': 'New Job',
|
|
||||||
'printer_id': 1,
|
|
||||||
'duration': 30,
|
|
||||||
'comments': 'Test job creation'
|
|
||||||
}),
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(data['title'], 'New Job')
|
|
||||||
self.assertEqual(data['duration'], 30)
|
|
||||||
|
|
||||||
def test_update_job(self):
|
|
||||||
# Als Benutzer den eigenen Job aktualisieren
|
|
||||||
token = self.get_token('user_test', 'user')
|
|
||||||
response = self.app.put('/api/jobs/1',
|
|
||||||
headers={'Authorization': f'Bearer {token}'},
|
|
||||||
data=json.dumps({
|
|
||||||
'comments': 'Updated comments',
|
|
||||||
'duration': 15 # Verlängerung
|
|
||||||
}),
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(data['comments'], 'Updated comments')
|
|
||||||
self.assertEqual(data['duration'], 75) # 60 + 15
|
|
||||||
|
|
||||||
def test_complete_job(self):
|
|
||||||
# Als Benutzer einen Job als abgeschlossen markieren
|
|
||||||
token = self.get_token('user_test', 'user')
|
|
||||||
response = self.app.put('/api/jobs/1',
|
|
||||||
headers={'Authorization': f'Bearer {token}'},
|
|
||||||
data=json.dumps({
|
|
||||||
'status': 'completed'
|
|
||||||
}),
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(data['status'], 'completed')
|
|
||||||
|
|
||||||
# Überprüfen, ob der Drucker wieder verfügbar ist
|
|
||||||
response = self.app.get('/api/printers/2')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(data['status'], 'available')
|
|
||||||
|
|
||||||
def test_get_remaining_time(self):
|
|
||||||
# Test: Verbleibende Zeit für einen aktiven Job abrufen
|
|
||||||
response = self.app.get('/api/job/1/remaining-time')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertIn('remaining_minutes', data)
|
|
||||||
# Der genaue Wert kann nicht überprüft werden, da er von der Zeit abhängt
|
|
||||||
|
|
||||||
def test_stats(self):
|
|
||||||
# Als Admin Statistiken abrufen
|
|
||||||
token = self.get_token('admin_test', 'admin')
|
|
||||||
response = self.app.get('/api/stats',
|
|
||||||
headers={'Authorization': f'Bearer {token}'})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertIn('printers', data)
|
|
||||||
self.assertIn('jobs', data)
|
|
||||||
self.assertIn('users', data)
|
|
||||||
self.assertEqual(data['printers']['total'], 2)
|
|
||||||
self.assertEqual(data['jobs']['total'], 1)
|
|
||||||
self.assertEqual(data['users']['total'], 2)
|
|
||||||
|
|
||||||
def test_test_endpoint(self):
|
|
||||||
# Test: API-Test-Endpunkt
|
|
||||||
response = self.app.get('/api/test')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
data = json.loads(response.data)
|
|
||||||
self.assertEqual(data['message'], 'MYP Backend API funktioniert!')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
@ -1,178 +0,0 @@
|
|||||||
# 🏭 MYP Backend - Standalone Server Konfiguration
|
|
||||||
# Backend-Service als vollständig unabhängiger Server
|
|
||||||
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# === BACKEND SERVICE ===
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
- BUILDKIT_INLINE_CACHE=1
|
|
||||||
image: myp/backend:latest
|
|
||||||
container_name: myp-backend-standalone
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
environment:
|
|
||||||
# Flask-Konfiguration
|
|
||||||
- FLASK_APP=app.py
|
|
||||||
- FLASK_ENV=${FLASK_ENV:-production}
|
|
||||||
- PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
# Datenbank
|
|
||||||
- DATABASE_PATH=${DATABASE_PATH:-instance/myp.db}
|
|
||||||
|
|
||||||
# Sicherheit
|
|
||||||
- SECRET_KEY=${SECRET_KEY:-7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F}
|
|
||||||
- JWT_SECRET=${JWT_SECRET:-secure-jwt-secret}
|
|
||||||
|
|
||||||
# CORS-Konfiguration für Frontend-Zugriff
|
|
||||||
- CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000,https://frontend.myp.local}
|
|
||||||
|
|
||||||
# Drucker-Konfiguration
|
|
||||||
- "PRINTERS=${PRINTERS:-{\"Drucker 1\": {\"ip\": \"192.168.0.100\"}, \"Drucker 2\": {\"ip\": \"192.168.0.101\"}, \"Drucker 3\": {\"ip\": \"192.168.0.102\"}, \"Drucker 4\": {\"ip\": \"192.168.0.103\"}, \"Drucker 5\": {\"ip\": \"192.168.0.104\"}, \"Drucker 6\": {\"ip\": \"192.168.0.106\"}}}"
|
|
||||||
|
|
||||||
# TAPO Smart Plug
|
|
||||||
- TAPO_USERNAME=${TAPO_USERNAME:-till.tomczak@mercedes-benz.com}
|
|
||||||
- TAPO_PASSWORD=${TAPO_PASSWORD:-744563017196A}
|
|
||||||
|
|
||||||
# Netzwerk
|
|
||||||
- HOST=0.0.0.0
|
|
||||||
- PORT=5000
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- backend_instance:/app/instance
|
|
||||||
- backend_logs:/app/logs
|
|
||||||
- backend_migrations:/app/migrations
|
|
||||||
- ./config:/app/config:ro
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- "5000:5000" # Direkter Port-Zugang für Backend-Server
|
|
||||||
|
|
||||||
networks:
|
|
||||||
- backend-network
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
labels:
|
|
||||||
- "service.type=backend"
|
|
||||||
- "service.name=myp-backend"
|
|
||||||
- "service.environment=${FLASK_ENV:-production}"
|
|
||||||
|
|
||||||
# === BACKEND DATENBANK (Optional: Separate PostgreSQL) ===
|
|
||||||
backend-db:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: myp-backend-db
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=${DB_NAME:-myp_backend}
|
|
||||||
- POSTGRES_USER=${DB_USER:-myp_user}
|
|
||||||
- POSTGRES_PASSWORD=${DB_PASSWORD:-secure_password}
|
|
||||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- backend_db_data:/var/lib/postgresql/data
|
|
||||||
- ./sql/init:/docker-entrypoint-initdb.d:ro
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
- backend-network
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myp_user} -d ${DB_NAME:-myp_backend}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
# === BACKEND CACHE (Redis) ===
|
|
||||||
backend-cache:
|
|
||||||
image: redis:7.2-alpine
|
|
||||||
container_name: myp-backend-cache
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-backend_cache_password}
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- backend_cache_data:/data
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
- backend-network
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
# === PERSISTENTE VOLUMES ===
|
|
||||||
volumes:
|
|
||||||
backend_instance:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: none
|
|
||||||
o: bind
|
|
||||||
device: ./instance
|
|
||||||
|
|
||||||
backend_logs:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: none
|
|
||||||
o: bind
|
|
||||||
device: ./logs
|
|
||||||
|
|
||||||
backend_migrations:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: none
|
|
||||||
o: bind
|
|
||||||
device: ./migrations
|
|
||||||
|
|
||||||
backend_db_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
backend_cache_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
# === BACKEND-NETZWERK ===
|
|
||||||
networks:
|
|
||||||
backend-network:
|
|
||||||
driver: bridge
|
|
||||||
driver_opts:
|
|
||||||
com.docker.network.enable_ipv6: "false"
|
|
||||||
com.docker.network.bridge.enable_ip_masquerade: "true"
|
|
||||||
labels:
|
|
||||||
- "description=MYP Backend Server Netzwerk"
|
|
||||||
- "project=myp-backend"
|
|
||||||
- "tier=backend"
|
|
||||||
|
|
||||||
# === KONFIGURATION FÜR BACKEND ===
|
|
||||||
x-backend-defaults: &backend-defaults
|
|
||||||
restart: unless-stopped
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
labels: "service,environment,tier"
|
|
||||||
|
|
||||||
x-healthcheck-backend: &backend-healthcheck
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
@ -1,39 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
backend:
|
|
||||||
build: .
|
|
||||||
container_name: myp-backend
|
|
||||||
networks:
|
|
||||||
backend_network:
|
|
||||||
ipv4_address: 192.168.0.5
|
|
||||||
environment:
|
|
||||||
- SECRET_KEY=${SECRET_KEY:-7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F}
|
|
||||||
- DATABASE_PATH=${DATABASE_PATH:-instance/myp.db}
|
|
||||||
- TAPO_USERNAME=${TAPO_USERNAME:-till.tomczak@mercedes-benz.com}
|
|
||||||
- TAPO_PASSWORD=${TAPO_PASSWORD:-744563017196A}
|
|
||||||
- "PRINTERS=${PRINTERS:-{\"Printer 1\": {\"ip\": \"192.168.0.100\"}, \"Printer 2\": {\"ip\": \"192.168.0.101\"}, \"Printer 3\": {\"ip\": \"192.168.0.102\"}, \"Printer 4\": {\"ip\": \"192.168.0.103\"}, \"Printer 5\": {\"ip\": \"192.168.0.104\"}, \"Printer 6\": {\"ip\": \"192.168.0.106\"}}}"
|
|
||||||
- FLASK_APP=app.py
|
|
||||||
- PYTHONUNBUFFERED=1
|
|
||||||
- HOST=0.0.0.0
|
|
||||||
- PORT=5000
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
volumes:
|
|
||||||
- ./logs:/app/logs
|
|
||||||
- ./instance:/app/instance
|
|
||||||
restart: always
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--spider", "http://localhost:5000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
networks:
|
|
||||||
backend_network:
|
|
||||||
driver: bridge
|
|
||||||
ipam:
|
|
||||||
config:
|
|
||||||
- subnet: 192.168.0.0/24
|
|
||||||
gateway: 192.168.0.1
|
|
@ -1,647 +0,0 @@
|
|||||||
# MYP Backend API-Dokumentation
|
|
||||||
|
|
||||||
Dieses Dokument beschreibt detailliert die API-Endpunkte des MYP (Manage Your Printer) Backend-Systems.
|
|
||||||
|
|
||||||
## Basis-URL
|
|
||||||
|
|
||||||
Die Basis-URL für alle API-Anfragen ist: `http://localhost:5000` (Entwicklungsumgebung) oder die URL, unter der die Anwendung gehostet wird.
|
|
||||||
|
|
||||||
## Authentifizierung
|
|
||||||
|
|
||||||
Die meisten Endpunkte erfordern eine Authentifizierung. Diese erfolgt über Cookies/Sessions, die bei der Anmeldung erstellt werden. Die Session wird für 7 Tage gespeichert.
|
|
||||||
|
|
||||||
### Benutzerregistrierung
|
|
||||||
|
|
||||||
**Endpunkt:** `POST /auth/register`
|
|
||||||
|
|
||||||
**Beschreibung:** Registriert einen neuen Benutzer im System.
|
|
||||||
|
|
||||||
**Request-Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"username": "string", // Erforderlich
|
|
||||||
"password": "string", // Erforderlich
|
|
||||||
"displayName": "string", // Optional (Standard: username)
|
|
||||||
"email": "string" // Optional
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Registrierung erfolgreich!",
|
|
||||||
"user": {
|
|
||||||
"id": "string",
|
|
||||||
"username": "string",
|
|
||||||
"displayName": "string",
|
|
||||||
"email": "string",
|
|
||||||
"role": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fehlerantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Benutzername bereits vergeben!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benutzeranmeldung
|
|
||||||
|
|
||||||
**Endpunkt:** `POST /auth/login`
|
|
||||||
|
|
||||||
**Beschreibung:** Meldet einen Benutzer an und erstellt eine Session.
|
|
||||||
|
|
||||||
**Request-Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"username": "string", // Erforderlich
|
|
||||||
"password": "string" // Erforderlich
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Anmeldung erfolgreich!",
|
|
||||||
"user": {
|
|
||||||
"id": "string",
|
|
||||||
"username": "string",
|
|
||||||
"displayName": "string",
|
|
||||||
"email": "string",
|
|
||||||
"role": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fehlerantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Ungültiger Benutzername oder Passwort!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Initialer Administrator
|
|
||||||
|
|
||||||
**Endpunkt:** `POST /api/create-initial-admin`
|
|
||||||
|
|
||||||
**Beschreibung:** Erstellt einen initialen Admin-Benutzer, falls noch keiner existiert.
|
|
||||||
|
|
||||||
**Request-Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"username": "string", // Erforderlich
|
|
||||||
"password": "string", // Erforderlich
|
|
||||||
"displayName": "string", // Optional (Standard: username)
|
|
||||||
"email": "string" // Optional
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Administrator wurde erfolgreich erstellt!",
|
|
||||||
"user": {
|
|
||||||
"id": "string",
|
|
||||||
"username": "string",
|
|
||||||
"displayName": "string",
|
|
||||||
"email": "string",
|
|
||||||
"role": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fehlerantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Es existiert bereits ein Administrator!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benutzer-Endpunkte
|
|
||||||
|
|
||||||
### Alle Benutzer abrufen (Admin)
|
|
||||||
|
|
||||||
**Endpunkt:** `GET /api/users`
|
|
||||||
|
|
||||||
**Beschreibung:** Gibt eine Liste aller Benutzer zurück.
|
|
||||||
|
|
||||||
**Erforderliche Rechte:** Admin
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"username": "string",
|
|
||||||
"email": "string",
|
|
||||||
"role": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benutzer abrufen (Admin)
|
|
||||||
|
|
||||||
**Endpunkt:** `GET /api/users/{userId}`
|
|
||||||
|
|
||||||
**Beschreibung:** Gibt die Details eines bestimmten Benutzers zurück.
|
|
||||||
|
|
||||||
**Erforderliche Rechte:** Admin
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"username": "string",
|
|
||||||
"email": "string",
|
|
||||||
"role": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fehlerantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Nicht gefunden!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benutzer aktualisieren (Admin)
|
|
||||||
|
|
||||||
**Endpunkt:** `PUT /api/users/{userId}`
|
|
||||||
|
|
||||||
**Beschreibung:** Aktualisiert die Daten eines Benutzers.
|
|
||||||
|
|
||||||
**Erforderliche Rechte:** Admin
|
|
||||||
|
|
||||||
**Request-Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"username": "string",
|
|
||||||
"email": "string",
|
|
||||||
"password": "string",
|
|
||||||
"role": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"username": "string",
|
|
||||||
"email": "string",
|
|
||||||
"role": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fehlerantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Benutzername bereits vergeben!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benutzer löschen (Admin)
|
|
||||||
|
|
||||||
**Endpunkt:** `DELETE /api/users/{userId}`
|
|
||||||
|
|
||||||
**Beschreibung:** Löscht einen Benutzer.
|
|
||||||
|
|
||||||
**Erforderliche Rechte:** Admin
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Benutzer gelöscht!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Drucker-Endpunkte
|
|
||||||
|
|
||||||
### Alle Drucker abrufen
|
|
||||||
|
|
||||||
**Endpunkt:** `GET /api/printers`
|
|
||||||
|
|
||||||
**Beschreibung:** Gibt eine Liste aller Drucker (Steckdosen) zurück.
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"status": 0, // 0 = available, 1 = busy
|
|
||||||
"latestJob": {
|
|
||||||
// Job-Objekt oder null, wenn kein aktiver Job
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Drucker hinzufügen (Admin)
|
|
||||||
|
|
||||||
**Endpunkt:** `POST /api/printers`
|
|
||||||
|
|
||||||
**Beschreibung:** Fügt einen neuen Drucker hinzu.
|
|
||||||
|
|
||||||
**Erforderliche Rechte:** Admin
|
|
||||||
|
|
||||||
**Request-Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"ipAddress": "string" // IP-Adresse der Tapo-Steckdose
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"status": 0, // 0 = available, 1 = busy
|
|
||||||
"latestJob": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Drucker abrufen
|
|
||||||
|
|
||||||
**Endpunkt:** `GET /api/printers/{printerId}`
|
|
||||||
|
|
||||||
**Beschreibung:** Gibt die Details eines bestimmten Druckers zurück.
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"status": 0, // 0 = available, 1 = busy
|
|
||||||
"latestJob": {
|
|
||||||
// Job-Objekt oder null, wenn kein aktiver Job
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fehlerantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Nicht gefunden!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Drucker aktualisieren (Admin)
|
|
||||||
|
|
||||||
**Endpunkt:** `PUT /api/printers/{printerId}`
|
|
||||||
|
|
||||||
**Beschreibung:** Aktualisiert die Daten eines Druckers.
|
|
||||||
|
|
||||||
**Erforderliche Rechte:** Admin
|
|
||||||
|
|
||||||
**Request-Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"ipAddress": "string", // IP-Adresse der Tapo-Steckdose
|
|
||||||
"status": 0 // 0 = available, 1 = busy
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"name": "string",
|
|
||||||
"description": "string",
|
|
||||||
"status": 0, // 0 = available, 1 = busy
|
|
||||||
"latestJob": {
|
|
||||||
// Job-Objekt oder null, wenn kein aktiver Job
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Drucker löschen (Admin)
|
|
||||||
|
|
||||||
**Endpunkt:** `DELETE /api/printers/{printerId}`
|
|
||||||
|
|
||||||
**Beschreibung:** Löscht einen Drucker.
|
|
||||||
|
|
||||||
**Erforderliche Rechte:** Admin
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Drucker gelöscht!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Druckauftrags-Endpunkte
|
|
||||||
|
|
||||||
### Alle Druckaufträge abrufen
|
|
||||||
|
|
||||||
**Endpunkt:** `GET /api/jobs`
|
|
||||||
|
|
||||||
**Beschreibung:** Gibt eine Liste aller Druckaufträge zurück (für Admins) oder der eigenen Druckaufträge (für Benutzer).
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"socketId": "uuid-string",
|
|
||||||
"userId": "uuid-string",
|
|
||||||
"startAt": "string (ISO 8601)",
|
|
||||||
"durationInMinutes": 60,
|
|
||||||
"comments": "string",
|
|
||||||
"aborted": false,
|
|
||||||
"abortReason": null,
|
|
||||||
"remainingMinutes": 30
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Druckauftrag erstellen
|
|
||||||
|
|
||||||
**Endpunkt:** `POST /api/jobs`
|
|
||||||
|
|
||||||
**Beschreibung:** Erstellt einen neuen Druckauftrag.
|
|
||||||
|
|
||||||
**Request-Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"printerId": "uuid-string",
|
|
||||||
"durationInMinutes": 60,
|
|
||||||
"comments": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"socketId": "uuid-string",
|
|
||||||
"userId": "uuid-string",
|
|
||||||
"startAt": "string (ISO 8601)",
|
|
||||||
"durationInMinutes": 60,
|
|
||||||
"comments": "string",
|
|
||||||
"aborted": false,
|
|
||||||
"abortReason": null,
|
|
||||||
"remainingMinutes": 60
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fehlerantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Drucker ist nicht verfügbar!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Druckauftrag abrufen
|
|
||||||
|
|
||||||
**Endpunkt:** `GET /api/jobs/{jobId}`
|
|
||||||
|
|
||||||
**Beschreibung:** Gibt die Details eines bestimmten Druckauftrags zurück.
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"socketId": "uuid-string",
|
|
||||||
"userId": "uuid-string",
|
|
||||||
"startAt": "string (ISO 8601)",
|
|
||||||
"durationInMinutes": 60,
|
|
||||||
"comments": "string",
|
|
||||||
"aborted": false,
|
|
||||||
"abortReason": null,
|
|
||||||
"remainingMinutes": 30
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fehlerantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Nicht gefunden!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Druckauftrag Kommentare aktualisieren
|
|
||||||
|
|
||||||
**Endpunkt:** `PUT /api/jobs/{jobId}/comments`
|
|
||||||
|
|
||||||
**Beschreibung:** Aktualisiert die Kommentare eines Druckauftrags.
|
|
||||||
|
|
||||||
**Request-Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"comments": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"socketId": "uuid-string",
|
|
||||||
"userId": "uuid-string",
|
|
||||||
"startAt": "string (ISO 8601)",
|
|
||||||
"durationInMinutes": 60,
|
|
||||||
"comments": "string",
|
|
||||||
"aborted": false,
|
|
||||||
"abortReason": null,
|
|
||||||
"remainingMinutes": 30
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Druckauftrag abbrechen
|
|
||||||
|
|
||||||
**Endpunkt:** `POST /api/jobs/{jobId}/abort`
|
|
||||||
|
|
||||||
**Beschreibung:** Bricht einen laufenden Druckauftrag ab.
|
|
||||||
|
|
||||||
**Request-Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"reason": "string" // Optional
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"socketId": "uuid-string",
|
|
||||||
"userId": "uuid-string",
|
|
||||||
"startAt": "string (ISO 8601)",
|
|
||||||
"durationInMinutes": 60,
|
|
||||||
"comments": "string",
|
|
||||||
"aborted": true,
|
|
||||||
"abortReason": "string",
|
|
||||||
"remainingMinutes": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Druckauftrag vorzeitig beenden
|
|
||||||
|
|
||||||
**Endpunkt:** `POST /api/jobs/{jobId}/finish`
|
|
||||||
|
|
||||||
**Beschreibung:** Beendet einen laufenden Druckauftrag vorzeitig.
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"socketId": "uuid-string",
|
|
||||||
"userId": "uuid-string",
|
|
||||||
"startAt": "string (ISO 8601)",
|
|
||||||
"durationInMinutes": 45, // Tatsächliche Dauer bis zum Beenden
|
|
||||||
"comments": "string",
|
|
||||||
"aborted": false,
|
|
||||||
"abortReason": null,
|
|
||||||
"remainingMinutes": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Druckauftrag verlängern
|
|
||||||
|
|
||||||
**Endpunkt:** `POST /api/jobs/{jobId}/extend`
|
|
||||||
|
|
||||||
**Beschreibung:** Verlängert die Laufzeit eines Druckauftrags.
|
|
||||||
|
|
||||||
**Request-Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"minutes": 30, // Zusätzliche Minuten
|
|
||||||
"hours": 0 // Zusätzliche Stunden (optional)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-string",
|
|
||||||
"socketId": "uuid-string",
|
|
||||||
"userId": "uuid-string",
|
|
||||||
"startAt": "string (ISO 8601)",
|
|
||||||
"durationInMinutes": 90, // Aktualisierte Gesamtdauer
|
|
||||||
"comments": "string",
|
|
||||||
"aborted": false,
|
|
||||||
"abortReason": null,
|
|
||||||
"remainingMinutes": 60
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Druckauftrag löschen
|
|
||||||
|
|
||||||
**Endpunkt:** `DELETE /api/jobs/{jobId}`
|
|
||||||
|
|
||||||
**Beschreibung:** Löscht einen Druckauftrag.
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Druckauftrag gelöscht!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verbleibende Zeit eines Druckauftrags abrufen
|
|
||||||
|
|
||||||
**Endpunkt:** `GET /api/job/{jobId}/remaining-time`
|
|
||||||
|
|
||||||
**Beschreibung:** Gibt die verbleibende Zeit eines aktiven Druckauftrags in Minuten zurück.
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"remaining_minutes": 30,
|
|
||||||
"job_status": "active", // active, completed
|
|
||||||
"socket_status": "busy" // busy, available
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status eines Druckauftrags abrufen
|
|
||||||
|
|
||||||
**Endpunkt:** `GET /api/job/{jobId}/status`
|
|
||||||
|
|
||||||
**Beschreibung:** Gibt detaillierte Statusinformationen zu einem Druckauftrag zurück.
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"job": {
|
|
||||||
"id": "uuid-string",
|
|
||||||
"socketId": "uuid-string",
|
|
||||||
"userId": "uuid-string",
|
|
||||||
"startAt": "string (ISO 8601)",
|
|
||||||
"durationInMinutes": 60,
|
|
||||||
"comments": "string",
|
|
||||||
"aborted": false,
|
|
||||||
"abortReason": null,
|
|
||||||
"remainingMinutes": 30
|
|
||||||
},
|
|
||||||
"status": "active", // active, completed, aborted
|
|
||||||
"socketStatus": "busy", // busy, available
|
|
||||||
"remainingMinutes": 30
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Statistik-Endpunkte
|
|
||||||
|
|
||||||
### Systemstatistiken abrufen (Admin)
|
|
||||||
|
|
||||||
**Endpunkt:** `GET /api/stats`
|
|
||||||
|
|
||||||
**Beschreibung:** Gibt Statistiken zu Druckern, Aufträgen und Benutzern zurück.
|
|
||||||
|
|
||||||
**Erforderliche Rechte:** Admin
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"printers": {
|
|
||||||
"total": 10,
|
|
||||||
"available": 5,
|
|
||||||
"utilization_rate": 0.5
|
|
||||||
},
|
|
||||||
"jobs": {
|
|
||||||
"total": 100,
|
|
||||||
"active": 5,
|
|
||||||
"completed": 90,
|
|
||||||
"avg_duration": 120
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"total": 50
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test-Endpunkt
|
|
||||||
|
|
||||||
### API-Test
|
|
||||||
|
|
||||||
**Endpunkt:** `GET /api/test`
|
|
||||||
|
|
||||||
**Beschreibung:** Testet, ob die API funktioniert.
|
|
||||||
|
|
||||||
**Erfolgsantwort:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "MYP Backend API funktioniert!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fehlercodes
|
|
||||||
|
|
||||||
| Statuscode | Beschreibung |
|
|
||||||
|------------|-----------------------------|
|
|
||||||
| 200 | OK - Anfrage erfolgreich |
|
|
||||||
| 201 | Created - Ressource erstellt |
|
|
||||||
| 400 | Bad Request - Ungültige Anfrage |
|
|
||||||
| 401 | Unauthorized - Authentifizierung erforderlich |
|
|
||||||
| 403 | Forbidden - Unzureichende Rechte |
|
|
||||||
| 404 | Not Found - Ressource nicht gefunden |
|
|
||||||
| 500 | Internal Server Error - Serverfehler |
|
|
230
backend/docs/COMMON_ERRORS.md
Normal file
230
backend/docs/COMMON_ERRORS.md
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
# Häufige Fehler und Lösungen
|
||||||
|
|
||||||
|
Dieses Dokument enthält häufig auftretende Probleme bei der Einrichtung und Nutzung von MYP im Kiosk-Modus und deren Lösungen.
|
||||||
|
|
||||||
|
## Installationsprobleme
|
||||||
|
|
||||||
|
### Fehler: "Paket nicht gefunden"
|
||||||
|
|
||||||
|
**Problem**: Beim Ausführen von `apt install` werden Pakete nicht gefunden.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Führe zuerst `sudo apt update` aus, um die Paketlisten zu aktualisieren
|
||||||
|
2. Stelle sicher, dass eine Internetverbindung besteht
|
||||||
|
3. Bei älteren Raspberry Pi OS Versionen ggf. Repository hinzufügen:
|
||||||
|
```bash
|
||||||
|
sudo apt-add-repository universe
|
||||||
|
sudo apt update
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: "Permission denied" beim Kopieren nach /opt/myp
|
||||||
|
|
||||||
|
**Problem**: Beim Kopieren der Dateien nach /opt/myp wird ein Permission-Fehler angezeigt.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Stelle sicher, dass du die Befehle in der richtigen Reihenfolge ausführst:
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /opt/myp
|
||||||
|
sudo chown $USER:$USER /opt/myp
|
||||||
|
```
|
||||||
|
2. Falls das nicht hilft, führe den Kopierbefehl mit sudo aus:
|
||||||
|
```bash
|
||||||
|
sudo cp -r ./myp/* /opt/myp/
|
||||||
|
sudo chown -R $USER:$USER /opt/myp/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flask-Backend Probleme
|
||||||
|
|
||||||
|
### Fehler: "MYP-Dienst startet nicht"
|
||||||
|
|
||||||
|
**Problem**: Der systemd-Dienst für das Flask-Backend startet nicht.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Überprüfe den Status des Dienstes:
|
||||||
|
```bash
|
||||||
|
sudo systemctl status myp.service
|
||||||
|
```
|
||||||
|
2. Schau in die Logs:
|
||||||
|
```bash
|
||||||
|
sudo journalctl -u myp.service -n 50
|
||||||
|
```
|
||||||
|
3. Häufige Ursachen:
|
||||||
|
- Falscher Pfad in myp.service: Überprüfe WorkingDirectory und ExecStart
|
||||||
|
- Python-Umgebung nicht korrekt: Überprüfe, ob die .venv-Umgebung existiert
|
||||||
|
- Abhängigkeiten fehlen: Führe `pip install -r requirements.txt` aus
|
||||||
|
|
||||||
|
### Fehler: "ModuleNotFoundError: No module named X"
|
||||||
|
|
||||||
|
**Problem**: Beim Start der Flask-App wird ein Python-Modul nicht gefunden.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Aktiviere die virtuelle Umgebung und installiere das fehlende Paket:
|
||||||
|
```bash
|
||||||
|
cd /opt/myp
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install <fehlende_module>
|
||||||
|
```
|
||||||
|
2. Überprüfe requirements.txt und installiere alle Abhängigkeiten:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: "Address already in use"
|
||||||
|
|
||||||
|
**Problem**: Flask kann nicht starten, weil Port 5000 bereits verwendet wird.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Finde den Prozess, der Port 5000 verwendet:
|
||||||
|
```bash
|
||||||
|
sudo lsof -i:5000
|
||||||
|
```
|
||||||
|
2. Beende den Prozess:
|
||||||
|
```bash
|
||||||
|
sudo kill <PID>
|
||||||
|
```
|
||||||
|
3. Falls nötig, ändere den Port in app.py:
|
||||||
|
```python
|
||||||
|
app.run(host="0.0.0.0", port=5001, debug=True)
|
||||||
|
```
|
||||||
|
(Und passe auch die URL im kiosk.sh an)
|
||||||
|
|
||||||
|
## Chromium Kiosk-Modus Probleme
|
||||||
|
|
||||||
|
### Fehler: "Chromium startet nicht im Kiosk-Modus"
|
||||||
|
|
||||||
|
**Problem**: Der Browser startet nicht automatisch oder nicht im Vollbildmodus.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Überprüfe den Status des User-Services:
|
||||||
|
```bash
|
||||||
|
systemctl --user status kiosk.service
|
||||||
|
```
|
||||||
|
2. Führe kiosk.sh manuell aus, um Fehlermeldungen zu sehen:
|
||||||
|
```bash
|
||||||
|
/home/pi/kiosk.sh
|
||||||
|
```
|
||||||
|
3. Prüfe, ob die notwendigen Pakete installiert sind:
|
||||||
|
```bash
|
||||||
|
sudo apt install --reinstall chromium-browser unclutter
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: "Chromium zeigt Fehlerdialoge statt der MYP-Oberfläche"
|
||||||
|
|
||||||
|
**Problem**: Der Browser zeigt Crash-Dialoge oder Warnungen an.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Lösche die Chromium-Einstellungen und starte neu:
|
||||||
|
```bash
|
||||||
|
rm -rf ~/.config/chromium/
|
||||||
|
```
|
||||||
|
2. Füge zusätzliche Parameter zu chromium-browser in kiosk.sh hinzu:
|
||||||
|
```bash
|
||||||
|
chromium-browser --kiosk --noerrdialogs --disable-infobars --disable-session-crashed-bubble \
|
||||||
|
--disable-features=DialMediaRouteProvider --window-position=0,0 \
|
||||||
|
--app=http://localhost:5000/ &
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: "Chromium öffnet sich, aber zeigt nicht die MYP-Seite"
|
||||||
|
|
||||||
|
**Problem**: Der Browser startet, aber die Anwendung wird nicht angezeigt.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Überprüfe, ob der Flask-Dienst läuft:
|
||||||
|
```bash
|
||||||
|
systemctl status myp.service
|
||||||
|
```
|
||||||
|
2. Teste, ob die Anwendung im Browser erreichbar ist:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5000/
|
||||||
|
```
|
||||||
|
3. Prüfe, ob Chromium mit der richtigen URL startet:
|
||||||
|
```bash
|
||||||
|
# In kiosk.sh
|
||||||
|
chromium-browser --kiosk --noerrdialogs --disable-infobars \
|
||||||
|
--window-position=0,0 --app=http://localhost:5000/ &
|
||||||
|
```
|
||||||
|
|
||||||
|
## Watchdog-Probleme
|
||||||
|
|
||||||
|
### Fehler: "Watchdog-Script funktioniert nicht"
|
||||||
|
|
||||||
|
**Problem**: Der Watchdog-Cronjob scheint nicht zu funktionieren.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Überprüfe, ob der Cron-Job eingerichtet ist:
|
||||||
|
```bash
|
||||||
|
crontab -l
|
||||||
|
```
|
||||||
|
2. Prüfe die Berechtigungen des Watchdog-Scripts:
|
||||||
|
```bash
|
||||||
|
chmod +x /home/pi/watchdog.sh
|
||||||
|
```
|
||||||
|
3. Führe das Script manuell aus und prüfe auf Fehler:
|
||||||
|
```bash
|
||||||
|
/home/pi/watchdog.sh
|
||||||
|
```
|
||||||
|
4. Überprüfe die Logdatei:
|
||||||
|
```bash
|
||||||
|
cat /home/pi/myp-watchdog.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: "Watchdog kann systemctl nicht ausführen"
|
||||||
|
|
||||||
|
**Problem**: Der Watchdog kann systemctl-Befehle nicht ausführen.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Erlaubnis für den pi-Benutzer zum Ausführen von systemctl hinzufügen:
|
||||||
|
```bash
|
||||||
|
echo "pi ALL=NOPASSWD: /bin/systemctl restart myp.service" | sudo tee /etc/sudoers.d/myp-watchdog
|
||||||
|
```
|
||||||
|
|
||||||
|
## Allgemeine Probleme
|
||||||
|
|
||||||
|
### Fehler: "Bildschirm wird nach einiger Zeit schwarz"
|
||||||
|
|
||||||
|
**Problem**: Trotz Konfiguration schaltet sich der Bildschirm nach einiger Zeit aus.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Stelle sicher, dass die xset-Befehle in kiosk.sh korrekt ausgeführt werden:
|
||||||
|
```bash
|
||||||
|
xset s off
|
||||||
|
xset s noblank
|
||||||
|
xset -dpms
|
||||||
|
```
|
||||||
|
2. Aktualisiere die Autostart-Datei:
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/xdg/lxsession/LXDE-pi/autostart
|
||||||
|
```
|
||||||
|
Füge folgende Zeilen hinzu:
|
||||||
|
```
|
||||||
|
@xset s off
|
||||||
|
@xset -dpms
|
||||||
|
@xset s noblank
|
||||||
|
```
|
||||||
|
3. Verwende ein Tool wie Caffeine:
|
||||||
|
```bash
|
||||||
|
sudo apt install caffeine
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehler: "System bootet nicht automatisch in den Kiosk-Modus"
|
||||||
|
|
||||||
|
**Problem**: Der automatische Start des Kiosk-Modus funktioniert nicht.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Überprüfe, ob der automatische Login aktiviert ist:
|
||||||
|
```bash
|
||||||
|
sudo raspi-config
|
||||||
|
# 1 System Options → S5 Boot/Auto Login → B4 Desktop Autologin
|
||||||
|
```
|
||||||
|
2. Stelle sicher, dass der User-Service aktiviert ist:
|
||||||
|
```bash
|
||||||
|
systemctl --user enable kiosk.service
|
||||||
|
```
|
||||||
|
3. Aktiviere Linger für den pi-Benutzer:
|
||||||
|
```bash
|
||||||
|
sudo loginctl enable-linger pi
|
||||||
|
```
|
||||||
|
4. Reboote das System und überprüfe den Status der Dienste:
|
||||||
|
```bash
|
||||||
|
sudo reboot
|
||||||
|
```
|
246
backend/docs/KIOSK-SETUP.md
Normal file
246
backend/docs/KIOSK-SETUP.md
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
# MYP im Kiosk-Modus
|
||||||
|
|
||||||
|
Diese Anleitung beschreibt, wie MYP (Manage Your Printer) auf einem Raspberry Pi 4 im Kiosk-Modus eingerichtet wird, sodass das System beim Booten automatisch startet.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Raspberry Pi 4 (oder kompatibel) mit Raspbian/Raspberry Pi OS
|
||||||
|
- Internetverbindung für die Installation (nach der Installation wird keine Verbindung mehr benötigt)
|
||||||
|
- Bildschirm, Tastatur und Maus für die Einrichtung
|
||||||
|
|
||||||
|
## Komponenten des Kiosk-Modus
|
||||||
|
|
||||||
|
Die Kiosk-Einrichtung besteht aus mehreren Komponenten:
|
||||||
|
|
||||||
|
1. **Flask-Backend-Dienst**: Systemd-Service zum Starten der MYP-Anwendung
|
||||||
|
2. **Chromium im Kiosk-Modus**: Browserinstanz, die das Dashboard anzeigt
|
||||||
|
3. **Watchdog**: Überwacht den Browser und das Backend, startet bei Bedarf neu
|
||||||
|
|
||||||
|
## Automatische Installation
|
||||||
|
|
||||||
|
Für die automatische Installation kann das mitgelieferte Setup-Script verwendet werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x setup.sh
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses Script führt alle notwendigen Schritte aus:
|
||||||
|
- Installation der benötigten Pakete
|
||||||
|
- Kopieren der MYP-Anwendung nach `/opt/myp`
|
||||||
|
- Einrichtung der Python-Umgebung und Installation der Abhängigkeiten
|
||||||
|
- Konfiguration der Systemd-Dienste
|
||||||
|
- Einrichtung des Kiosk-Modus
|
||||||
|
- Einrichtung des Watchdogs
|
||||||
|
|
||||||
|
Nach der Ausführung des Scripts muss noch der automatische Login aktiviert werden:
|
||||||
|
```bash
|
||||||
|
sudo raspi-config
|
||||||
|
# 1 System Options → S5 Boot/Auto Login → B4 Desktop Autologin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manuelle Installation
|
||||||
|
|
||||||
|
Falls eine manuelle Installation bevorzugt wird, können die folgenden Schritte ausgeführt werden:
|
||||||
|
|
||||||
|
### 1. Pakete installieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y python3 python3-pip python3-venv chromium-browser \
|
||||||
|
unclutter xdotool xscreensaver git
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. MYP nach /opt/myp kopieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /opt/myp
|
||||||
|
sudo chown $USER:$USER /opt/myp
|
||||||
|
cp -r ./myp/* /opt/myp
|
||||||
|
cd /opt/myp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Python-Umgebung und Abhängigkeiten einrichten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Systemd-Dienst für das Flask-Backend
|
||||||
|
|
||||||
|
Datei erstellen: `/etc/systemd/system/myp.service`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=MYP Flask Backend
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=pi
|
||||||
|
WorkingDirectory=/opt/myp
|
||||||
|
ExecStart=/opt/myp/.venv/bin/python /opt/myp/app.py
|
||||||
|
Restart=always
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Dienst aktivieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now myp.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Kiosk-Script einrichten
|
||||||
|
|
||||||
|
Datei erstellen: `/home/pi/kiosk.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Bildschirm-Blanking verhindern
|
||||||
|
xset s off
|
||||||
|
xset s noblank
|
||||||
|
xset -dpms
|
||||||
|
|
||||||
|
# Mauszeiger ausblenden
|
||||||
|
unclutter -idle 0.5 -root &
|
||||||
|
|
||||||
|
# Chromium-Crash-Dialoge unterdrücken
|
||||||
|
sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' \
|
||||||
|
"$HOME/.config/chromium/Default/Preferences" 2>/dev/null || true
|
||||||
|
sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' \
|
||||||
|
"$HOME/.config/chromium/Default/Preferences" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Browser starten
|
||||||
|
chromium-browser --kiosk --noerrdialogs --disable-infobars \
|
||||||
|
--window-position=0,0 --app=http://localhost:5000/ &
|
||||||
|
```
|
||||||
|
|
||||||
|
Ausführbar machen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x /home/pi/kiosk.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Systemd-User-Dienst für den Browser
|
||||||
|
|
||||||
|
Verzeichnis erstellen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /home/pi/.config/systemd/user
|
||||||
|
```
|
||||||
|
|
||||||
|
Datei erstellen: `/home/pi/.config/systemd/user/kiosk.service`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Chromium Kiosk
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=forking
|
||||||
|
ExecStart=/home/pi/kiosk.sh
|
||||||
|
Restart=on-abort
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=xsession.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Dienst aktivieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable kiosk.service
|
||||||
|
sudo loginctl enable-linger pi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Watchdog einrichten
|
||||||
|
|
||||||
|
Datei erstellen: `/home/pi/watchdog.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# MYP Watchdog für Chromium Browser
|
||||||
|
|
||||||
|
# Funktion zum Loggen von Nachrichten
|
||||||
|
log() {
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> /home/pi/myp-watchdog.log
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prüfen, ob Chromium läuft
|
||||||
|
if ! pgrep -x "chromium-browse" > /dev/null; then
|
||||||
|
log "Chromium nicht gefunden - starte neu"
|
||||||
|
|
||||||
|
# Alle eventuell noch vorhandenen Chromium-Prozesse beenden
|
||||||
|
pkill -f chromium || true
|
||||||
|
|
||||||
|
# Warten bis alle Prozesse beendet sind
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Kiosk-Script neu starten
|
||||||
|
/home/pi/kiosk.sh
|
||||||
|
|
||||||
|
log "Chromium neugestartet"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob MYP Flask-Dienst läuft
|
||||||
|
if ! systemctl is-active --quiet myp.service; then
|
||||||
|
log "MYP Flask-Dienst ist nicht aktiv - starte neu"
|
||||||
|
|
||||||
|
# Dienst neustarten
|
||||||
|
sudo systemctl restart myp.service
|
||||||
|
|
||||||
|
log "MYP Flask-Dienst neugestartet"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Ausführbar machen und Cron-Job einrichten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x /home/pi/watchdog.sh
|
||||||
|
(crontab -l 2>/dev/null || echo "") | grep -v "watchdog.sh" | { cat; echo "*/5 * * * * /home/pi/watchdog.sh > /dev/null 2>&1"; } | crontab -
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Automatischen Desktop-Login einschalten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo raspi-config
|
||||||
|
# 1 System Options → S5 Boot/Auto Login → B4 Desktop Autologin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Bildschirm nie ausschalten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo sed -i 's/#BLANK_TIME=.*/BLANK_TIME=0/' /etc/xdg/lxsession/LXDE-pi/autostart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ablauf beim Booten
|
||||||
|
|
||||||
|
1. Der Raspberry Pi startet und fährt bis zum Multi-User-Target hoch
|
||||||
|
2. `myp.service` wird gestartet und das Flask-Backend sowie der Plug-Scheduler laufen
|
||||||
|
3. LightDM startet und meldet den Benutzer `pi` automatisch an
|
||||||
|
4. Nach dem Anmelden wird der User-Scope geladen und `kiosk.service` gestartet
|
||||||
|
5. `kiosk.sh` startet Chromium im Kiosk-Modus und öffnet die MYP-Oberfläche
|
||||||
|
6. Der Watchdog-Cron-Job überwacht alle 5 Minuten, ob alles läuft
|
||||||
|
|
||||||
|
## Fehlerbehebung
|
||||||
|
|
||||||
|
- **MYP startet nicht**: `systemctl status myp.service` zeigt den Status des Dienstes
|
||||||
|
- **Browser startet nicht**: `systemctl --user status kiosk.service` zeigt den Status des Kiosk-Dienstes
|
||||||
|
- **Watchdog-Logs**: In `/home/pi/myp-watchdog.log` werden Probleme und Neustarts protokolliert
|
||||||
|
|
||||||
|
## Anpassung für andere Benutzer
|
||||||
|
|
||||||
|
Falls ein anderer Benutzer als `pi` verwendet wird, müssen folgende Anpassungen vorgenommen werden:
|
||||||
|
|
||||||
|
1. In `myp.service`: `User=` auf den entsprechenden Benutzer ändern
|
||||||
|
2. Pfade in `kiosk.sh` und `kiosk.service` anpassen
|
||||||
|
3. `loginctl enable-linger` für den entsprechenden Benutzer aktivieren
|
@ -1,213 +0,0 @@
|
|||||||
# MYP - Projektdokumentation für das IHK-Abschlussprojekt
|
|
||||||
|
|
||||||
## Projektübersicht
|
|
||||||
|
|
||||||
**Projektname:** MYP (Manage Your Printer)
|
|
||||||
**Projekttyp:** IHK-Abschlussprojekt für Fachinformatiker für digitale Vernetzung
|
|
||||||
**Zeitraum:** [Dein Projektzeitraum]
|
|
||||||
**Team:** 2 Personen (Frontend- und Backend-Entwicklung)
|
|
||||||
|
|
||||||
## Projektziel
|
|
||||||
|
|
||||||
Das Ziel des Projektes ist die Entwicklung einer Reservierungs- und Steuerungsplattform für 3D-Drucker, die es Benutzern ermöglicht, Drucker zu reservieren und deren Stromversorgung automatisch über WLAN-Steckdosen (Tapo P115) zu steuern. Die Plattform soll eine einfache Verwaltung der Drucker und ihrer Auslastung bieten sowie den Stromverbrauch optimieren, indem Drucker nur während aktiver Reservierungen mit Strom versorgt werden.
|
|
||||||
|
|
||||||
## Aufgabenbeschreibung
|
|
||||||
|
|
||||||
Als Fachinformatiker für digitale Vernetzung besteht meine Aufgabe in der Entwicklung des Backend-Systems, das folgende Funktionen bereitstellt:
|
|
||||||
|
|
||||||
1. **API-Backend für das Frontend**: Entwicklung einer RESTful API, die mit dem Frontend kommuniziert und alle notwendigen Daten bereitstellt.
|
|
||||||
|
|
||||||
2. **Authentifizierungssystem**: Integration einer OAuth-Authentifizierung über GitHub, um Benutzer zu identifizieren und Zugriffskontrolle zu gewährleisten.
|
|
||||||
|
|
||||||
3. **Datenbankverwaltung**: Erstellung und Verwaltung der Datenbankmodelle für Benutzer, Drucker und Reservierungen.
|
|
||||||
|
|
||||||
4. **Steckdosensteuerung**: Implementierung einer Schnittstelle zu Tapo P115 WLAN-Steckdosen, um die Stromversorgung der Drucker basierend auf Reservierungen zu steuern.
|
|
||||||
|
|
||||||
5. **Automatisierung**: Entwicklung von Mechanismen zur automatischen Überwachung von Reservierungen und Steuerung der Steckdosen.
|
|
||||||
|
|
||||||
6. **Sicherheit**: Implementierung von Sicherheitsmaßnahmen zum Schutz der Anwendung und der Daten.
|
|
||||||
|
|
||||||
7. **Dokumentation**: Erstellung einer umfassenden Dokumentation für Entwicklung, Installation und Nutzung des Systems.
|
|
||||||
|
|
||||||
## Technische Umsetzung
|
|
||||||
|
|
||||||
### Backend (Mein Verantwortungsbereich)
|
|
||||||
|
|
||||||
#### Verwendete Technologien
|
|
||||||
|
|
||||||
- **Programmiersprache**: Python 3.11
|
|
||||||
- **Web-Framework**: Flask 2.3.3
|
|
||||||
- **Datenbank-ORM**: SQLAlchemy 3.1.1
|
|
||||||
- **Datenbank**: SQLite (für Entwicklung), erweiterbar auf PostgreSQL für Produktion
|
|
||||||
- **Authentifizierung**: Authlib für GitHub OAuth
|
|
||||||
- **Steckdosen-Steuerung**: Tapo Python Library
|
|
||||||
- **Container-Technologie**: Docker und Docker Compose
|
|
||||||
|
|
||||||
#### Architektur
|
|
||||||
|
|
||||||
Die Backend-Anwendung folgt einer klassischen dreischichtigen Architektur:
|
|
||||||
|
|
||||||
1. **Datenmodell-Schicht**: SQLAlchemy ORM-Modelle für Benutzer, Sessions, Drucker und Druckaufträge
|
|
||||||
2. **Business-Logic-Schicht**: Implementierung der Geschäftslogik für Reservierungsverwaltung und Steckdosensteuerung
|
|
||||||
3. **API-Schicht**: RESTful API-Endpunkte, die vom Frontend konsumiert werden
|
|
||||||
|
|
||||||
Zusätzlich wurden folgende Features implementiert:
|
|
||||||
|
|
||||||
- **OAuth-Authentifizierung**: Implementierung einer sicheren Authentifizierung über GitHub
|
|
||||||
- **Session-Management**: Server-seitige Session-Verwaltung für Benutzerauthentifizierung
|
|
||||||
- **Steckdosensteuerung**: Asynchrone Steuerung der Tapo P115 WLAN-Steckdosen
|
|
||||||
- **CLI-Befehle**: Flask CLI-Befehle für automatisierte Aufgaben wie die Überprüfung abgelaufener Reservierungen
|
|
||||||
|
|
||||||
#### Datenmodell
|
|
||||||
|
|
||||||
Das Datenmodell besteht aus vier Hauptentitäten:
|
|
||||||
|
|
||||||
1. **User**: Benutzer mit GitHub-Authentifizierung und Rollenverwaltung
|
|
||||||
2. **Session**: Sitzungsdaten für die Authentifizierung
|
|
||||||
3. **Printer**: Drucker mit Status und IP-Adresse der zugehörigen Steckdose
|
|
||||||
4. **PrintJob**: Reservierungen mit Start- und Endzeit, Dauer und Status
|
|
||||||
|
|
||||||
#### API-Endpunkte
|
|
||||||
|
|
||||||
Die API wurde speziell entwickelt, um nahtlos mit dem bestehenden Frontend zusammenzuarbeiten. Sie bietet Endpunkte für:
|
|
||||||
|
|
||||||
- Authentifizierung und Benutzerverwaltung
|
|
||||||
- Druckerverwaltung
|
|
||||||
- Reservierungsverwaltung (Erstellen, Abbrechen, Verlängern)
|
|
||||||
- Statusinformationen wie verbleibende Zeit
|
|
||||||
|
|
||||||
#### Steckdosensteuerung
|
|
||||||
|
|
||||||
Die Steuerung der Tapo P115 WLAN-Steckdosen erfolgt über die Tapo Python Library. Das System:
|
|
||||||
|
|
||||||
- Schaltet Steckdosen bei Erstellung einer Reservierung ein
|
|
||||||
- Schaltet Steckdosen bei Abbruch oder Beendigung einer Reservierung aus
|
|
||||||
- Überprüft regelmäßig abgelaufene Reservierungen und schaltet die entsprechenden Steckdosen aus
|
|
||||||
|
|
||||||
#### Automatisierung
|
|
||||||
|
|
||||||
Das System implementiert mehrere Automatisierungsmechanismen:
|
|
||||||
|
|
||||||
- **Automatische Steckdosensteuerung**: Ein- und Ausschalten der Steckdosen basierend auf Reservierungsstatus
|
|
||||||
- **Job-Überprüfung**: CLI-Befehl `flask check-jobs` zur regelmäßigen Überprüfung abgelaufener Reservierungen
|
|
||||||
- **Logging**: Automatische Protokollierung aller Aktionen zur Fehlerdiagnose
|
|
||||||
|
|
||||||
### Frontend (Verantwortungsbereich des Teampartners)
|
|
||||||
|
|
||||||
Das Frontend wurde von meinem Teampartner entwickelt und besteht aus:
|
|
||||||
|
|
||||||
- Next.js-Anwendung mit React-Komponenten
|
|
||||||
- Tailwind CSS für das Styling
|
|
||||||
- Serverless Functions für API-Integrationen
|
|
||||||
- Responsive Design für Desktop- und Mobile-Nutzung
|
|
||||||
|
|
||||||
## Projektergebnisse
|
|
||||||
|
|
||||||
Das Projekt hat erfolgreich eine funktionsfähige Reservierungs- und Steuerungsplattform für 3D-Drucker geschaffen, die es Benutzern ermöglicht:
|
|
||||||
|
|
||||||
1. Sich über GitHub zu authentifizieren
|
|
||||||
2. Verfügbare Drucker zu sehen und zu reservieren
|
|
||||||
3. Ihre Reservierungen zu verwalten (verlängern, abbrechen, kommentieren)
|
|
||||||
4. Als Administrator Drucker und Benutzer zu verwalten
|
|
||||||
|
|
||||||
Technische Errungenschaften:
|
|
||||||
|
|
||||||
1. Nahtlose Integration mit dem Frontend
|
|
||||||
2. Erfolgreiche Implementierung der Steckdosensteuerung
|
|
||||||
3. Sichere Authentifizierung über GitHub OAuth
|
|
||||||
4. Optimierte Stromnutzung durch automatische Steckdosensteuerung
|
|
||||||
|
|
||||||
## Herausforderungen und Lösungen
|
|
||||||
|
|
||||||
### Herausforderung 1: GitHub OAuth-Integration
|
|
||||||
|
|
||||||
Die Integration der GitHub-Authentifizierung, insbesondere mit GitHub Enterprise, erforderte eine sorgfältige Konfiguration der OAuth-Einstellungen und URL-Anpassungen.
|
|
||||||
|
|
||||||
**Lösung:** Implementierung mit Authlib und anpassbaren Konfigurationsoptionen für verschiedene GitHub-Instanzen.
|
|
||||||
|
|
||||||
### Herausforderung 2: Tapo P115 Steuerung
|
|
||||||
|
|
||||||
Die Kommunikation mit den Tapo P115 WLAN-Steckdosen erforderte eine zuverlässige und asynchrone Implementierung.
|
|
||||||
|
|
||||||
**Lösung:** Verwendung der Tapo Python Library mit asynchronem Handling und robusten Fehlerbehandlungsmechanismen.
|
|
||||||
|
|
||||||
### Herausforderung 3: Kompatibilität mit bestehendem Frontend
|
|
||||||
|
|
||||||
Das Backend musste mit dem bereits entwickelten Frontend kompatibel sein, was eine genaue Anpassung der API-Endpunkte und Datenstrukturen erforderte.
|
|
||||||
|
|
||||||
**Lösung:** Sorgfältige Analyse des Frontend-Codes, um die erwarteten API-Strukturen zu verstehen und das Backend entsprechend zu implementieren.
|
|
||||||
|
|
||||||
### Herausforderung 4: Automatische Steckdosensteuerung
|
|
||||||
|
|
||||||
Die zuverlässige Steuerung der Steckdosen bei abgelaufenen Reservierungen war eine Herausforderung.
|
|
||||||
|
|
||||||
**Lösung:** Implementierung eines CLI-Befehls, der regelmäßig durch Cron-Jobs ausgeführt werden kann, um abgelaufene Reservierungen zu überprüfen.
|
|
||||||
|
|
||||||
## Fachliche Reflexion
|
|
||||||
|
|
||||||
Das Projekt erforderte ein breites Spektrum an Fähigkeiten aus dem Bereich der digitalen Vernetzung:
|
|
||||||
|
|
||||||
1. **Netzwerkkommunikation**: Implementierung der Kommunikation zwischen Backend, Frontend und WLAN-Steckdosen über verschiedene Protokolle.
|
|
||||||
|
|
||||||
2. **Systemintegration**: Integration verschiedener Systeme (GitHub OAuth, Datenbank, Tapo-Steckdosen) zu einer kohärenten Anwendung.
|
|
||||||
|
|
||||||
3. **API-Design**: Entwicklung einer RESTful API, die den Anforderungen des Frontends entspricht und zukunftssicher ist.
|
|
||||||
|
|
||||||
4. **Datenbankentwurf**: Erstellung eines optimierten Datenbankschemas für die Anwendung.
|
|
||||||
|
|
||||||
5. **Sicherheitskonzepte**: Implementierung von Sicherheitsmaßnahmen wie OAuth, Session-Management und Zugriffskontrollen.
|
|
||||||
|
|
||||||
6. **Automatisierung**: Entwicklung von Automatisierungsprozessen für die Steckdosensteuerung und Job-Überwachung.
|
|
||||||
|
|
||||||
Diese Aspekte entsprechen direkt den Kernkompetenzen des Berufsbildes "Fachinformatiker für digitale Vernetzung" und zeigen die praktische Anwendung dieser Fähigkeiten in einem realen Projekt.
|
|
||||||
|
|
||||||
## Ausblick und Weiterentwicklung
|
|
||||||
|
|
||||||
Das System bietet verschiedene Möglichkeiten zur Weiterentwicklung:
|
|
||||||
|
|
||||||
1. **Erweiterung der Steckdosenunterstützung**: Integration weiterer Smart-Home-Geräte neben Tapo P115.
|
|
||||||
|
|
||||||
2. **Benachrichtigungssystem**: Implementierung von E-Mail- oder Push-Benachrichtigungen für Reservierungserinnerungen.
|
|
||||||
|
|
||||||
3. **Erweiterte Statistiken**: Detailliertere Nutzungsstatistiken und Visualisierungen für Administratoren.
|
|
||||||
|
|
||||||
4. **Mobile App**: Entwicklung einer nativen mobilen App für iOS und Android.
|
|
||||||
|
|
||||||
5. **Verbesserte Automatisierung**: Integration mit weiteren Systemen wie 3D-Drucker-APIs für direktes Monitoring des Druckstatus.
|
|
||||||
|
|
||||||
## Fazit
|
|
||||||
|
|
||||||
Das MYP-Projekt zeigt erfolgreich, wie moderne Webtechnologien und IoT-Geräte kombiniert werden können, um eine praktische Lösung für die Verwaltung von 3D-Druckern zu schaffen.
|
|
||||||
|
|
||||||
Als angehender Fachinformatiker für digitale Vernetzung konnte ich meine Fähigkeiten in den Bereichen Programmierung, Systemintegration, Netzwerkkommunikation und Automatisierung anwenden und erweitern.
|
|
||||||
|
|
||||||
Die Zusammenarbeit im Team mit klarer Aufgabenteilung (Frontend/Backend) hat zu einem erfolgreichen Projektergebnis geführt, das die gestellten Anforderungen erfüllt und einen praktischen Nutzen bietet.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Anhang
|
|
||||||
|
|
||||||
### Installation und Einrichtung
|
|
||||||
|
|
||||||
Detaillierte Anweisungen zur Installation und Einrichtung des Backend-Systems finden sich in der README.md-Datei.
|
|
||||||
|
|
||||||
### Wichtige Konfigurationsparameter
|
|
||||||
|
|
||||||
Die folgenden Umgebungsvariablen müssen konfiguriert werden:
|
|
||||||
|
|
||||||
- `SECRET_KEY`: Geheimer Schlüssel für die Session-Verschlüsselung
|
|
||||||
- `DATABASE_URL`: URL zur Datenbank
|
|
||||||
- `OAUTH_CLIENT_ID`: GitHub OAuth Client ID
|
|
||||||
- `OAUTH_CLIENT_SECRET`: GitHub OAuth Client Secret
|
|
||||||
- `GITHUB_API_BASE_URL`, `GITHUB_AUTHORIZE_URL`, `GITHUB_TOKEN_URL`: URLs für GitHub OAuth
|
|
||||||
- `TAPO_USERNAME`: Benutzername für die Tapo-Steckdosen
|
|
||||||
- `TAPO_PASSWORD`: Passwort für die Tapo-Steckdosen
|
|
||||||
- `TAPO_DEVICES`: JSON-Objekt mit der Zuordnung von Drucker-IDs zu IP-Adressen
|
|
||||||
|
|
||||||
### Cron-Job-Einrichtung
|
|
||||||
|
|
||||||
Für die automatische Überprüfung abgelaufener Jobs kann folgender Cron-Job eingerichtet werden:
|
|
||||||
|
|
||||||
```
|
|
||||||
*/5 * * * * cd /pfad/zum/projekt && /pfad/zur/venv/bin/flask check-jobs >> /pfad/zum/projekt/logs/cron.log 2>&1
|
|
||||||
```
|
|
@ -1,189 +1,118 @@
|
|||||||
# MYP Backend-Steuerungsplattform
|
# MYP - Manage Your Printer
|
||||||
|
|
||||||
Dies ist das Backend für das MYP (Manage Your Printer) Projekt, ein IHK-Abschlussprojekt für Fachinformatiker für digitale Vernetzung. Die Plattform ist mit Python und Flask implementiert und stellt eine RESTful API zur Verfügung, die es ermöglicht, 3D-Drucker zu verwalten, zu reservieren und über WLAN-Steckdosen (Tapo P115) zu steuern.
|
Ein System zur Verwaltung und Steuerung von 3D-Druckern über TP-Link Tapo P110 Smart Plugs.
|
||||||
|
|
||||||
## Funktionen
|
## Funktionsumfang
|
||||||
|
|
||||||
- Lokales Authentifizierungssystem (Offline-fähig)
|
- Benutzer- und Rechteverwaltung (Admin/User)
|
||||||
- Rollen-basierte Zugriffskontrolle (Admin/User/Guest)
|
- Verwaltung von Druckern und Smart Plugs
|
||||||
- Druckerverwaltung (Hinzufügen, Bearbeiten, Löschen)
|
- Reservierungssystem für Drucker (Zeitplanung)
|
||||||
- Reservierungsverwaltung (Erstellen, Abbrechen, Verlängern)
|
- Automatisches Ein-/Ausschalten der Drucker über Smart Plugs
|
||||||
- Fernsteuerung von WLAN-Steckdosen (Tapo P115)
|
- Statistikerfassung für Druckaufträge
|
||||||
- Statistikerfassung und -anzeige
|
- **NEU**: Kiosk-Modus für automatischen Start auf Raspberry Pi
|
||||||
- RESTful API für die Kommunikation mit dem Frontend
|
|
||||||
|
|
||||||
## Technologie-Stack
|
## Systemvoraussetzungen
|
||||||
|
|
||||||
- **Python**: Programmiersprache
|
- Raspberry Pi 4 (oder kompatibel)
|
||||||
- **Flask**: Web-Framework
|
- Python 3.11+
|
||||||
- **SQLite**: Integrierte Datenbank (kann für Produktion durch PostgreSQL ersetzt werden)
|
- Internetzugang für die Installation (danach offline nutzbar)
|
||||||
- **PyP100**: Python-Bibliothek zur Steuerung der Tapo P115 WLAN-Steckdosen
|
- TP-Link Tapo P110 Smart Plugs im lokalen Netzwerk
|
||||||
- **Gunicorn**: WSGI HTTP Server für die Produktionsumgebung
|
|
||||||
- **Docker**: Containerisierung der Anwendung
|
|
||||||
|
|
||||||
## Projekt-Struktur
|
## Installation
|
||||||
|
|
||||||
- `app.py`: Hauptanwendungsdatei mit allen Routen und Modellen
|
### Standardinstallation
|
||||||
- `requirements.txt`: Liste aller Python-Abhängigkeiten
|
|
||||||
- `Dockerfile`: Docker-Konfiguration
|
|
||||||
- `docker-compose.yml`: Docker Compose Konfiguration für einfaches Deployment
|
|
||||||
- `.env.example`: Beispiel für die Umgebungsvariablen
|
|
||||||
- `logs/`: Logdateien (automatisch erstellt)
|
|
||||||
- `instance/`: SQLite-Datenbank (automatisch erstellt)
|
|
||||||
|
|
||||||
## Installation und Ausführung
|
1. Repository klonen oder Dateien auf den Raspberry Pi kopieren
|
||||||
|
|
||||||
### Lokal (Entwicklung)
|
2. Abhängigkeiten installieren:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
1. Python 3.8 oder höher installieren
|
3. Anwendung starten:
|
||||||
2. Repository klonen
|
```bash
|
||||||
3. Ins Projektverzeichnis wechseln
|
python app.py
|
||||||
4. Virtuelle Umgebung erstellen (optional, aber empfohlen)
|
```
|
||||||
```
|
|
||||||
python -m venv venv
|
|
||||||
source venv/bin/activate # Unter Windows: venv\Scripts\activate
|
|
||||||
```
|
|
||||||
5. Abhängigkeiten installieren
|
|
||||||
```
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
6. `.env.example` nach `.env` kopieren und anpassen
|
|
||||||
```
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
7. Anwendung starten
|
|
||||||
```
|
|
||||||
python app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
Die Anwendung läuft dann unter http://localhost:5000 oder unter der IP-Adresse des Raspberry Pi.
|
||||||
|
|
||||||
### Mit Docker
|
### Kiosk-Modus Installation
|
||||||
|
|
||||||
1. Docker und Docker Compose installieren
|
Für den vollautomatischen Start im Kiosk-Modus (z.B. für IHK-Prüfungen):
|
||||||
2. Ins Projektverzeichnis wechseln
|
|
||||||
3. `.env.example` nach `.env` kopieren und anpassen
|
|
||||||
```
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
4. Anwendung starten
|
|
||||||
```
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
```bash
|
||||||
|
chmod +x setup.sh
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
## API-Endpunkte
|
Detaillierte Anweisungen finden sich in der [Kiosk-Setup Anleitung](KIOSK-SETUP.md).
|
||||||
|
|
||||||
|
## 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
|
### Authentifizierung
|
||||||
|
|
||||||
- `POST /auth/register`: Neuen Benutzer registrieren
|
- `POST /auth/register` - Neuen Benutzer registrieren
|
||||||
- `POST /auth/login`: Benutzer anmelden
|
- `POST /auth/login` - Anmelden (erstellt eine Session für 7 Tage)
|
||||||
- `POST /auth/logout`: Abmelden und Session beenden
|
|
||||||
- `POST /api/create-initial-admin`: Initialen Administrator erstellen
|
|
||||||
- `GET /api/me`: Aktuelle Benutzerinformationen abrufen
|
|
||||||
|
|
||||||
### Benutzer
|
|
||||||
|
|
||||||
- `GET /api/users`: Liste aller Benutzer (Admin)
|
|
||||||
- `GET /api/users/<id>`: Details zu einem Benutzer (Admin)
|
|
||||||
- `PUT /api/users/<id>`: Benutzer aktualisieren (Admin)
|
|
||||||
- `DELETE /api/users/<id>`: Benutzer löschen (Admin)
|
|
||||||
|
|
||||||
### Drucker
|
### Drucker
|
||||||
|
|
||||||
- `GET /api/printers`: Liste aller Drucker
|
- `GET /api/printers` - Alle Drucker auflisten
|
||||||
- `POST /api/printers`: Drucker hinzufügen (Admin)
|
- `GET /api/printers/<printerId>` - Einzelnen Drucker abrufen
|
||||||
- `GET /api/printers/<id>`: Details zu einem Drucker
|
- `POST /api/printers` - Neuen Drucker anlegen
|
||||||
- `PUT /api/printers/<id>`: Drucker aktualisieren (Admin)
|
- `DELETE /api/printers/<printerId>` - Drucker löschen
|
||||||
- `DELETE /api/printers/<id>`: Drucker löschen (Admin)
|
|
||||||
|
|
||||||
### Druckaufträge
|
### Jobs/Reservierungen
|
||||||
|
|
||||||
- `GET /api/jobs`: Liste aller Druckaufträge (Admin) oder eigener Druckaufträge (Benutzer)
|
- `GET /api/jobs` - Alle Reservierungen abrufen
|
||||||
- `POST /api/jobs`: Druckauftrag erstellen
|
- `POST /api/jobs` - Neue Reservierung anlegen
|
||||||
- `GET /api/jobs/<id>`: Details zu einem Druckauftrag
|
- `GET /api/jobs/<jobId>` - Reservierungsdetails abrufen
|
||||||
- `POST /api/jobs/<id>/abort`: Druckauftrag abbrechen
|
- `POST /api/jobs/<jobId>/finish` - Job beenden (Plug ausschalten)
|
||||||
- `POST /api/jobs/<id>/finish`: Druckauftrag vorzeitig beenden
|
- `POST /api/jobs/<jobId>/abort` - Job abbrechen (Plug ausschalten)
|
||||||
- `POST /api/jobs/<id>/extend`: Druckauftrag verlängern
|
- `POST /api/jobs/<jobId>/extend` - Endzeit verschieben
|
||||||
- `PUT /api/jobs/<id>/comments`: Kommentare aktualisieren
|
- `GET /api/jobs/<jobId>/status` - Aktuellen Plug-Status abrufen
|
||||||
- `GET /api/job/<id>/remaining-time`: Verbleibende Zeit für einen aktiven Druckauftrag
|
- `GET /api/jobs/<jobId>/remaining-time` - Restzeit in Sekunden abrufen
|
||||||
|
- `DELETE /api/jobs/<jobId>` - Job löschen
|
||||||
|
|
||||||
### Statistiken
|
### Benutzer
|
||||||
|
|
||||||
- `GET /api/stats`: Statistiken zu Druckern, Aufträgen und Benutzern (Admin)
|
- `GET /api/users` - Alle Benutzer auflisten (nur Admin)
|
||||||
|
- `GET /api/users/<userId>` - Einzelnen Benutzer abrufen
|
||||||
|
- `DELETE /api/users/<userId>` - Benutzer löschen (nur Admin)
|
||||||
|
|
||||||
## Datenmodell
|
### Sonstiges
|
||||||
|
|
||||||
### Benutzer (User)
|
- `GET /api/stats` - Globale Statistik (Druckzeit, etc.)
|
||||||
|
- `GET /api/test` - Health-Check
|
||||||
|
|
||||||
- id (String UUID, Primary Key)
|
## Sicherheitshinweise
|
||||||
- username (String, Unique)
|
|
||||||
- password_hash (String)
|
|
||||||
- display_name (String)
|
|
||||||
- email (String, Unique)
|
|
||||||
- role (String, 'admin', 'user' oder 'guest')
|
|
||||||
|
|
||||||
### Session
|
- 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.
|
||||||
|
|
||||||
- id (String UUID, Primary Key)
|
## Wartung und Troubleshooting
|
||||||
- user_id (String UUID, Foreign Key zu User)
|
|
||||||
- expires_at (DateTime)
|
|
||||||
|
|
||||||
### Drucker (Printer)
|
- 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
|
||||||
|
|
||||||
- id (String UUID, Primary Key)
|
|
||||||
- name (String)
|
|
||||||
- description (Text)
|
|
||||||
- status (Integer, 0=available, 1=busy, 2=maintenance)
|
|
||||||
- ip_address (String, IP-Adresse der Tapo-Steckdose)
|
|
||||||
|
|
||||||
### Druckauftrag (PrintJob)
|
|
||||||
|
|
||||||
- id (String UUID, Primary Key)
|
|
||||||
- printer_id (String UUID, Foreign Key zu Printer)
|
|
||||||
- user_id (String UUID, Foreign Key zu User)
|
|
||||||
- start_at (DateTime)
|
|
||||||
- duration_in_minutes (Integer)
|
|
||||||
- comments (Text)
|
|
||||||
- aborted (Boolean)
|
|
||||||
- abort_reason (Text)
|
|
||||||
|
|
||||||
## Steckdosensteuerung
|
|
||||||
|
|
||||||
Die Anwendung steuert Tapo P115 WLAN-Steckdosen, um die Drucker basierend auf Reservierungen ein- und auszuschalten:
|
|
||||||
|
|
||||||
- Bei Erstellung eines Druckauftrags wird die Steckdose des zugehörigen Druckers automatisch eingeschaltet
|
|
||||||
- Bei Abbruch oder vorzeitiger Beendigung eines Druckauftrags wird die Steckdose ausgeschaltet
|
|
||||||
- Nach Ablauf der Reservierungszeit wird die Steckdose automatisch ausgeschaltet
|
|
||||||
- Ein CLI-Befehl `flask check-jobs` überprüft regelmäßig abgelaufene Jobs und schaltet Steckdosen aus
|
|
||||||
|
|
||||||
## Sicherheit
|
|
||||||
|
|
||||||
- Die Anwendung verwendet ein lokales Authentifizierungssystem mit Passwort-Hashing
|
|
||||||
- Sitzungsdaten werden in Server-Side-Sessions gespeichert
|
|
||||||
- Zugriffskontrollen sind implementiert, um sicherzustellen, dass Benutzer nur auf ihre eigenen Daten zugreifen können
|
|
||||||
- Admin-Benutzer haben Zugriff auf alle Daten und können Systemkonfigurationen ändern
|
|
||||||
- Der erste registrierte Benutzer wird automatisch zum Administrator
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
Die Anwendung protokolliert Aktivitäten in rotierenden Logdateien in einem `logs` Verzeichnis. Dies hilft bei der Fehlersuche und Überwachung der Anwendung im Betrieb.
|
|
||||||
|
|
||||||
## Umgebungsvariablen
|
|
||||||
|
|
||||||
Die folgenden Umgebungsvariablen müssen konfiguriert werden:
|
|
||||||
|
|
||||||
- `SECRET_KEY`: Geheimer Schlüssel für die Session-Verschlüsselung
|
|
||||||
- `DATABASE_PATH`: Pfad zur Datenbank (Standard: SQLite-Datenbank im Instance-Verzeichnis)
|
|
||||||
- `TAPO_USERNAME`: Benutzername für die Tapo-Steckdosen
|
|
||||||
- `TAPO_PASSWORD`: Passwort für die Tapo-Steckdosen
|
|
||||||
- `PRINTERS`: JSON-Objekt mit der Zuordnung von Drucker-Namen zu IP-Adressen der Steckdosen im Format: `{"Printer 1": {"ip": "192.168.1.100"}, "Printer 2": {"ip": "192.168.1.101"}, ...}`
|
|
||||||
|
|
||||||
## Automatisierung
|
|
||||||
|
|
||||||
Die Anwendung beinhaltet einen CLI-Befehl `flask check-jobs`, der regelmäßig ausgeführt werden sollte (z.B. als Cron-Job), um abgelaufene Druckaufträge zu überprüfen und die zugehörigen Steckdosen auszuschalten.
|
|
||||||
|
|
||||||
## Kompatibilität mit dem Frontend
|
|
||||||
|
|
||||||
Das Backend wurde speziell für die Kompatibilität mit dem bestehenden Frontend entwickelt, welches in `/frontend` zu finden ist. Die API-Endpunkte und Datenstrukturen sind so gestaltet, dass sie nahtlos mit dem Frontend zusammenarbeiten.
|
|
||||||
|
62
backend/docs/ROADMAP.md
Normal file
62
backend/docs/ROADMAP.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# MYP Entwicklungs-Roadmap
|
||||||
|
|
||||||
|
Dieses Dokument skizziert die geplanten Entwicklungsschritte für MYP (Manage Your Printer).
|
||||||
|
|
||||||
|
## Version 1.0 (aktuell)
|
||||||
|
|
||||||
|
- [x] Benutzer- und Rechteverwaltung (Admin/User)
|
||||||
|
- [x] Verwaltung von Druckern und Smart Plugs
|
||||||
|
- [x] Reservierungssystem für Drucker (Zeitplanung)
|
||||||
|
- [x] Automatisches Ein-/Ausschalten der Drucker über Smart Plugs
|
||||||
|
- [x] Grundlegende Statistikerfassung
|
||||||
|
- [x] Kiosk-Modus für Raspberry Pi
|
||||||
|
|
||||||
|
## Version 1.1 (nächstes Release)
|
||||||
|
|
||||||
|
- [ ] Verbessertes Dashboard mit Echtzeit-Status aller Drucker
|
||||||
|
- [ ] Optimierte Benutzeroberfläche für Touchscreen-Bedienung
|
||||||
|
- [ ] Verbesserte Fehlerbehandlung und Selbstheilungsmechanismen
|
||||||
|
- [ ] Unterstützung für verschiedene Sprachen (Internationalisierung)
|
||||||
|
- [ ] Erweiterte Logging-Funktionalität
|
||||||
|
|
||||||
|
## Version 1.2 (mittelfristig)
|
||||||
|
|
||||||
|
- [ ] Unterstützung für weitere Smart-Plug-Typen (nicht nur TP-Link Tapo)
|
||||||
|
- [ ] Telegram- oder E-Mail-Benachrichtigungen bei Job-Ende/Problemen
|
||||||
|
- [ ] Verbesserte Statistik mit visuellen Darstellungen (Graphen, Charts)
|
||||||
|
- [ ] Exportfunktion für Nutzungsdaten (CSV, PDF)
|
||||||
|
- [ ] Einfache Nutzungsanleitungen direkt in der Web-Oberfläche
|
||||||
|
|
||||||
|
## Version 2.0 (langfristig)
|
||||||
|
|
||||||
|
- [ ] Direkte Integration mit OctoPrint für 3D-Drucker-Steuerung
|
||||||
|
- [ ] Kamera-Integration zur visuellen Überwachung der Drucker
|
||||||
|
- [ ] Materialverwaltung mit Bestandsführung
|
||||||
|
- [ ] Kostenberechnung für Druckaufträge
|
||||||
|
- [ ] Prognose der Energiekosten basierend auf Messwerten der P110-Plugs
|
||||||
|
- [ ] Mobile App für iOS/Android
|
||||||
|
|
||||||
|
## Technische Schulden & Optimierungen
|
||||||
|
|
||||||
|
- [ ] Umstellung auf asynchrone API mit FastAPI
|
||||||
|
- [ ] Frontend mit modernem Framework (Vue.js, React)
|
||||||
|
- [ ] API-Dokumentation mit Swagger/OpenAPI
|
||||||
|
- [ ] Verbessertes Datenbankschema für bessere Skalierbarkeit
|
||||||
|
- [ ] Unit- und Integrationstests
|
||||||
|
- [ ] Containerisierung mit Docker für einfachere Bereitstellung
|
||||||
|
- [ ] Upgrade auf Python 3.12+ und neuere Abhängigkeiten
|
||||||
|
|
||||||
|
## Sicherheitsverbesserungen
|
||||||
|
|
||||||
|
- [ ] HTTPS-Unterstützung
|
||||||
|
- [ ] Verbesserte Passwort-Richtlinien
|
||||||
|
- [ ] 2-Faktor-Authentifizierung für Administratoren
|
||||||
|
- [ ] Regelmäßige Sicherheits-Audits
|
||||||
|
- [ ] Sichere Speicherung von Zugangsdaten (nicht im Code)
|
||||||
|
|
||||||
|
## Wartbarkeit
|
||||||
|
|
||||||
|
- [ ] Umfassendere Dokumentation für Entwickler
|
||||||
|
- [ ] Code-Refactoring für bessere Lesbarkeit und Wartbarkeit
|
||||||
|
- [ ] Aufteilen von app.py in mehrere Module
|
||||||
|
- [ ] Verbessertes Fehlerhandling und Logging
|
@ -1,86 +0,0 @@
|
|||||||
# MYP Backend - Standalone Server Konfiguration
|
|
||||||
# Umgebungsvariablen ausschließlich für den Backend-Server
|
|
||||||
|
|
||||||
# === FLASK KONFIGURATION ===
|
|
||||||
FLASK_APP=app.py
|
|
||||||
FLASK_ENV=production
|
|
||||||
PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
# === DATENBANK KONFIGURATION ===
|
|
||||||
# SQLite (Default)
|
|
||||||
DATABASE_PATH=instance/myp.db
|
|
||||||
|
|
||||||
# PostgreSQL (Optional - für Produktionsumgebung empfohlen)
|
|
||||||
DB_NAME=myp_backend
|
|
||||||
DB_USER=myp_user
|
|
||||||
DB_PASSWORD=secure_backend_password
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
|
|
||||||
# === SICHERHEIT ===
|
|
||||||
SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F
|
|
||||||
JWT_SECRET=secure-jwt-secret-backend-2024
|
|
||||||
JWT_ACCESS_TOKEN_EXPIRES=3600
|
|
||||||
JWT_REFRESH_TOKEN_EXPIRES=2592000
|
|
||||||
|
|
||||||
# === CORS KONFIGURATION ===
|
|
||||||
# Erlaubte Frontend-Origins
|
|
||||||
CORS_ORIGINS=http://localhost:3000,https://frontend.myp.local,https://myp.frontend.local
|
|
||||||
|
|
||||||
# === DRUCKER KONFIGURATION ===
|
|
||||||
PRINTERS={"Drucker 1": {"ip": "192.168.0.100"}, "Drucker 2": {"ip": "192.168.0.101"}, "Drucker 3": {"ip": "192.168.0.102"}, "Drucker 4": {"ip": "192.168.0.103"}, "Drucker 5": {"ip": "192.168.0.104"}, "Drucker 6": {"ip": "192.168.0.106"}}
|
|
||||||
|
|
||||||
# === TAPO SMART PLUG ===
|
|
||||||
TAPO_USERNAME=till.tomczak@mercedes-benz.com
|
|
||||||
TAPO_PASSWORD=744563017196A
|
|
||||||
|
|
||||||
# === NETZWERK KONFIGURATION ===
|
|
||||||
HOST=0.0.0.0
|
|
||||||
PORT=5000
|
|
||||||
BACKEND_URL=http://localhost:5000
|
|
||||||
|
|
||||||
# === CACHE KONFIGURATION ===
|
|
||||||
REDIS_PASSWORD=backend_cache_password
|
|
||||||
REDIS_HOST=localhost
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_DB=0
|
|
||||||
|
|
||||||
# === LOGGING ===
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
LOG_FILE=logs/backend.log
|
|
||||||
LOG_MAX_BYTES=10485760
|
|
||||||
LOG_BACKUP_COUNT=5
|
|
||||||
|
|
||||||
# === PRODUKTIONS-KONFIGURATION ===
|
|
||||||
# Gunicorn-Einstellungen
|
|
||||||
WORKERS=4
|
|
||||||
BIND_ADDRESS=0.0.0.0:5000
|
|
||||||
TIMEOUT=30
|
|
||||||
KEEP_ALIVE=5
|
|
||||||
MAX_REQUESTS=1000
|
|
||||||
MAX_REQUESTS_JITTER=100
|
|
||||||
|
|
||||||
# === SICHERHEITS-EINSTELLUNGEN ===
|
|
||||||
WTF_CSRF_ENABLED=true
|
|
||||||
FORCE_HTTPS=false
|
|
||||||
RATE_LIMIT_ENABLED=true
|
|
||||||
MAX_REQUESTS_PER_MINUTE=60
|
|
||||||
RATE_LIMIT_WINDOW_MINUTES=15
|
|
||||||
|
|
||||||
# === MONITORING ===
|
|
||||||
HEALTH_CHECK_INTERVAL=30
|
|
||||||
METRICS_ENABLED=true
|
|
||||||
METRICS_PORT=9090
|
|
||||||
|
|
||||||
# === JOB-KONFIGURATION ===
|
|
||||||
JOB_CHECK_INTERVAL=60
|
|
||||||
SOCKET_CHECK_INTERVAL=120
|
|
||||||
|
|
||||||
# === DATEISYSTEM ===
|
|
||||||
UPLOAD_FOLDER=uploads
|
|
||||||
MAX_CONTENT_LENGTH=16777216
|
|
||||||
|
|
||||||
# === ENTWICKLUNG ===
|
|
||||||
DEBUG=false
|
|
||||||
TESTING=false
|
|
||||||
DEVELOPMENT=false
|
|
@ -1,68 +0,0 @@
|
|||||||
# MYP Backend - Umgebungsvariablen Konfiguration
|
|
||||||
# Kopiere diese Datei zu .env und passe die Werte an deine Umgebung an
|
|
||||||
|
|
||||||
# === Flask-Konfiguration ===
|
|
||||||
# Umgebung: development, production, testing
|
|
||||||
FLASK_ENV=production
|
|
||||||
|
|
||||||
# Geheimer Schlüssel für Sessions und Tokens
|
|
||||||
# WICHTIG: Generiere einen sicheren Schlüssel für die Produktion!
|
|
||||||
# Beispiel: python -c "import secrets; print(secrets.token_hex(32))"
|
|
||||||
SECRET_KEY=your-super-secret-key-here
|
|
||||||
|
|
||||||
# === Datenbank ===
|
|
||||||
# Pfad zur SQLite-Datenbankdatei
|
|
||||||
DATABASE_PATH=instance/myp.db
|
|
||||||
|
|
||||||
# === Job-Verwaltung ===
|
|
||||||
# Intervall für Job-Überprüfung in Sekunden
|
|
||||||
JOB_CHECK_INTERVAL=60
|
|
||||||
|
|
||||||
# === Tapo Smart Plugs ===
|
|
||||||
# Anmeldedaten für Tapo-Steckdosen
|
|
||||||
TAPO_USERNAME=your-tapo-email@example.com
|
|
||||||
TAPO_PASSWORD=your-tapo-password
|
|
||||||
|
|
||||||
# Drucker-Konfiguration (JSON-Format)
|
|
||||||
# Beispiel: {"Drucker1":{"ip":"192.168.1.100"},"Drucker2":{"ip":"192.168.1.101"}}
|
|
||||||
PRINTERS={}
|
|
||||||
|
|
||||||
# === Sicherheit ===
|
|
||||||
# API-Schlüssel für externe Zugriffe (optional)
|
|
||||||
API_KEY=
|
|
||||||
|
|
||||||
# Rate Limiting
|
|
||||||
MAX_REQUESTS_PER_MINUTE=60
|
|
||||||
RATE_LIMIT_WINDOW_MINUTES=15
|
|
||||||
|
|
||||||
# HTTPS-Erzwingung (nur in Produktion mit SSL-Zertifikat)
|
|
||||||
FORCE_HTTPS=false
|
|
||||||
|
|
||||||
# === Logging ===
|
|
||||||
# Log-Level: DEBUG, INFO, WARNING, ERROR
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
|
|
||||||
# Maximale Log-Dateigröße in Bytes (Standard: 10MB)
|
|
||||||
LOG_MAX_BYTES=10485760
|
|
||||||
|
|
||||||
# Anzahl der Log-Backup-Dateien
|
|
||||||
LOG_BACKUP_COUNT=10
|
|
||||||
|
|
||||||
# === Server-Konfiguration ===
|
|
||||||
# Anzahl der Gunicorn-Worker-Prozesse
|
|
||||||
WORKERS=4
|
|
||||||
|
|
||||||
# Server-Adresse und Port
|
|
||||||
BIND_ADDRESS=0.0.0.0
|
|
||||||
PORT=5000
|
|
||||||
|
|
||||||
# Request-Timeout in Sekunden
|
|
||||||
TIMEOUT=30
|
|
||||||
|
|
||||||
# === Monitoring ===
|
|
||||||
# Aktiviere Metriken-Sammlung
|
|
||||||
METRICS_ENABLED=true
|
|
||||||
|
|
||||||
# === Entwicklung (nur für FLASK_ENV=development) ===
|
|
||||||
# Debug-Modus
|
|
||||||
DEBUG=false
|
|
@ -1,346 +0,0 @@
|
|||||||
"""
|
|
||||||
Routing-Modul für die neue Frontend V2-Implementierung.
|
|
||||||
Stellt die notwendigen Endpunkte bereit, während die Original-API-Endpunkte intakt bleiben.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify, g, session
|
|
||||||
import datetime
|
|
||||||
import os
|
|
||||||
from functools import wraps
|
|
||||||
import logging
|
|
||||||
import jwt
|
|
||||||
|
|
||||||
# Blueprint für Frontend V2 erstellen
|
|
||||||
frontend_v2 = Blueprint('frontend_v2', __name__, template_folder='frontend_v2/templates')
|
|
||||||
|
|
||||||
# Logger konfigurieren
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Importiere Funktionen aus dem Hauptmodul
|
|
||||||
# Diese werden während der Registrierung des Blueprints übergeben,
|
|
||||||
# um zirkuläre Importe zu vermeiden
|
|
||||||
get_current_user = None
|
|
||||||
get_user_by_id = None
|
|
||||||
get_socket_by_id = None
|
|
||||||
get_job_by_id = None
|
|
||||||
get_all_sockets = None
|
|
||||||
get_all_users = None
|
|
||||||
get_all_jobs = None
|
|
||||||
get_jobs_by_user = None
|
|
||||||
login_required = None
|
|
||||||
admin_required = None
|
|
||||||
delete_session = None
|
|
||||||
socket_to_dict = None
|
|
||||||
job_to_dict = None
|
|
||||||
user_to_dict = None
|
|
||||||
|
|
||||||
def set_app_functions(app_functions):
|
|
||||||
"""
|
|
||||||
Setzt die importierten Funktionen aus dem Hauptmodul.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app_functions: Ein Dictionary mit Funktionen aus dem Hauptmodul
|
|
||||||
"""
|
|
||||||
global get_current_user, get_user_by_id, get_socket_by_id, get_job_by_id
|
|
||||||
global get_all_sockets, get_all_users, get_all_jobs, get_jobs_by_user
|
|
||||||
global login_required, admin_required, delete_session
|
|
||||||
global socket_to_dict, job_to_dict, user_to_dict
|
|
||||||
|
|
||||||
get_current_user = app_functions.get('get_current_user')
|
|
||||||
get_user_by_id = app_functions.get('get_user_by_id')
|
|
||||||
get_socket_by_id = app_functions.get('get_socket_by_id')
|
|
||||||
get_job_by_id = app_functions.get('get_job_by_id')
|
|
||||||
get_all_sockets = app_functions.get('get_all_sockets')
|
|
||||||
get_all_users = app_functions.get('get_all_users')
|
|
||||||
get_all_jobs = app_functions.get('get_all_jobs')
|
|
||||||
get_jobs_by_user = app_functions.get('get_jobs_by_user')
|
|
||||||
login_required = app_functions.get('login_required')
|
|
||||||
admin_required = app_functions.get('admin_required')
|
|
||||||
delete_session = app_functions.get('delete_session')
|
|
||||||
socket_to_dict = app_functions.get('socket_to_dict')
|
|
||||||
job_to_dict = app_functions.get('job_to_dict')
|
|
||||||
user_to_dict = app_functions.get('user_to_dict')
|
|
||||||
|
|
||||||
# Wrapper für Login-Erfordernis im Frontend
|
|
||||||
def frontend_login_required(f):
|
|
||||||
@wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
user = get_current_user()
|
|
||||||
if not user:
|
|
||||||
flash('Bitte melden Sie sich an, um diese Seite zu besuchen.', 'error')
|
|
||||||
return redirect(url_for('frontend_v2.login'))
|
|
||||||
|
|
||||||
g.current_user = user
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
# Wrapper für Admin-Erfordernis im Frontend
|
|
||||||
def frontend_admin_required(f):
|
|
||||||
@wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
if not g.get('current_user') or g.current_user['role'] != 'admin':
|
|
||||||
flash('Sie benötigen Administrator-Rechte, um diese Seite zu besuchen.', 'error')
|
|
||||||
return redirect(url_for('frontend_v2.dashboard'))
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
# Öffentliche Routen
|
|
||||||
@frontend_v2.route('/')
|
|
||||||
def index():
|
|
||||||
current_user = get_current_user()
|
|
||||||
if current_user:
|
|
||||||
return redirect(url_for('frontend_v2.dashboard'))
|
|
||||||
return redirect(url_for('frontend_v2.login'))
|
|
||||||
|
|
||||||
@frontend_v2.route('/login')
|
|
||||||
def login():
|
|
||||||
current_user = get_current_user()
|
|
||||||
if current_user:
|
|
||||||
return redirect(url_for('frontend_v2.dashboard'))
|
|
||||||
return render_template('login.html', current_user=None, active_page='login')
|
|
||||||
|
|
||||||
@frontend_v2.route('/register')
|
|
||||||
def register():
|
|
||||||
current_user = get_current_user()
|
|
||||||
if current_user:
|
|
||||||
return redirect(url_for('frontend_v2.dashboard'))
|
|
||||||
return render_template('register.html', current_user=None, active_page='register')
|
|
||||||
|
|
||||||
@frontend_v2.route('/logout')
|
|
||||||
def logout():
|
|
||||||
session_id = session.get('session_id')
|
|
||||||
if session_id:
|
|
||||||
delete_session(session_id)
|
|
||||||
session.pop('session_id', None)
|
|
||||||
|
|
||||||
flash('Sie wurden erfolgreich abgemeldet.', 'success')
|
|
||||||
return redirect(url_for('frontend_v2.login'))
|
|
||||||
|
|
||||||
# Geschützte Routen
|
|
||||||
@frontend_v2.route('/dashboard')
|
|
||||||
@frontend_login_required
|
|
||||||
def dashboard():
|
|
||||||
current_user = g.current_user
|
|
||||||
current_year = datetime.datetime.now().year
|
|
||||||
return render_template('dashboard.html',
|
|
||||||
current_user=user_to_dict(current_user),
|
|
||||||
current_year=current_year,
|
|
||||||
active_page='dashboard')
|
|
||||||
|
|
||||||
@frontend_v2.route('/jobs')
|
|
||||||
@frontend_login_required
|
|
||||||
def jobs():
|
|
||||||
current_user = g.current_user
|
|
||||||
|
|
||||||
# Admins sehen alle Jobs, normale Benutzer nur ihre eigenen
|
|
||||||
if current_user['role'] == 'admin':
|
|
||||||
all_jobs = get_all_jobs()
|
|
||||||
else:
|
|
||||||
all_jobs = get_jobs_by_user(current_user['id'])
|
|
||||||
|
|
||||||
jobs_list = [job_to_dict(job) for job in all_jobs]
|
|
||||||
|
|
||||||
# Sortiere Jobs nach Startzeit (neueste zuerst)
|
|
||||||
jobs_list.sort(key=lambda x: x['startAt'], reverse=True)
|
|
||||||
|
|
||||||
# Gruppiere Jobs nach Status (aktiv, abgebrochen, abgeschlossen)
|
|
||||||
active_jobs = [job for job in jobs_list if job['remainingMinutes'] > 0 and not job['aborted']]
|
|
||||||
completed_jobs = [job for job in jobs_list if job['remainingMinutes'] == 0 and not job['aborted']]
|
|
||||||
aborted_jobs = [job for job in jobs_list if job['aborted']]
|
|
||||||
|
|
||||||
return render_template('jobs.html',
|
|
||||||
current_user=user_to_dict(current_user),
|
|
||||||
active_jobs=active_jobs,
|
|
||||||
completed_jobs=completed_jobs,
|
|
||||||
aborted_jobs=aborted_jobs,
|
|
||||||
active_page='jobs')
|
|
||||||
|
|
||||||
@frontend_v2.route('/job/<job_id>')
|
|
||||||
@frontend_login_required
|
|
||||||
def job_details(job_id):
|
|
||||||
current_user = g.current_user
|
|
||||||
|
|
||||||
job = get_job_by_id(job_id)
|
|
||||||
if not job:
|
|
||||||
flash('Der angeforderte Job wurde nicht gefunden.', 'error')
|
|
||||||
return redirect(url_for('frontend_v2.jobs'))
|
|
||||||
|
|
||||||
# Benutzer können nur ihre eigenen Jobs sehen, es sei denn, sie sind Admins
|
|
||||||
if current_user['role'] != 'admin' and job['user_id'] != current_user['id']:
|
|
||||||
flash('Sie haben keine Berechtigung, diesen Job anzusehen.', 'error')
|
|
||||||
return redirect(url_for('frontend_v2.jobs'))
|
|
||||||
|
|
||||||
job_data = job_to_dict(job)
|
|
||||||
|
|
||||||
# Holen Sie sich die Drucker-Informationen
|
|
||||||
printer = get_socket_by_id(job['socket_id'])
|
|
||||||
printer_data = socket_to_dict(printer) if printer else None
|
|
||||||
|
|
||||||
# Benutzerinformationen abrufen
|
|
||||||
job_user = get_user_by_id(job['user_id'])
|
|
||||||
job_user_data = user_to_dict(job_user) if job_user else None
|
|
||||||
|
|
||||||
return render_template('job_details.html',
|
|
||||||
current_user=user_to_dict(current_user),
|
|
||||||
job=job_data,
|
|
||||||
printer=printer_data,
|
|
||||||
job_user=job_user_data,
|
|
||||||
active_page='jobs')
|
|
||||||
|
|
||||||
@frontend_v2.route('/profile')
|
|
||||||
@frontend_login_required
|
|
||||||
def profile():
|
|
||||||
current_user = g.current_user
|
|
||||||
|
|
||||||
# Benutzer-Jobs abrufen
|
|
||||||
user_jobs = get_jobs_by_user(current_user['id'])
|
|
||||||
jobs_list = [job_to_dict(job) for job in user_jobs]
|
|
||||||
|
|
||||||
# Jobs nach Startzeit sortieren (neueste zuerst)
|
|
||||||
jobs_list.sort(key=lambda x: x['startAt'], reverse=True)
|
|
||||||
|
|
||||||
# Gruppiere Jobs nach Status (aktiv, abgebrochen, abgeschlossen)
|
|
||||||
active_jobs = [job for job in jobs_list if job['remainingMinutes'] > 0 and not job['aborted']]
|
|
||||||
recent_jobs = jobs_list[:5] # Die 5 neuesten Jobs
|
|
||||||
|
|
||||||
# Nutzungsstatistiken berechnen
|
|
||||||
total_jobs = len(jobs_list)
|
|
||||||
total_minutes_used = sum(job['durationInMinutes'] for job in jobs_list if not job['aborted'])
|
|
||||||
avg_duration = total_minutes_used // total_jobs if total_jobs > 0 else 0
|
|
||||||
|
|
||||||
return render_template('profile.html',
|
|
||||||
current_user=user_to_dict(current_user),
|
|
||||||
active_jobs=active_jobs,
|
|
||||||
recent_jobs=recent_jobs,
|
|
||||||
total_jobs=total_jobs,
|
|
||||||
total_minutes_used=total_minutes_used,
|
|
||||||
avg_duration=avg_duration,
|
|
||||||
active_page='profile')
|
|
||||||
|
|
||||||
# Admin-Routen
|
|
||||||
@frontend_v2.route('/printers')
|
|
||||||
@frontend_login_required
|
|
||||||
@frontend_admin_required
|
|
||||||
def printers():
|
|
||||||
current_user = g.current_user
|
|
||||||
|
|
||||||
# Alle Drucker abrufen
|
|
||||||
all_sockets = get_all_sockets()
|
|
||||||
printers_list = [socket_to_dict(socket) for socket in all_sockets]
|
|
||||||
|
|
||||||
return render_template('printers.html',
|
|
||||||
current_user=user_to_dict(current_user),
|
|
||||||
printers=printers_list,
|
|
||||||
active_page='printers')
|
|
||||||
|
|
||||||
@frontend_v2.route('/printer/<printer_id>')
|
|
||||||
@frontend_login_required
|
|
||||||
@frontend_admin_required
|
|
||||||
def printer_details(printer_id):
|
|
||||||
current_user = g.current_user
|
|
||||||
|
|
||||||
printer = get_socket_by_id(printer_id)
|
|
||||||
if not printer:
|
|
||||||
flash('Der angeforderte Drucker wurde nicht gefunden.', 'error')
|
|
||||||
return redirect(url_for('frontend_v2.printers'))
|
|
||||||
|
|
||||||
printer_data = socket_to_dict(printer)
|
|
||||||
|
|
||||||
return render_template('printer_details.html',
|
|
||||||
current_user=user_to_dict(current_user),
|
|
||||||
printer=printer_data,
|
|
||||||
active_page='printers')
|
|
||||||
|
|
||||||
@frontend_v2.route('/users')
|
|
||||||
@frontend_login_required
|
|
||||||
@frontend_admin_required
|
|
||||||
def users():
|
|
||||||
current_user = g.current_user
|
|
||||||
|
|
||||||
# Alle Benutzer abrufen
|
|
||||||
all_users = get_all_users()
|
|
||||||
users_list = [user_to_dict(user) for user in all_users]
|
|
||||||
|
|
||||||
return render_template('users.html',
|
|
||||||
current_user=user_to_dict(current_user),
|
|
||||||
users=users_list,
|
|
||||||
active_page='users')
|
|
||||||
|
|
||||||
@frontend_v2.route('/user/<user_id>')
|
|
||||||
@frontend_login_required
|
|
||||||
@frontend_admin_required
|
|
||||||
def user_details(user_id):
|
|
||||||
current_user = g.current_user
|
|
||||||
|
|
||||||
user = get_user_by_id(user_id)
|
|
||||||
if not user:
|
|
||||||
flash('Der angeforderte Benutzer wurde nicht gefunden.', 'error')
|
|
||||||
return redirect(url_for('frontend_v2.users'))
|
|
||||||
|
|
||||||
user_data = user_to_dict(user)
|
|
||||||
|
|
||||||
# Benutzer-Jobs abrufen
|
|
||||||
user_jobs = get_jobs_by_user(user_id)
|
|
||||||
jobs_list = [job_to_dict(job) for job in user_jobs]
|
|
||||||
|
|
||||||
# Jobs nach Startzeit sortieren (neueste zuerst)
|
|
||||||
jobs_list.sort(key=lambda x: x['startAt'], reverse=True)
|
|
||||||
|
|
||||||
# Gruppiere Jobs nach Status
|
|
||||||
active_jobs = [job for job in jobs_list if job['remainingMinutes'] > 0 and not job['aborted']]
|
|
||||||
completed_jobs = [job for job in jobs_list if job['remainingMinutes'] == 0 and not job['aborted']]
|
|
||||||
aborted_jobs = [job for job in jobs_list if job['aborted']]
|
|
||||||
|
|
||||||
# Nutzungsstatistiken berechnen
|
|
||||||
total_jobs = len(jobs_list)
|
|
||||||
total_minutes_used = sum(job['durationInMinutes'] for job in jobs_list if not job['aborted'])
|
|
||||||
avg_duration = total_minutes_used // total_jobs if total_jobs > 0 else 0
|
|
||||||
|
|
||||||
return render_template('user_details.html',
|
|
||||||
current_user=user_to_dict(current_user),
|
|
||||||
user=user_data,
|
|
||||||
active_jobs=active_jobs,
|
|
||||||
completed_jobs=completed_jobs,
|
|
||||||
aborted_jobs=aborted_jobs,
|
|
||||||
total_jobs=total_jobs,
|
|
||||||
total_minutes_used=total_minutes_used,
|
|
||||||
avg_duration=avg_duration,
|
|
||||||
active_page='users')
|
|
||||||
|
|
||||||
@frontend_v2.route('/statistics')
|
|
||||||
@frontend_login_required
|
|
||||||
@frontend_admin_required
|
|
||||||
def statistics():
|
|
||||||
current_user = g.current_user
|
|
||||||
|
|
||||||
return render_template('statistics.html',
|
|
||||||
current_user=user_to_dict(current_user),
|
|
||||||
active_page='statistics')
|
|
||||||
|
|
||||||
# Fehlerbehandlung
|
|
||||||
@frontend_v2.errorhandler(404)
|
|
||||||
def page_not_found(e):
|
|
||||||
current_user = get_current_user()
|
|
||||||
return render_template('error.html',
|
|
||||||
current_user=user_to_dict(current_user) if current_user else None,
|
|
||||||
error_code=404,
|
|
||||||
error_message='Die angeforderte Seite wurde nicht gefunden.'), 404
|
|
||||||
|
|
||||||
@frontend_v2.errorhandler(403)
|
|
||||||
def forbidden(e):
|
|
||||||
current_user = get_current_user()
|
|
||||||
return render_template('error.html',
|
|
||||||
current_user=user_to_dict(current_user) if current_user else None,
|
|
||||||
error_code=403,
|
|
||||||
error_message='Sie haben keine Berechtigung, auf diese Seite zuzugreifen.'), 403
|
|
||||||
|
|
||||||
@frontend_v2.errorhandler(500)
|
|
||||||
def server_error(e):
|
|
||||||
current_user = get_current_user()
|
|
||||||
logger.error(f'Serverfehler: {e}')
|
|
||||||
return render_template('error.html',
|
|
||||||
current_user=user_to_dict(current_user) if current_user else None,
|
|
||||||
error_code=500,
|
|
||||||
error_message='Ein interner Serverfehler ist aufgetreten.'), 500
|
|
@ -1,284 +0,0 @@
|
|||||||
# MYP Backend - Windows PowerShell Installations-Skript
|
|
||||||
# Installiert das Backend für Produktionsbetrieb oder Entwicklung
|
|
||||||
|
|
||||||
param(
|
|
||||||
[switch]$Production,
|
|
||||||
[switch]$Development,
|
|
||||||
[switch]$Clean,
|
|
||||||
[switch]$Logs,
|
|
||||||
[switch]$Help
|
|
||||||
)
|
|
||||||
|
|
||||||
# Farben für PowerShell
|
|
||||||
$Red = "Red"
|
|
||||||
$Green = "Green"
|
|
||||||
$Yellow = "Yellow"
|
|
||||||
$Blue = "Cyan"
|
|
||||||
|
|
||||||
function Write-Log {
|
|
||||||
param([string]$Message, [string]$Color = "White")
|
|
||||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
||||||
Write-Host "[$timestamp] $Message" -ForegroundColor $Color
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Success {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Log "SUCCESS: $Message" -Color $Green
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Warning {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Log "WARNING: $Message" -Color $Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Error {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Log "FEHLER: $Message" -Color $Red
|
|
||||||
}
|
|
||||||
|
|
||||||
# Verbesserte Funktion zum Parsen der Umgebungsvariablen
|
|
||||||
function Set-EnvironmentFromFile {
|
|
||||||
param([string]$FilePath)
|
|
||||||
|
|
||||||
if (-not (Test-Path $FilePath)) {
|
|
||||||
Write-Warning "$FilePath nicht gefunden"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log "⚙️ Lade Umgebungsvariablen aus $FilePath..." -Color $Blue
|
|
||||||
|
|
||||||
try {
|
|
||||||
$EnvContent = Get-Content $FilePath -Raw
|
|
||||||
$Lines = $EnvContent -split "`r?`n"
|
|
||||||
|
|
||||||
foreach ($Line in $Lines) {
|
|
||||||
# Überspringe leere Zeilen und Kommentare
|
|
||||||
if ([string]::IsNullOrWhiteSpace($Line) -or $Line.TrimStart().StartsWith('#')) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Finde den ersten = Zeichen
|
|
||||||
$EqualIndex = $Line.IndexOf('=')
|
|
||||||
if ($EqualIndex -le 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extrahiere Key und Value
|
|
||||||
$Key = $Line.Substring(0, $EqualIndex).Trim()
|
|
||||||
$Value = $Line.Substring($EqualIndex + 1).Trim()
|
|
||||||
|
|
||||||
# Entferne umgebende Anführungszeichen, falls vorhanden
|
|
||||||
if (($Value.StartsWith('"') -and $Value.EndsWith('"')) -or
|
|
||||||
($Value.StartsWith("'") -and $Value.EndsWith("'"))) {
|
|
||||||
$Value = $Value.Substring(1, $Value.Length - 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Setze Umgebungsvariable
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($Key)) {
|
|
||||||
[Environment]::SetEnvironmentVariable($Key, $Value, "Process")
|
|
||||||
Write-Log "Geladen: $Key" -Color $Blue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Success "Umgebungsvariablen erfolgreich geladen"
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
Write-Error "Fehler beim Laden der Umgebungsvariablen: $_"
|
|
||||||
Write-Warning "Verwende Standard-Umgebungsvariablen"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Banner
|
|
||||||
Write-Host "========================================" -ForegroundColor $Blue
|
|
||||||
Write-Host "🏭 MYP Backend - Windows Installation" -ForegroundColor $Blue
|
|
||||||
Write-Host "========================================" -ForegroundColor $Blue
|
|
||||||
|
|
||||||
if ($Help) {
|
|
||||||
Write-Host @"
|
|
||||||
Verwendung: .\install.ps1 [OPTIONEN]
|
|
||||||
|
|
||||||
OPTIONEN:
|
|
||||||
-Production Produktions-Installation
|
|
||||||
-Development Entwicklungs-Installation
|
|
||||||
-Clean Bereinige vorherige Installation
|
|
||||||
-Logs Zeige detaillierte Logs
|
|
||||||
-Help Zeige diese Hilfe
|
|
||||||
|
|
||||||
BEISPIELE:
|
|
||||||
.\install.ps1 -Production
|
|
||||||
.\install.ps1 -Development -Logs
|
|
||||||
.\install.ps1 -Clean
|
|
||||||
"@
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bestimme Installationsmodus
|
|
||||||
$InstallMode = "development"
|
|
||||||
if ($Production) {
|
|
||||||
$InstallMode = "production"
|
|
||||||
Write-Log "🏭 Produktions-Installation gestartet" -Color $Blue
|
|
||||||
} elseif ($Development) {
|
|
||||||
$InstallMode = "development"
|
|
||||||
Write-Log "🔧 Entwicklungs-Installation gestartet" -Color $Blue
|
|
||||||
} else {
|
|
||||||
Write-Log "🔧 Standard-Installation (Entwicklung)" -Color $Blue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Arbeitsverzeichnis prüfen
|
|
||||||
$CurrentDir = Get-Location
|
|
||||||
Write-Log "Arbeitsverzeichnis: $CurrentDir"
|
|
||||||
|
|
||||||
if (-not (Test-Path "app.py")) {
|
|
||||||
Write-Error "app.py nicht gefunden! Bitte im Backend-Verzeichnis ausführen."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Python-Version prüfen
|
|
||||||
Write-Log "🐍 Prüfe Python-Installation..." -Color $Blue
|
|
||||||
|
|
||||||
try {
|
|
||||||
$PythonVersion = python --version 2>&1
|
|
||||||
Write-Log "Python-Version: $PythonVersion"
|
|
||||||
|
|
||||||
# Prüfe Mindestversion (3.8+)
|
|
||||||
$VersionMatch = $PythonVersion -match "Python (\d+)\.(\d+)"
|
|
||||||
if ($VersionMatch) {
|
|
||||||
$Major = [int]$Matches[1]
|
|
||||||
$Minor = [int]$Matches[2]
|
|
||||||
|
|
||||||
if ($Major -lt 3 -or ($Major -eq 3 -and $Minor -lt 8)) {
|
|
||||||
Write-Error "Python 3.8+ erforderlich, gefunden: $PythonVersion"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Success "Python-Version ist kompatibel"
|
|
||||||
} catch {
|
|
||||||
Write-Error "Python nicht gefunden! Bitte Python 3.8+ installieren."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bereinigung (falls gewünscht)
|
|
||||||
if ($Clean) {
|
|
||||||
Write-Log "🧹 Bereinige vorherige Installation..." -Color $Yellow
|
|
||||||
|
|
||||||
if (Test-Path "instance") {
|
|
||||||
Remove-Item -Recurse -Force "instance"
|
|
||||||
Write-Log "Datenbank-Verzeichnis entfernt"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path "logs") {
|
|
||||||
Remove-Item -Recurse -Force "logs"
|
|
||||||
Write-Log "Log-Verzeichnis entfernt"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Success "Bereinigung abgeschlossen"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Erstelle erforderliche Verzeichnisse
|
|
||||||
Write-Log "📁 Erstelle Verzeichnisse..." -Color $Blue
|
|
||||||
|
|
||||||
$Directories = @("instance", "logs", "uploads")
|
|
||||||
foreach ($Dir in $Directories) {
|
|
||||||
if (-not (Test-Path $Dir)) {
|
|
||||||
New-Item -ItemType Directory -Path $Dir | Out-Null
|
|
||||||
Write-Log "Verzeichnis erstellt: $Dir"
|
|
||||||
} else {
|
|
||||||
Write-Log "Verzeichnis existiert bereits: $Dir"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Installiere Python-Dependencies
|
|
||||||
Write-Log "📦 Installiere Python-Pakete..." -Color $Blue
|
|
||||||
|
|
||||||
if (Test-Path "requirements.txt") {
|
|
||||||
try {
|
|
||||||
if ($Logs) {
|
|
||||||
pip install -r requirements.txt
|
|
||||||
} else {
|
|
||||||
pip install -r requirements.txt --quiet
|
|
||||||
}
|
|
||||||
Write-Success "Python-Pakete installiert"
|
|
||||||
} catch {
|
|
||||||
Write-Error "Fehler beim Installieren der Python-Pakete: $_"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Warning "requirements.txt nicht gefunden"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Umgebungskonfiguration
|
|
||||||
Write-Log "⚙️ Konfiguriere Umgebung..." -Color $Blue
|
|
||||||
|
|
||||||
# Lade Umgebungsvariablen für Tests
|
|
||||||
Set-EnvironmentFromFile "env.backend"
|
|
||||||
|
|
||||||
# Datenbank initialisieren
|
|
||||||
Write-Log "🗄️ Initialisiere Datenbank..." -Color $Blue
|
|
||||||
|
|
||||||
try {
|
|
||||||
$env:FLASK_APP = "app.py"
|
|
||||||
$env:FLASK_ENV = $InstallMode
|
|
||||||
|
|
||||||
# Verwende das Test-Skript für die Datenbank-Initialisierung
|
|
||||||
Write-Log "Führe Datenbank-Initialisierung über Test-Skript aus..."
|
|
||||||
python test-backend-setup.py
|
|
||||||
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
Write-Success "Datenbank erfolgreich initialisiert"
|
|
||||||
} else {
|
|
||||||
Write-Warning "Datenbank-Initialisierung mit Warnungen abgeschlossen"
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Error "Fehler bei der Datenbank-Initialisierung: $_"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Konfigurationstest
|
|
||||||
Write-Log "🧪 Teste Konfiguration..." -Color $Blue
|
|
||||||
|
|
||||||
try {
|
|
||||||
python test-backend-setup.py | Out-Null
|
|
||||||
$TestResult = $LASTEXITCODE
|
|
||||||
|
|
||||||
if ($TestResult -eq 0) {
|
|
||||||
Write-Success "Alle Konfigurationstests bestanden"
|
|
||||||
} else {
|
|
||||||
Write-Warning "Einige Konfigurationstests fehlgeschlagen (Code: $TestResult)"
|
|
||||||
if ($Logs) {
|
|
||||||
Write-Log "Führe detaillierte Tests aus..."
|
|
||||||
python test-backend-setup.py
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Warning "Konfigurationstest konnte nicht ausgeführt werden: $_"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Installation abgeschlossen
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "========================================" -ForegroundColor $Green
|
|
||||||
Write-Host "✅ MYP Backend Installation abgeschlossen!" -ForegroundColor $Green
|
|
||||||
Write-Host "========================================" -ForegroundColor $Green
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
Write-Host "📋 Nächste Schritte:" -ForegroundColor $Blue
|
|
||||||
Write-Host "1. Backend starten:" -ForegroundColor $White
|
|
||||||
Write-Host " .\start-backend-server.ps1 -Development" -ForegroundColor $Yellow
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "2. Health-Check testen:" -ForegroundColor $White
|
|
||||||
Write-Host " curl http://localhost:5000/monitoring/health/simple" -ForegroundColor $Yellow
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "3. Logs überwachen:" -ForegroundColor $White
|
|
||||||
Write-Host " Get-Content logs\myp.log -Wait" -ForegroundColor $Yellow
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
if ($InstallMode -eq "production") {
|
|
||||||
Write-Host "🏭 Produktions-Hinweise:" -ForegroundColor $Blue
|
|
||||||
Write-Host "- Verwende einen Reverse Proxy (nginx/Apache)" -ForegroundColor $White
|
|
||||||
Write-Host "- Konfiguriere SSL/TLS-Zertifikate" -ForegroundColor $White
|
|
||||||
Write-Host "- Überwache Logs und Metriken" -ForegroundColor $White
|
|
||||||
Write-Host "- Führe regelmäßige Backups durch" -ForegroundColor $White
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Success "Installation erfolgreich abgeschlossen!"
|
|
@ -1,364 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 🏭 MYP Backend - Installations-Skript
|
|
||||||
# Installiert das Backend für Produktionsbetrieb oder Entwicklung
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Farbcodes für Ausgabe
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[0;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Funktion zur Ausgabe mit Zeitstempel
|
|
||||||
log() {
|
|
||||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
success_log() {
|
|
||||||
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] SUCCESS:${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
warning_log() {
|
|
||||||
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING:${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log() {
|
|
||||||
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
# Banner
|
|
||||||
echo "========================================"
|
|
||||||
echo "🏭 MYP Backend - Installation"
|
|
||||||
echo "========================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Arbeitsverzeichnis
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
log "Arbeitsverzeichnis: $SCRIPT_DIR"
|
|
||||||
|
|
||||||
# Installation-Modus bestimmen
|
|
||||||
INSTALL_MODE="development"
|
|
||||||
if [ "$1" = "--production" ]; then
|
|
||||||
INSTALL_MODE="production"
|
|
||||||
log "🚀 Produktions-Installation gewählt"
|
|
||||||
elif [ "$1" = "--development" ]; then
|
|
||||||
INSTALL_MODE="development"
|
|
||||||
log "🔧 Entwicklungs-Installation gewählt"
|
|
||||||
else
|
|
||||||
echo "Wählen Sie den Installationsmodus:"
|
|
||||||
echo "1) Entwicklung (empfohlen für lokale Tests)"
|
|
||||||
echo "2) Produktion (für Server-Deployment)"
|
|
||||||
read -p "Ihre Wahl (1/2): " choice
|
|
||||||
|
|
||||||
case $choice in
|
|
||||||
1) INSTALL_MODE="development" ;;
|
|
||||||
2) INSTALL_MODE="production" ;;
|
|
||||||
*) log "Verwende Standard-Entwicklungsmodus" && INSTALL_MODE="development" ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Installationsmodus: $INSTALL_MODE"
|
|
||||||
|
|
||||||
# Bereinige vorherige Installation
|
|
||||||
cleanup_existing_installation() {
|
|
||||||
log "🧹 Bereinige vorherige Installation..."
|
|
||||||
|
|
||||||
# Stoppe laufende Prozesse
|
|
||||||
if pgrep -f "flask run" > /dev/null; then
|
|
||||||
log "Stoppe laufende Flask-Prozesse..."
|
|
||||||
pkill -f "flask run" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if pgrep -f "gunicorn" > /dev/null; then
|
|
||||||
log "Stoppe laufende Gunicorn-Prozesse..."
|
|
||||||
pkill -f "gunicorn.*wsgi:application" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Entferne alte virtuelle Umgebung
|
|
||||||
if [ -d "venv" ]; then
|
|
||||||
log "Entferne alte virtuelle Umgebung..."
|
|
||||||
rm -rf venv
|
|
||||||
fi
|
|
||||||
|
|
||||||
success_log "Bereinigung abgeschlossen"
|
|
||||||
}
|
|
||||||
|
|
||||||
# System-Dependencies prüfen und installieren
|
|
||||||
install_system_dependencies() {
|
|
||||||
log "🔧 Prüfe System-Dependencies..."
|
|
||||||
|
|
||||||
# Betriebssystem erkennen
|
|
||||||
if [ -f /etc/os-release ]; then
|
|
||||||
. /etc/os-release
|
|
||||||
OS=$NAME
|
|
||||||
VER=$VERSION_ID
|
|
||||||
elif type lsb_release >/dev/null 2>&1; then
|
|
||||||
OS=$(lsb_release -si)
|
|
||||||
VER=$(lsb_release -sr)
|
|
||||||
else
|
|
||||||
OS=$(uname -s)
|
|
||||||
VER=$(uname -r)
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Erkanntes System: $OS $VER"
|
|
||||||
|
|
||||||
# Python 3 prüfen
|
|
||||||
if ! command -v python3 &> /dev/null; then
|
|
||||||
error_log "Python 3 ist nicht installiert!"
|
|
||||||
log "Installationsanleitung:"
|
|
||||||
if [[ "$OS" == *"Ubuntu"* ]] || [[ "$OS" == *"Debian"* ]]; then
|
|
||||||
log "sudo apt update && sudo apt install python3 python3-pip python3-venv"
|
|
||||||
elif [[ "$OS" == *"CentOS"* ]] || [[ "$OS" == *"Red Hat"* ]]; then
|
|
||||||
log "sudo yum install python3 python3-pip"
|
|
||||||
elif [[ "$OS" == *"Alpine"* ]]; then
|
|
||||||
log "sudo apk add python3 py3-pip"
|
|
||||||
else
|
|
||||||
log "Bitte installieren Sie Python 3 manuell für Ihr System"
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Python-Version prüfen
|
|
||||||
PYTHON_VERSION=$(python3 -c "import sys; print('.'.join(map(str, sys.version_info[:2])))")
|
|
||||||
log "Python-Version: $PYTHON_VERSION"
|
|
||||||
|
|
||||||
if python3 -c "import sys; exit(0 if sys.version_info >= (3, 8) else 1)"; then
|
|
||||||
success_log "Python-Version ist kompatibel (>= 3.8)"
|
|
||||||
else
|
|
||||||
error_log "Python-Version ist zu alt! Benötigt wird mindestens Python 3.8"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# pip prüfen
|
|
||||||
if ! command -v pip3 &> /dev/null; then
|
|
||||||
error_log "pip3 ist nicht installiert!"
|
|
||||||
log "Installiere pip3..."
|
|
||||||
if [[ "$OS" == *"Ubuntu"* ]] || [[ "$OS" == *"Debian"* ]]; then
|
|
||||||
sudo apt install python3-pip
|
|
||||||
else
|
|
||||||
error_log "Bitte installieren Sie pip3 manuell"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Weitere notwendige System-Pakete prüfen
|
|
||||||
if [[ "$OS" == *"Ubuntu"* ]] || [[ "$OS" == *"Debian"* ]]; then
|
|
||||||
log "Prüfe System-Pakete für Ubuntu/Debian..."
|
|
||||||
|
|
||||||
# Prüfe ob build-essential installiert ist (für Compilation von Python-Paketen)
|
|
||||||
if ! dpkg -l | grep -q build-essential; then
|
|
||||||
log "Installiere build-essential..."
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y build-essential python3-dev
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prüfe curl für Health-Checks
|
|
||||||
if ! command -v curl &> /dev/null; then
|
|
||||||
log "Installiere curl..."
|
|
||||||
sudo apt install -y curl
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
success_log "System-Dependencies sind verfügbar"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Python virtuelle Umgebung erstellen
|
|
||||||
create_virtual_environment() {
|
|
||||||
log "🐍 Erstelle Python virtuelle Umgebung..."
|
|
||||||
|
|
||||||
# Erstelle virtuelle Umgebung
|
|
||||||
python3 -m venv venv
|
|
||||||
|
|
||||||
# Aktiviere virtuelle Umgebung
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
# Upgrade pip in virtueller Umgebung
|
|
||||||
log "Aktualisiere pip..."
|
|
||||||
pip install --upgrade pip
|
|
||||||
|
|
||||||
success_log "Virtuelle Umgebung erstellt und aktiviert"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Python-Dependencies installieren
|
|
||||||
install_python_dependencies() {
|
|
||||||
log "📦 Installiere Python-Dependencies..."
|
|
||||||
|
|
||||||
# Aktiviere virtuelle Umgebung
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
# Installiere Requirements
|
|
||||||
if [ -f "requirements.txt" ]; then
|
|
||||||
log "Installiere aus requirements.txt..."
|
|
||||||
pip install -r requirements.txt
|
|
||||||
else
|
|
||||||
error_log "requirements.txt nicht gefunden!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Produktions-spezifische Dependencies
|
|
||||||
if [ "$INSTALL_MODE" = "production" ]; then
|
|
||||||
log "Installiere Produktions-Dependencies..."
|
|
||||||
pip install gunicorn supervisor
|
|
||||||
fi
|
|
||||||
|
|
||||||
success_log "Python-Dependencies installiert"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Konfiguration vorbereiten
|
|
||||||
prepare_configuration() {
|
|
||||||
log "⚙️ Bereite Konfiguration vor..."
|
|
||||||
|
|
||||||
# Erstelle notwendige Verzeichnisse
|
|
||||||
mkdir -p instance logs migrations/versions uploads
|
|
||||||
|
|
||||||
# Setze Standard-Umgebungsvariablen für die Installation
|
|
||||||
log "Setze Konfiguration (hardgecodet)..."
|
|
||||||
export FLASK_APP=app.py
|
|
||||||
export FLASK_ENV=$INSTALL_MODE
|
|
||||||
export SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F
|
|
||||||
export DATABASE_PATH=instance/myp.db
|
|
||||||
export LOG_LEVEL=INFO
|
|
||||||
export JOB_CHECK_INTERVAL=60
|
|
||||||
export SOCKET_CHECK_INTERVAL=120
|
|
||||||
export PRINTERS='{"Drucker 1": {"ip": "192.168.0.100"}, "Drucker 2": {"ip": "192.168.0.101"}, "Drucker 3": {"ip": "192.168.0.102"}, "Drucker 4": {"ip": "192.168.0.103"}, "Drucker 5": {"ip": "192.168.0.104"}, "Drucker 6": {"ip": "192.168.0.106"}}'
|
|
||||||
export TAPO_USERNAME=till.tomczak@mercedes-benz.com
|
|
||||||
export TAPO_PASSWORD=744563017196A
|
|
||||||
export HOST=0.0.0.0
|
|
||||||
export PORT=5000
|
|
||||||
export BACKEND_URL=http://localhost:5000
|
|
||||||
export UPLOAD_FOLDER=uploads
|
|
||||||
export MAX_CONTENT_LENGTH=16777216
|
|
||||||
export DEBUG=false
|
|
||||||
export TESTING=false
|
|
||||||
export DEVELOPMENT=false
|
|
||||||
|
|
||||||
success_log "Konfiguration vorbereitet (hardgecodet)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Datenbank initialisieren
|
|
||||||
initialize_database() {
|
|
||||||
log "🗄️ Initialisiere Datenbank..."
|
|
||||||
|
|
||||||
# Aktiviere virtuelle Umgebung
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
# Umgebungsvariablen sind bereits in prepare_configuration() gesetzt
|
|
||||||
log "Verwende hardgecodete Konfiguration..."
|
|
||||||
|
|
||||||
# Setze Flask-App
|
|
||||||
export FLASK_APP=app.py
|
|
||||||
export FLASK_ENV=$INSTALL_MODE
|
|
||||||
|
|
||||||
# Initialisiere Datenbank
|
|
||||||
python3 -c "
|
|
||||||
from app import create_app, init_db
|
|
||||||
app = create_app('$INSTALL_MODE')
|
|
||||||
with app.app_context():
|
|
||||||
init_db()
|
|
||||||
print('✅ Datenbank initialisiert')
|
|
||||||
"
|
|
||||||
|
|
||||||
success_log "Datenbank initialisiert"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Systemd-Service für Produktion erstellen
|
|
||||||
create_systemd_service() {
|
|
||||||
if [ "$INSTALL_MODE" = "production" ]; then
|
|
||||||
log "🔧 Erstelle systemd-Service für Produktion..."
|
|
||||||
|
|
||||||
SERVICE_FILE="/etc/systemd/system/myp-backend.service"
|
|
||||||
|
|
||||||
sudo tee $SERVICE_FILE > /dev/null << EOF
|
|
||||||
[Unit]
|
|
||||||
Description=MYP Backend Flask Application
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=exec
|
|
||||||
User=$USER
|
|
||||||
Group=$USER
|
|
||||||
WorkingDirectory=$SCRIPT_DIR
|
|
||||||
Environment=PATH=$SCRIPT_DIR/venv/bin
|
|
||||||
Environment=FLASK_APP=app.py
|
|
||||||
Environment=FLASK_ENV=production
|
|
||||||
Environment=SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F
|
|
||||||
Environment=DATABASE_PATH=instance/myp.db
|
|
||||||
Environment=LOG_LEVEL=INFO
|
|
||||||
Environment=JOB_CHECK_INTERVAL=60
|
|
||||||
Environment=SOCKET_CHECK_INTERVAL=120
|
|
||||||
Environment=PRINTERS={"Drucker 1": {"ip": "192.168.0.100"}, "Drucker 2": {"ip": "192.168.0.101"}, "Drucker 3": {"ip": "192.168.0.102"}, "Drucker 4": {"ip": "192.168.0.103"}, "Drucker 5": {"ip": "192.168.0.104"}, "Drucker 6": {"ip": "192.168.0.106"}}
|
|
||||||
Environment=TAPO_USERNAME=till.tomczak@mercedes-benz.com
|
|
||||||
Environment=TAPO_PASSWORD=744563017196A
|
|
||||||
Environment=HOST=0.0.0.0
|
|
||||||
Environment=PORT=5000
|
|
||||||
Environment=BACKEND_URL=http://localhost:5000
|
|
||||||
Environment=UPLOAD_FOLDER=uploads
|
|
||||||
Environment=MAX_CONTENT_LENGTH=16777216
|
|
||||||
Environment=DEBUG=false
|
|
||||||
Environment=TESTING=false
|
|
||||||
Environment=DEVELOPMENT=false
|
|
||||||
ExecStart=$SCRIPT_DIR/venv/bin/gunicorn --bind 0.0.0.0:5000 --workers 4 wsgi:application
|
|
||||||
ExecReload=/bin/kill -s HUP \$MAINPID
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable myp-backend
|
|
||||||
|
|
||||||
success_log "Systemd-Service erstellt: myp-backend.service"
|
|
||||||
log "Starten mit: sudo systemctl start myp-backend"
|
|
||||||
log "Status prüfen mit: sudo systemctl status myp-backend"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Hauptinstallation
|
|
||||||
main() {
|
|
||||||
cleanup_existing_installation
|
|
||||||
install_system_dependencies
|
|
||||||
create_virtual_environment
|
|
||||||
install_python_dependencies
|
|
||||||
prepare_configuration
|
|
||||||
initialize_database
|
|
||||||
create_systemd_service
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "========================================"
|
|
||||||
success_log "🎉 Installation erfolgreich abgeschlossen!"
|
|
||||||
echo "========================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
log "Nächste Schritte:"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ "$INSTALL_MODE" = "production" ]; then
|
|
||||||
echo "📋 Produktionsbetrieb:"
|
|
||||||
echo " 1. Service starten: sudo systemctl start myp-backend"
|
|
||||||
echo " 2. Service prüfen: sudo systemctl status myp-backend"
|
|
||||||
echo " 3. Oder manuell: ./start-production.sh"
|
|
||||||
echo " 4. Logs anzeigen: sudo journalctl -u myp-backend -f"
|
|
||||||
else
|
|
||||||
echo "🔧 Entwicklungsbetrieb:"
|
|
||||||
echo " 1. Server starten: ./start-backend-server.sh"
|
|
||||||
echo " 2. Development-Server: ./start-backend-server.sh --development"
|
|
||||||
echo " 3. Debug-Modus: ./start-debug-server.sh"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📡 Backend wird verfügbar sein unter:"
|
|
||||||
echo " - API: http://localhost:5000"
|
|
||||||
echo " - Health-Check: http://localhost:5000/health"
|
|
||||||
echo " - Test: http://localhost:5000/api/test"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Installation starten
|
|
||||||
main "$@"
|
|
11
backend/install/kiosk.service
Normal file
11
backend/install/kiosk.service
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Chromium Kiosk
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=forking
|
||||||
|
ExecStart=/home/pi/kiosk.sh
|
||||||
|
Restart=on-abort
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=xsession.target
|
19
backend/install/kiosk.sh
Executable file
19
backend/install/kiosk.sh
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Bildschirm-Blanking verhindern
|
||||||
|
xset s off
|
||||||
|
xset s noblank
|
||||||
|
xset -dpms
|
||||||
|
|
||||||
|
# Mauszeiger ausblenden
|
||||||
|
unclutter -idle 0.5 -root &
|
||||||
|
|
||||||
|
# Chromium-Crash-Dialoge unterdrücken
|
||||||
|
sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' \
|
||||||
|
"$HOME/.config/chromium/Default/Preferences" 2>/dev/null || true
|
||||||
|
sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' \
|
||||||
|
"$HOME/.config/chromium/Default/Preferences" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Browser starten
|
||||||
|
chromium-browser --kiosk --noerrdialogs --disable-infobars \
|
||||||
|
--window-position=0,0 --app=http://localhost:5000/ &
|
14
backend/install/myp.service
Normal file
14
backend/install/myp.service
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MYP Flask Backend
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=pi
|
||||||
|
WorkingDirectory=/opt/myp
|
||||||
|
ExecStart=/opt/myp/.venv/bin/python /opt/myp/app.py
|
||||||
|
Restart=always
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
6
backend/install/requirements.txt
Normal file
6
backend/install/requirements.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
flask==2.3.3
|
||||||
|
flask-login==0.6.2
|
||||||
|
sqlalchemy==2.0.20
|
||||||
|
PyP100==0.1.1
|
||||||
|
bcrypt==4.0.1
|
||||||
|
gunicorn==21.2.0
|
90
backend/install/setup.sh
Executable file
90
backend/install/setup.sh
Executable file
@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# MYP Kiosk-Modus Einrichtungsskript
|
||||||
|
|
||||||
|
# Fehlerabbruch aktivieren
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "===== MYP Kiosk-Modus Einrichtungsskript ====="
|
||||||
|
echo "Dieses Skript richtet MYP für den automatischen Start im Kiosk-Modus ein."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Benötigte Pakete installieren
|
||||||
|
echo "1. Installiere benötigte Pakete..."
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y python3 python3-pip python3-venv chromium-browser \
|
||||||
|
unclutter xdotool xscreensaver git
|
||||||
|
|
||||||
|
# 2. Verzeichnis für MYP erstellen und Projekt kopieren
|
||||||
|
echo "2. Kopiere MYP nach /opt/myp..."
|
||||||
|
sudo mkdir -p /opt/myp
|
||||||
|
sudo chown $USER:$USER /opt/myp
|
||||||
|
|
||||||
|
# Aktuelle Verzeichnisstruktur ermitteln
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
echo "Kopiere Dateien von $SCRIPT_DIR nach /opt/myp..."
|
||||||
|
cp -r "$SCRIPT_DIR"/* /opt/myp/
|
||||||
|
|
||||||
|
# 3. Python-Umgebung und Abhängigkeiten einrichten
|
||||||
|
echo "3. Richte Python-Umgebung ein..."
|
||||||
|
cd /opt/myp
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 4. Systemd-Dienst für Flask-Backend einrichten
|
||||||
|
echo "4. Richte Flask-Backend-Dienst ein..."
|
||||||
|
sudo cp /opt/myp/myp.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable myp.service
|
||||||
|
sudo systemctl start myp.service
|
||||||
|
|
||||||
|
# 5. Kiosk-Script einrichten
|
||||||
|
echo "5. Richte Kiosk-Script ein..."
|
||||||
|
cp /opt/myp/kiosk.sh /home/pi/
|
||||||
|
chmod +x /home/pi/kiosk.sh
|
||||||
|
|
||||||
|
# 6. Systemd-User-Dienst für Kiosk einrichten
|
||||||
|
echo "6. Richte Kiosk-Dienst ein..."
|
||||||
|
mkdir -p /home/pi/.config/systemd/user
|
||||||
|
cp /opt/myp/kiosk.service /home/pi/.config/systemd/user/
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable kiosk.service
|
||||||
|
|
||||||
|
# 7. Linger für den pi-Benutzer aktivieren
|
||||||
|
echo "7. Aktiviere User-Linger für pi-Benutzer..."
|
||||||
|
sudo loginctl enable-linger pi
|
||||||
|
|
||||||
|
# 8. Watchdog-Script einrichten
|
||||||
|
echo "8. Richte Watchdog-Script ein..."
|
||||||
|
cp /opt/myp/watchdog.sh /home/pi/
|
||||||
|
chmod +x /home/pi/watchdog.sh
|
||||||
|
|
||||||
|
# 9. Cron-Job für Watchdog einrichten
|
||||||
|
echo "9. Richte Cron-Job für Watchdog ein..."
|
||||||
|
(crontab -l 2>/dev/null || echo "") | grep -v "watchdog.sh" | { cat; echo "*/5 * * * * /home/pi/watchdog.sh > /dev/null 2>&1"; } | crontab -
|
||||||
|
|
||||||
|
# 10. Automatischen Login einrichten
|
||||||
|
echo "10. Automatischer Login wird manuell über raspi-config eingerichtet"
|
||||||
|
echo " Führe 'sudo raspi-config' aus und wähle:"
|
||||||
|
echo " 1 System Options → S5 Boot/Auto Login → B4 Desktop Autologin"
|
||||||
|
|
||||||
|
# 11. Bildschirm nie ausschalten
|
||||||
|
echo "11. Deaktiviere Bildschirmschoner..."
|
||||||
|
sudo sed -i 's/#BLANK_TIME=.*/BLANK_TIME=0/' /etc/xdg/lxsession/LXDE-pi/autostart
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "===== Installation abgeschlossen ====="
|
||||||
|
echo "Um die Einrichtung zu vervollständigen, führe 'sudo raspi-config' aus"
|
||||||
|
echo "und aktiviere den automatischen Login: "
|
||||||
|
echo "1 System Options → S5 Boot/Auto Login → B4 Desktop Autologin"
|
||||||
|
echo ""
|
||||||
|
echo "Nach einem Neustart sollte der Raspberry Pi automatisch:"
|
||||||
|
echo "1. Die MYP-Flask-Anwendung starten"
|
||||||
|
echo "2. Den Chromium-Browser im Kiosk-Modus öffnen"
|
||||||
|
echo ""
|
||||||
|
echo "MYP ist erreichbar unter: http://localhost:5000/"
|
||||||
|
echo ""
|
||||||
|
echo "Ein Watchdog-Script überwacht alle 5 Minuten, ob Chromium und der MYP-Dienst"
|
||||||
|
echo "noch laufen und startet sie bei Bedarf neu."
|
||||||
|
echo ""
|
||||||
|
echo "Starte den Raspberry Pi neu mit 'sudo reboot'"
|
40
backend/install/watchdog.sh
Executable file
40
backend/install/watchdog.sh
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# MYP Watchdog für Chromium Browser
|
||||||
|
# Empfohlene Ausführung über crontab: */5 * * * * /home/pi/watchdog.sh > /dev/null 2>&1
|
||||||
|
|
||||||
|
# Funktion zum Loggen von Nachrichten
|
||||||
|
log() {
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> /home/pi/myp-watchdog.log
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prüfen, ob Chromium läuft
|
||||||
|
if ! pgrep -x "chromium-browse" > /dev/null; then
|
||||||
|
log "Chromium nicht gefunden - starte neu"
|
||||||
|
|
||||||
|
# Alle eventuell noch vorhandenen Chromium-Prozesse beenden
|
||||||
|
pkill -f chromium || true
|
||||||
|
|
||||||
|
# Warten bis alle Prozesse beendet sind
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Kiosk-Script neu starten
|
||||||
|
/home/pi/kiosk.sh
|
||||||
|
|
||||||
|
log "Chromium neugestartet"
|
||||||
|
else
|
||||||
|
# Optional: Nur für Debug-Zwecke
|
||||||
|
# log "Chromium läuft normal"
|
||||||
|
:
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob MYP Flask-Dienst läuft
|
||||||
|
if ! systemctl is-active --quiet myp.service; then
|
||||||
|
log "MYP Flask-Dienst ist nicht aktiv - starte neu"
|
||||||
|
|
||||||
|
# Dienst neustarten
|
||||||
|
sudo systemctl restart myp.service
|
||||||
|
|
||||||
|
log "MYP Flask-Dienst neugestartet"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
@ -1,89 +0,0 @@
|
|||||||
root@raspberrypi:/home/user/Projektarbeit-MYP/backend# python3 app.py
|
|
||||||
[2025-03-24 09:38:15,229] INFO in app: MYP Backend starting up
|
|
||||||
[2025-03-24 09:38:15,338] INFO in app: Initialisiere Drucker aus Umgebungsvariablen
|
|
||||||
[2025-03-24 09:38:15,353] INFO in app: Neuer Drucker angelegt: Printer 1 mit IP 192.168.0.100
|
|
||||||
[2025-03-24 09:38:16,197] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.100: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:16,197] INFO in app: Neue Steckdose mit IP 192.168.0.100 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:16,209] INFO in app: Neuer Drucker angelegt: Printer 2 mit IP 192.168.0.101
|
|
||||||
[2025-03-24 09:38:16,521] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.101: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:16,522] INFO in app: Neue Steckdose mit IP 192.168.0.101 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:16,536] INFO in app: Neuer Drucker angelegt: Printer 3 mit IP 192.168.0.102
|
|
||||||
[2025-03-24 09:38:17,082] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.102: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:17,083] INFO in app: Neue Steckdose mit IP 192.168.0.102 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:17,096] INFO in app: Neuer Drucker angelegt: Printer 4 mit IP 192.168.0.103
|
|
||||||
[2025-03-24 09:38:18,248] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.103: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:18,249] INFO in app: Neue Steckdose mit IP 192.168.0.103 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:18,263] INFO in app: Neuer Drucker angelegt: Printer 5 mit IP 192.168.0.104
|
|
||||||
[2025-03-24 09:38:18,635] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.104: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:18,636] INFO in app: Neue Steckdose mit IP 192.168.0.104 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:18,650] INFO in app: Neuer Drucker angelegt: Printer 6 mit IP 192.168.0.106
|
|
||||||
[2025-03-24 09:38:21,004] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.106: HTTPConnectionPool(host='192.168.0.106', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x7fb0b1dd90>, 'Connection to 192.168.0.106 timed out. (connect timeout=2)'))
|
|
||||||
[2025-03-24 09:38:21,006] INFO in app: Neue Steckdose mit IP 192.168.0.106 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:21,007] INFO in app: Starte Hintergrund-Thread für Job-Überprüfung und Steckdosen-Monitoring
|
|
||||||
[2025-03-24 09:38:21,008] INFO in app: Hintergrund-Thread für Job-Überprüfung gestartet
|
|
||||||
[2025-03-24 09:38:21,014] INFO in app: 0 abgelaufene Jobs überprüft, 0 Steckdosen aktualisiert.
|
|
||||||
* Serving Flask app 'app'
|
|
||||||
* Debug mode: on
|
|
||||||
[2025-03-24 09:38:21,023] INFO in app: Überprüfe Verbindungsstatus von 6 Steckdosen
|
|
||||||
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
|
||||||
* Running on all addresses (0.0.0.0)
|
|
||||||
* Running on http://127.0.0.1:5000
|
|
||||||
* Running on http://192.168.0.105:5000
|
|
||||||
Press CTRL+C to quit
|
|
||||||
* Restarting with stat
|
|
||||||
[2025-03-24 09:38:21,810] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.100: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:21,826] INFO in app: Verbindungsstatus für Steckdose 80c65076-acdb-4448-ac6e-05a44b35f5b2 geändert: offline
|
|
||||||
[2025-03-24 09:38:21,845] WARNING in app: Steckdose Printer 1 (192.168.0.100) ist nicht erreichbar
|
|
||||||
[2025-03-24 09:38:21,913] INFO in app: MYP Backend starting up
|
|
||||||
[2025-03-24 09:38:21,968] INFO in app: Initialisiere Drucker aus Umgebungsvariablen
|
|
||||||
[2025-03-24 09:38:21,969] INFO in app: Drucker mit IP 192.168.0.100 existiert bereits in der Datenbank
|
|
||||||
[2025-03-24 09:38:22,109] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.101: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:22,120] INFO in app: Verbindungsstatus für Steckdose 19e70cd5-5fdb-439b-80e3-807015c7cb15 geändert: offline
|
|
||||||
[2025-03-24 09:38:22,134] WARNING in app: Steckdose Printer 2 (192.168.0.101) ist nicht erreichbar
|
|
||||||
[2025-03-24 09:38:22,666] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.100: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:22,667] INFO in app: Steckdose mit IP 192.168.0.100 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:22,668] INFO in app: Drucker mit IP 192.168.0.101 existiert bereits in der Datenbank
|
|
||||||
[2025-03-24 09:38:22,806] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.102: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:22,819] INFO in app: Verbindungsstatus für Steckdose 7cdc29a8-3593-4666-8419-070914c6d6c5 geändert: offline
|
|
||||||
[2025-03-24 09:38:22,831] WARNING in app: Steckdose Printer 3 (192.168.0.102) ist nicht erreichbar
|
|
||||||
[2025-03-24 09:38:23,222] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.101: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:23,223] INFO in app: Steckdose mit IP 192.168.0.101 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:23,223] INFO in app: Drucker mit IP 192.168.0.102 existiert bereits in der Datenbank
|
|
||||||
[2025-03-24 09:38:23,228] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.103: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:23,243] INFO in app: Verbindungsstatus für Steckdose 69be8092-0eea-4797-a940-51bdec244cf7 geändert: offline
|
|
||||||
[2025-03-24 09:38:23,256] WARNING in app: Steckdose Printer 4 (192.168.0.103) ist nicht erreichbar
|
|
||||||
[2025-03-24 09:38:23,458] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.104: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:23,476] INFO in app: Verbindungsstatus für Steckdose 90caa30e-adaf-44ec-a680-6beea72a570a geändert: offline
|
|
||||||
[2025-03-24 09:38:23,489] WARNING in app: Steckdose Printer 5 (192.168.0.104) ist nicht erreichbar
|
|
||||||
[2025-03-24 09:38:23,492] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.102: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:23,493] INFO in app: Steckdose mit IP 192.168.0.102 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:23,493] INFO in app: Drucker mit IP 192.168.0.103 existiert bereits in der Datenbank
|
|
||||||
[2025-03-24 09:38:24,058] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.103: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:24,058] INFO in app: Steckdose mit IP 192.168.0.103 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:24,059] INFO in app: Drucker mit IP 192.168.0.104 existiert bereits in der Datenbank
|
|
||||||
[2025-03-24 09:38:24,610] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.104: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:24,611] INFO in app: Steckdose mit IP 192.168.0.104 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:24,612] INFO in app: Drucker mit IP 192.168.0.106 existiert bereits in der Datenbank
|
|
||||||
[2025-03-24 09:38:26,344] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.106: HTTPConnectionPool(host='192.168.0.106', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x7fafc91790>, 'Connection to 192.168.0.106 timed out. (connect timeout=2)'))
|
|
||||||
[2025-03-24 09:38:26,357] INFO in app: Verbindungsstatus für Steckdose 2b6b9831-e4c1-4f60-8107-69cbc8b58e2c geändert: offline
|
|
||||||
[2025-03-24 09:38:26,370] WARNING in app: Steckdose Printer 6 (192.168.0.106) ist nicht erreichbar
|
|
||||||
[2025-03-24 09:38:26,371] INFO in app: Verbindungsüberprüfung abgeschlossen: 0 online, 6 offline, 0 übersprungen
|
|
||||||
[2025-03-24 09:38:26,371] INFO in app: Nächste Socket-Überprüfung in 120 Sekunden
|
|
||||||
[2025-03-24 09:38:26,775] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.106: HTTPConnectionPool(host='192.168.0.106', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x7f8214df50>, 'Connection to 192.168.0.106 timed out. (connect timeout=2)'))
|
|
||||||
[2025-03-24 09:38:26,776] INFO in app: Steckdose mit IP 192.168.0.106 wurde beim Start ausgeschaltet
|
|
||||||
[2025-03-24 09:38:26,776] INFO in app: Starte Hintergrund-Thread für Job-Überprüfung und Steckdosen-Monitoring
|
|
||||||
[2025-03-24 09:38:26,777] INFO in app: Hintergrund-Thread für Job-Überprüfung gestartet
|
|
||||||
[2025-03-24 09:38:26,780] INFO in app: 0 abgelaufene Jobs überprüft, 0 Steckdosen aktualisiert.
|
|
||||||
[2025-03-24 09:38:26,784] INFO in app: Überprüfe Verbindungsstatus von 6 Steckdosen
|
|
||||||
* Debugger is active!
|
|
||||||
* Debugger PIN: 101-484-383
|
|
||||||
[2025-03-24 09:38:27,279] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.100: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:27,280] WARNING in app: Steckdose Printer 1 (192.168.0.100) ist nicht erreichbar
|
|
||||||
[2025-03-24 09:38:27,719] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.101: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:27,720] WARNING in app: Steckdose Printer 2 (192.168.0.101) ist nicht erreichbar
|
|
||||||
[2025-03-24 09:38:28,073] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.102: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:28,074] WARNING in app: Steckdose Printer 3 (192.168.0.102) ist nicht erreichbar
|
|
||||||
[2025-03-24 09:38:28,887] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.103: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:28,887] WARNING in app: Steckdose Printer 4 (192.168.0.103) ist nicht erreichbar
|
|
||||||
[2025-03-24 09:38:29,312] ERROR in app: Fehler bei der Anmeldung an P100-Gerät 192.168.0.104: Expecting value: line 1 column 1 (char 0)
|
|
||||||
[2025-03-24 09:38:29,312] WARNING in app: Steckdose Printer 5 (192.168.0.104) ist nicht erreichbar
|
|
@ -1 +0,0 @@
|
|||||||
Single-database configuration for Flask.
|
|
@ -1,50 +0,0 @@
|
|||||||
# A generic, single database configuration.
|
|
||||||
|
|
||||||
[alembic]
|
|
||||||
# template used to generate migration files
|
|
||||||
# file_template = %%(rev)s_%%(slug)s
|
|
||||||
|
|
||||||
# set to 'true' to run the environment during
|
|
||||||
# the 'revision' command, regardless of autogenerate
|
|
||||||
# revision_environment = false
|
|
||||||
|
|
||||||
|
|
||||||
# Logging configuration
|
|
||||||
[loggers]
|
|
||||||
keys = root,sqlalchemy,alembic,flask_migrate
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys = console
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys = generic
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
level = WARN
|
|
||||||
handlers = console
|
|
||||||
qualname =
|
|
||||||
|
|
||||||
[logger_sqlalchemy]
|
|
||||||
level = WARN
|
|
||||||
handlers =
|
|
||||||
qualname = sqlalchemy.engine
|
|
||||||
|
|
||||||
[logger_alembic]
|
|
||||||
level = INFO
|
|
||||||
handlers =
|
|
||||||
qualname = alembic
|
|
||||||
|
|
||||||
[logger_flask_migrate]
|
|
||||||
level = INFO
|
|
||||||
handlers =
|
|
||||||
qualname = flask_migrate
|
|
||||||
|
|
||||||
[handler_console]
|
|
||||||
class = StreamHandler
|
|
||||||
args = (sys.stderr,)
|
|
||||||
level = NOTSET
|
|
||||||
formatter = generic
|
|
||||||
|
|
||||||
[formatter_generic]
|
|
||||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
||||||
datefmt = %H:%M:%S
|
|
@ -1,113 +0,0 @@
|
|||||||
import logging
|
|
||||||
from logging.config import fileConfig
|
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
from alembic import context
|
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
|
||||||
# access to the values within the .ini file in use.
|
|
||||||
config = context.config
|
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
|
||||||
# This line sets up loggers basically.
|
|
||||||
fileConfig(config.config_file_name)
|
|
||||||
logger = logging.getLogger('alembic.env')
|
|
||||||
|
|
||||||
|
|
||||||
def get_engine():
|
|
||||||
try:
|
|
||||||
# this works with Flask-SQLAlchemy<3 and Alchemical
|
|
||||||
return current_app.extensions['migrate'].db.get_engine()
|
|
||||||
except (TypeError, AttributeError):
|
|
||||||
# this works with Flask-SQLAlchemy>=3
|
|
||||||
return current_app.extensions['migrate'].db.engine
|
|
||||||
|
|
||||||
|
|
||||||
def get_engine_url():
|
|
||||||
try:
|
|
||||||
return get_engine().url.render_as_string(hide_password=False).replace(
|
|
||||||
'%', '%%')
|
|
||||||
except AttributeError:
|
|
||||||
return str(get_engine().url).replace('%', '%%')
|
|
||||||
|
|
||||||
|
|
||||||
# add your model's MetaData object here
|
|
||||||
# for 'autogenerate' support
|
|
||||||
# from myapp import mymodel
|
|
||||||
# target_metadata = mymodel.Base.metadata
|
|
||||||
config.set_main_option('sqlalchemy.url', get_engine_url())
|
|
||||||
target_db = current_app.extensions['migrate'].db
|
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
|
||||||
# can be acquired:
|
|
||||||
# my_important_option = config.get_main_option("my_important_option")
|
|
||||||
# ... etc.
|
|
||||||
|
|
||||||
|
|
||||||
def get_metadata():
|
|
||||||
if hasattr(target_db, 'metadatas'):
|
|
||||||
return target_db.metadatas[None]
|
|
||||||
return target_db.metadata
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline():
|
|
||||||
"""Run migrations in 'offline' mode.
|
|
||||||
|
|
||||||
This configures the context with just a URL
|
|
||||||
and not an Engine, though an Engine is acceptable
|
|
||||||
here as well. By skipping the Engine creation
|
|
||||||
we don't even need a DBAPI to be available.
|
|
||||||
|
|
||||||
Calls to context.execute() here emit the given string to the
|
|
||||||
script output.
|
|
||||||
|
|
||||||
"""
|
|
||||||
url = config.get_main_option("sqlalchemy.url")
|
|
||||||
context.configure(
|
|
||||||
url=url, target_metadata=get_metadata(), literal_binds=True
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online():
|
|
||||||
"""Run migrations in 'online' mode.
|
|
||||||
|
|
||||||
In this scenario we need to create an Engine
|
|
||||||
and associate a connection with the context.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# this callback is used to prevent an auto-migration from being generated
|
|
||||||
# when there are no changes to the schema
|
|
||||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
|
||||||
def process_revision_directives(context, revision, directives):
|
|
||||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
|
||||||
script = directives[0]
|
|
||||||
if script.upgrade_ops.is_empty():
|
|
||||||
directives[:] = []
|
|
||||||
logger.info('No changes in schema detected.')
|
|
||||||
|
|
||||||
conf_args = current_app.extensions['migrate'].configure_args
|
|
||||||
if conf_args.get("process_revision_directives") is None:
|
|
||||||
conf_args["process_revision_directives"] = process_revision_directives
|
|
||||||
|
|
||||||
connectable = get_engine()
|
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
|
||||||
context.configure(
|
|
||||||
connection=connection,
|
|
||||||
target_metadata=get_metadata(),
|
|
||||||
**conf_args
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
|
||||||
run_migrations_offline()
|
|
||||||
else:
|
|
||||||
run_migrations_online()
|
|
@ -1,24 +0,0 @@
|
|||||||
"""${message}
|
|
||||||
|
|
||||||
Revision ID: ${up_revision}
|
|
||||||
Revises: ${down_revision | comma,n}
|
|
||||||
Create Date: ${create_date}
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
${imports if imports else ""}
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = ${repr(up_revision)}
|
|
||||||
down_revision = ${repr(down_revision)}
|
|
||||||
branch_labels = ${repr(branch_labels)}
|
|
||||||
depends_on = ${repr(depends_on)}
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
${upgrades if upgrades else "pass"}
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
${downgrades if downgrades else "pass"}
|
|
@ -1,42 +0,0 @@
|
|||||||
"""Add waiting_approval column to job table
|
|
||||||
|
|
||||||
Revision ID: add_waiting_approval
|
|
||||||
Revises: af3faaa3844c
|
|
||||||
Create Date: 2025-03-12 14:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'add_waiting_approval'
|
|
||||||
down_revision = 'af3faaa3844c'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# Füge die neue Spalte waiting_approval zur job-Tabelle hinzu
|
|
||||||
with op.batch_alter_table('job', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('waiting_approval', sa.Integer(), server_default='0', nullable=False))
|
|
||||||
|
|
||||||
# SQLite-kompatible Migration für die print_job-Tabelle, falls diese existiert
|
|
||||||
try:
|
|
||||||
with op.batch_alter_table('print_job', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('waiting_approval', sa.Boolean(), server_default='0', nullable=False))
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Migration für print_job-Tabelle übersprungen: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# Entferne die waiting_approval-Spalte aus der job-Tabelle
|
|
||||||
with op.batch_alter_table('job', schema=None) as batch_op:
|
|
||||||
batch_op.drop_column('waiting_approval')
|
|
||||||
|
|
||||||
# SQLite-kompatible Migration für die print_job-Tabelle, falls diese existiert
|
|
||||||
try:
|
|
||||||
with op.batch_alter_table('print_job', schema=None) as batch_op:
|
|
||||||
batch_op.drop_column('waiting_approval')
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Downgrade für print_job-Tabelle übersprungen: {e}")
|
|
@ -1,81 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: af3faaa3844c
|
|
||||||
Revises:
|
|
||||||
Create Date: 2025-03-11 11:16:04.961964
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'af3faaa3844c'
|
|
||||||
down_revision = None
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('printer',
|
|
||||||
sa.Column('id', sa.String(length=36), nullable=False),
|
|
||||||
sa.Column('name', sa.String(length=64), nullable=False),
|
|
||||||
sa.Column('description', sa.Text(), nullable=False),
|
|
||||||
sa.Column('status', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('ip_address', sa.String(length=15), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
with op.batch_alter_table('printer', schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f('ix_printer_name'), ['name'], unique=False)
|
|
||||||
|
|
||||||
op.create_table('user',
|
|
||||||
sa.Column('id', sa.String(length=36), nullable=False),
|
|
||||||
sa.Column('username', sa.String(length=64), nullable=True),
|
|
||||||
sa.Column('password_hash', sa.String(length=128), nullable=True),
|
|
||||||
sa.Column('display_name', sa.String(length=100), nullable=True),
|
|
||||||
sa.Column('email', sa.String(length=120), nullable=True),
|
|
||||||
sa.Column('role', sa.String(length=20), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True)
|
|
||||||
batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
|
|
||||||
|
|
||||||
op.create_table('print_job',
|
|
||||||
sa.Column('id', sa.String(length=36), nullable=False),
|
|
||||||
sa.Column('printer_id', sa.String(length=36), nullable=False),
|
|
||||||
sa.Column('user_id', sa.String(length=36), nullable=False),
|
|
||||||
sa.Column('start_at', sa.DateTime(), nullable=True),
|
|
||||||
sa.Column('duration_in_minutes', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('comments', sa.Text(), nullable=True),
|
|
||||||
sa.Column('aborted', sa.Boolean(), nullable=True),
|
|
||||||
sa.Column('abort_reason', sa.Text(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(['printer_id'], ['printer.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_table('session',
|
|
||||||
sa.Column('id', sa.String(length=36), nullable=False),
|
|
||||||
sa.Column('user_id', sa.String(length=36), nullable=False),
|
|
||||||
sa.Column('expires_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table('session')
|
|
||||||
op.drop_table('print_job')
|
|
||||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f('ix_user_username'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_user_email'))
|
|
||||||
|
|
||||||
op.drop_table('user')
|
|
||||||
with op.batch_alter_table('printer', schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f('ix_printer_name'))
|
|
||||||
|
|
||||||
op.drop_table('printer')
|
|
||||||
# ### end Alembic commands ###
|
|
@ -1,330 +0,0 @@
|
|||||||
"""
|
|
||||||
Monitoring und Health Check Module für die MYP Flask-Anwendung.
|
|
||||||
Bietet Endpunkte für Systemüberwachung und Performance-Metriken.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, current_app
|
|
||||||
import psutil
|
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
import datetime
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
# Blueprint für Monitoring-Endpunkte
|
|
||||||
monitoring_bp = Blueprint('monitoring', __name__, url_prefix='/monitoring')
|
|
||||||
|
|
||||||
# Metriken-Speicher
|
|
||||||
metrics = {
|
|
||||||
'requests_total': defaultdict(int),
|
|
||||||
'request_duration': defaultdict(list),
|
|
||||||
'database_queries': 0,
|
|
||||||
'active_jobs': 0,
|
|
||||||
'error_count': defaultdict(int),
|
|
||||||
'startup_time': datetime.datetime.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
class HealthCheck:
|
|
||||||
"""Klasse für System-Health-Checks."""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_database():
|
|
||||||
"""
|
|
||||||
Überprüft die Datenbankverbindung.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Status und Details der Datenbankverbindung
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
db_path = current_app.config.get('DATABASE', 'instance/myp.db')
|
|
||||||
|
|
||||||
# Bei In-Memory-DB für Tests
|
|
||||||
if db_path == ':memory:':
|
|
||||||
return {'status': 'healthy', 'message': 'In-Memory-Datenbank aktiv'}
|
|
||||||
|
|
||||||
# Datei-basierte Datenbank prüfen
|
|
||||||
if not os.path.exists(db_path):
|
|
||||||
return {'status': 'unhealthy', 'message': 'Datenbankdatei nicht gefunden'}
|
|
||||||
|
|
||||||
# Verbindung testen
|
|
||||||
conn = sqlite3.connect(db_path, timeout=5)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute('SELECT 1')
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
# Dateigröße ermitteln
|
|
||||||
db_size = os.path.getsize(db_path)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'status': 'healthy',
|
|
||||||
'message': 'Datenbankverbindung erfolgreich',
|
|
||||||
'database_path': db_path,
|
|
||||||
'database_size_bytes': db_size
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
'status': 'unhealthy',
|
|
||||||
'message': f'Datenbankfehler: {str(e)}'
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_disk_space():
|
|
||||||
"""
|
|
||||||
Überprüft den verfügbaren Festplattenspeicher.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Status und Details des Festplattenspeichers
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
disk_usage = psutil.disk_usage('.')
|
|
||||||
free_gb = disk_usage.free / (1024**3)
|
|
||||||
total_gb = disk_usage.total / (1024**3)
|
|
||||||
used_percent = (disk_usage.used / disk_usage.total) * 100
|
|
||||||
|
|
||||||
status = 'healthy'
|
|
||||||
if used_percent > 90:
|
|
||||||
status = 'critical'
|
|
||||||
elif used_percent > 80:
|
|
||||||
status = 'warning'
|
|
||||||
|
|
||||||
return {
|
|
||||||
'status': status,
|
|
||||||
'free_gb': round(free_gb, 2),
|
|
||||||
'total_gb': round(total_gb, 2),
|
|
||||||
'used_percent': round(used_percent, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
'status': 'unhealthy',
|
|
||||||
'message': f'Festplattenfehler: {str(e)}'
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_memory():
|
|
||||||
"""
|
|
||||||
Überprüft die Speichernutzung.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Status und Details der Speichernutzung
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
memory = psutil.virtual_memory()
|
|
||||||
|
|
||||||
status = 'healthy'
|
|
||||||
if memory.percent > 90:
|
|
||||||
status = 'critical'
|
|
||||||
elif memory.percent > 80:
|
|
||||||
status = 'warning'
|
|
||||||
|
|
||||||
return {
|
|
||||||
'status': status,
|
|
||||||
'total_gb': round(memory.total / (1024**3), 2),
|
|
||||||
'available_gb': round(memory.available / (1024**3), 2),
|
|
||||||
'used_percent': round(memory.percent, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
'status': 'unhealthy',
|
|
||||||
'message': f'Speicherfehler: {str(e)}'
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_background_threads():
|
|
||||||
"""
|
|
||||||
Überprüft die Hintergrund-Threads.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Status der Hintergrund-Threads
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
active_threads = [t.name for t in threading.enumerate() if t.is_alive()]
|
|
||||||
job_checker_running = any('job_checker' in name for name in active_threads)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'status': 'healthy' if job_checker_running else 'warning',
|
|
||||||
'job_checker_running': job_checker_running,
|
|
||||||
'active_threads': active_threads,
|
|
||||||
'thread_count': len(active_threads)
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
'status': 'unhealthy',
|
|
||||||
'message': f'Thread-Fehler: {str(e)}'
|
|
||||||
}
|
|
||||||
|
|
||||||
@monitoring_bp.route('/health')
|
|
||||||
def health_check():
|
|
||||||
"""
|
|
||||||
Umfassender Health Check aller Systemkomponenten.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON: Status aller Systemkomponenten
|
|
||||||
"""
|
|
||||||
checks = {
|
|
||||||
'database': HealthCheck.check_database(),
|
|
||||||
'disk_space': HealthCheck.check_disk_space(),
|
|
||||||
'memory': HealthCheck.check_memory(),
|
|
||||||
'background_threads': HealthCheck.check_background_threads()
|
|
||||||
}
|
|
||||||
|
|
||||||
# Gesamtstatus bestimmen
|
|
||||||
overall_status = 'healthy'
|
|
||||||
for check in checks.values():
|
|
||||||
if check['status'] == 'unhealthy':
|
|
||||||
overall_status = 'unhealthy'
|
|
||||||
break
|
|
||||||
elif check['status'] in ['warning', 'critical']:
|
|
||||||
overall_status = 'degraded'
|
|
||||||
|
|
||||||
response = {
|
|
||||||
'status': overall_status,
|
|
||||||
'timestamp': datetime.datetime.now().isoformat(),
|
|
||||||
'checks': checks
|
|
||||||
}
|
|
||||||
|
|
||||||
status_code = 200 if overall_status == 'healthy' else 503
|
|
||||||
return jsonify(response), status_code
|
|
||||||
|
|
||||||
@monitoring_bp.route('/health/simple')
|
|
||||||
def simple_health_check():
|
|
||||||
"""
|
|
||||||
Einfacher Health Check für Load Balancer.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON: Einfacher Status
|
|
||||||
"""
|
|
||||||
return jsonify({'status': 'ok', 'timestamp': datetime.datetime.now().isoformat()})
|
|
||||||
|
|
||||||
@monitoring_bp.route('/metrics')
|
|
||||||
def get_metrics():
|
|
||||||
"""
|
|
||||||
Sammelt und gibt Performance-Metriken zurück.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON: System- und Anwendungsmetriken
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# System-Metriken
|
|
||||||
cpu_percent = psutil.cpu_percent(interval=1)
|
|
||||||
memory = psutil.virtual_memory()
|
|
||||||
disk = psutil.disk_usage('.')
|
|
||||||
|
|
||||||
# Uptime berechnen
|
|
||||||
uptime = datetime.datetime.now() - metrics['startup_time']
|
|
||||||
|
|
||||||
# Anwendungsmetriken
|
|
||||||
app_metrics = {
|
|
||||||
'system': {
|
|
||||||
'cpu_percent': cpu_percent,
|
|
||||||
'memory_percent': memory.percent,
|
|
||||||
'disk_percent': (disk.used / disk.total) * 100,
|
|
||||||
'uptime_seconds': uptime.total_seconds()
|
|
||||||
},
|
|
||||||
'application': {
|
|
||||||
'requests_total': dict(metrics['requests_total']),
|
|
||||||
'database_queries_total': metrics['database_queries'],
|
|
||||||
'active_jobs': metrics['active_jobs'],
|
|
||||||
'error_count': dict(metrics['error_count']),
|
|
||||||
'startup_time': metrics['startup_time'].isoformat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonify(app_metrics)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(f"Fehler beim Sammeln der Metriken: {e}")
|
|
||||||
return jsonify({'error': 'Metriken nicht verfügbar'}), 500
|
|
||||||
|
|
||||||
@monitoring_bp.route('/info')
|
|
||||||
def get_info():
|
|
||||||
"""
|
|
||||||
Gibt allgemeine Informationen über die Anwendung zurück.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON: Anwendungsinformationen
|
|
||||||
"""
|
|
||||||
return jsonify({
|
|
||||||
'application': 'MYP Backend',
|
|
||||||
'version': '2.0.0',
|
|
||||||
'flask_env': current_app.config.get('FLASK_ENV', 'unknown'),
|
|
||||||
'debug': current_app.debug,
|
|
||||||
'startup_time': metrics['startup_time'].isoformat(),
|
|
||||||
'python_version': os.sys.version,
|
|
||||||
'config': {
|
|
||||||
'database': current_app.config.get('DATABASE'),
|
|
||||||
'job_check_interval': current_app.config.get('JOB_CHECK_INTERVAL'),
|
|
||||||
'security_enabled': current_app.config.get('SECURITY_ENABLED', False),
|
|
||||||
'rate_limit_enabled': current_app.config.get('RATE_LIMIT_ENABLED', False)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
def record_request_metric(endpoint, method, status_code, duration):
|
|
||||||
"""
|
|
||||||
Zeichnet Request-Metriken auf.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
endpoint: API-Endpunkt
|
|
||||||
method: HTTP-Methode
|
|
||||||
status_code: HTTP-Status-Code
|
|
||||||
duration: Request-Dauer in Sekunden
|
|
||||||
"""
|
|
||||||
key = f"{method}_{endpoint}"
|
|
||||||
metrics['requests_total'][key] += 1
|
|
||||||
metrics['request_duration'][key].append(duration)
|
|
||||||
|
|
||||||
if status_code >= 400:
|
|
||||||
metrics['error_count'][str(status_code)] += 1
|
|
||||||
|
|
||||||
def record_database_query():
|
|
||||||
"""Zeichnet eine Datenbankabfrage auf."""
|
|
||||||
metrics['database_queries'] += 1
|
|
||||||
|
|
||||||
def update_active_jobs(count):
|
|
||||||
"""
|
|
||||||
Aktualisiert die Anzahl aktiver Jobs.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
count: Anzahl aktiver Jobs
|
|
||||||
"""
|
|
||||||
metrics['active_jobs'] = count
|
|
||||||
|
|
||||||
class RequestMetricsMiddleware:
|
|
||||||
"""Middleware für automatisches Request-Tracking."""
|
|
||||||
|
|
||||||
def __init__(self, app=None):
|
|
||||||
self.app = app
|
|
||||||
if app is not None:
|
|
||||||
self.init_app(app)
|
|
||||||
|
|
||||||
def init_app(self, app):
|
|
||||||
"""Initialisiert die Middleware mit der Flask-App."""
|
|
||||||
app.before_request(self.before_request)
|
|
||||||
app.after_request(self.after_request)
|
|
||||||
|
|
||||||
def before_request(self):
|
|
||||||
"""Startet die Zeitmessung für den Request."""
|
|
||||||
from flask import g
|
|
||||||
g.start_time = time.time()
|
|
||||||
|
|
||||||
def after_request(self, response):
|
|
||||||
"""Zeichnet Metriken nach dem Request auf."""
|
|
||||||
from flask import g, request
|
|
||||||
|
|
||||||
if hasattr(g, 'start_time'):
|
|
||||||
duration = time.time() - g.start_time
|
|
||||||
record_request_metric(
|
|
||||||
request.endpoint or 'unknown',
|
|
||||||
request.method,
|
|
||||||
response.status_code,
|
|
||||||
duration
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
# Globale Middleware-Instanz
|
|
||||||
request_metrics = RequestMetricsMiddleware()
|
|
@ -1,36 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=MYP Backend Flask Application
|
|
||||||
Documentation=https://github.com/your-org/myp
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=notify
|
|
||||||
User=myp
|
|
||||||
Group=myp
|
|
||||||
WorkingDirectory=/opt/myp/backend
|
|
||||||
Environment=PATH=/opt/myp/venv/bin
|
|
||||||
Environment=FLASK_ENV=production
|
|
||||||
ExecStart=/opt/myp/venv/bin/gunicorn --workers=4 --worker-class=sync --bind=0.0.0.0:5000 --timeout=30 --keep-alive=5 --max-requests=1000 --max-requests-jitter=100 --preload --access-logfile=logs/access.log --error-logfile=logs/error.log --log-level=info --capture-output --enable-stdio-inheritance wsgi:application
|
|
||||||
ExecReload=/bin/kill -s HUP $MAINPID
|
|
||||||
KillMode=mixed
|
|
||||||
TimeoutStopSec=5
|
|
||||||
PrivateTmp=true
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=10
|
|
||||||
|
|
||||||
# Security settings
|
|
||||||
NoNewPrivileges=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadWritePaths=/opt/myp/backend/logs /opt/myp/backend/instance
|
|
||||||
ProtectKernelTunables=true
|
|
||||||
ProtectKernelModules=true
|
|
||||||
ProtectControlGroups=true
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
SyslogIdentifier=myp-backend
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
@ -1,185 +0,0 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
import socket
|
|
||||||
import subprocess
|
|
||||||
import platform
|
|
||||||
import netifaces
|
|
||||||
import requests
|
|
||||||
from datetime import datetime
|
|
||||||
import logging
|
|
||||||
|
|
||||||
class NetworkConfig:
|
|
||||||
"""Verwaltet die Netzwerkkonfiguration für das MYP-System."""
|
|
||||||
|
|
||||||
CONFIG_FILE = 'instance/network_config.json'
|
|
||||||
DEFAULT_CONFIG = {
|
|
||||||
'backend_hostname': '192.168.0.5',
|
|
||||||
'backend_port': 5000,
|
|
||||||
'frontend_hostname': '192.168.0.106',
|
|
||||||
'frontend_port': 3000
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, app=None):
|
|
||||||
"""Initialisierung der Netzwerkkonfiguration."""
|
|
||||||
self.logger = logging.getLogger('myp.network')
|
|
||||||
self.config = self.DEFAULT_CONFIG.copy()
|
|
||||||
self.last_check = None
|
|
||||||
self.backend_status = "Nicht überprüft"
|
|
||||||
self.frontend_status = "Nicht überprüft"
|
|
||||||
|
|
||||||
# Stelle sicher, dass das Verzeichnis existiert
|
|
||||||
os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True)
|
|
||||||
|
|
||||||
# Lade gespeicherte Konfiguration, falls vorhanden
|
|
||||||
self.load_config()
|
|
||||||
|
|
||||||
if app:
|
|
||||||
self.init_app(app)
|
|
||||||
|
|
||||||
def init_app(self, app):
|
|
||||||
"""Initialisiert die Anwendung mit dieser Konfiguration."""
|
|
||||||
app.network_config = self
|
|
||||||
|
|
||||||
# Registriere Route für Netzwerkkonfiguration
|
|
||||||
@app.route('/admin/network-config', methods=['GET', 'POST'])
|
|
||||||
def network_config():
|
|
||||||
from flask import request, render_template, flash, redirect, url_for
|
|
||||||
|
|
||||||
# Prüfe aktuelle Status
|
|
||||||
self.check_connection_statuses()
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
# Aktualisiere Konfiguration
|
|
||||||
self.config['backend_hostname'] = request.form.get('backend_hostname', self.DEFAULT_CONFIG['backend_hostname'])
|
|
||||||
self.config['backend_port'] = int(request.form.get('backend_port', self.DEFAULT_CONFIG['backend_port']))
|
|
||||||
self.config['frontend_hostname'] = request.form.get('frontend_hostname', self.DEFAULT_CONFIG['frontend_hostname'])
|
|
||||||
self.config['frontend_port'] = int(request.form.get('frontend_port', self.DEFAULT_CONFIG['frontend_port']))
|
|
||||||
|
|
||||||
# Speichere Konfiguration
|
|
||||||
self.save_config()
|
|
||||||
|
|
||||||
# Teste die neue Konfiguration
|
|
||||||
self.check_connection_statuses()
|
|
||||||
|
|
||||||
flash('Netzwerkkonfiguration erfolgreich gespeichert!', 'success')
|
|
||||||
return redirect(url_for('network_config'))
|
|
||||||
|
|
||||||
# Ermittle Netzwerkschnittstellen
|
|
||||||
network_interfaces = self.get_network_interfaces()
|
|
||||||
|
|
||||||
return render_template('network_config.html',
|
|
||||||
config=self.config,
|
|
||||||
backend_status=self.backend_status,
|
|
||||||
frontend_status=self.frontend_status,
|
|
||||||
last_check=self.last_check,
|
|
||||||
network_interfaces=network_interfaces,
|
|
||||||
message=request.args.get('message'),
|
|
||||||
message_type=request.args.get('message_type', 'info'))
|
|
||||||
|
|
||||||
def load_config(self):
|
|
||||||
"""Lädt die gespeicherte Konfiguration."""
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.CONFIG_FILE):
|
|
||||||
with open(self.CONFIG_FILE, 'r') as f:
|
|
||||||
saved_config = json.load(f)
|
|
||||||
self.config.update(saved_config)
|
|
||||||
self.logger.info(f"Netzwerkkonfiguration geladen: {self.config}")
|
|
||||||
else:
|
|
||||||
self.logger.info(f"Keine gespeicherte Konfiguration gefunden, verwende Standardwerte: {self.config}")
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Fehler beim Laden der Netzwerkkonfiguration: {e}")
|
|
||||||
|
|
||||||
def save_config(self):
|
|
||||||
"""Speichert die aktuelle Konfiguration."""
|
|
||||||
try:
|
|
||||||
with open(self.CONFIG_FILE, 'w') as f:
|
|
||||||
json.dump(self.config, f, indent=4)
|
|
||||||
self.logger.info(f"Netzwerkkonfiguration gespeichert: {self.config}")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Fehler beim Speichern der Netzwerkkonfiguration: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_backend_url(self):
|
|
||||||
"""Gibt die Backend-URL zurück."""
|
|
||||||
return f"http://{self.config['backend_hostname']}:{self.config['backend_port']}"
|
|
||||||
|
|
||||||
def get_frontend_url(self):
|
|
||||||
"""Gibt die Frontend-URL zurück."""
|
|
||||||
return f"http://{self.config['frontend_hostname']}:{self.config['frontend_port']}"
|
|
||||||
|
|
||||||
def check_connection_statuses(self):
|
|
||||||
"""Überprüft den Verbindungsstatus zu Backend und Frontend."""
|
|
||||||
self.last_check = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
|
|
||||||
|
|
||||||
# Prüfe Backend-Verbindung
|
|
||||||
backend_url = self.get_backend_url()
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{backend_url}/api/test", timeout=3)
|
|
||||||
if response.status_code == 200:
|
|
||||||
self.backend_status = "Verbunden"
|
|
||||||
else:
|
|
||||||
self.backend_status = f"Fehler: HTTP {response.status_code}"
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
self.backend_status = f"Nicht erreichbar: {str(e)}"
|
|
||||||
|
|
||||||
# Prüfe Frontend-Verbindung
|
|
||||||
frontend_url = self.get_frontend_url()
|
|
||||||
try:
|
|
||||||
response = requests.get(frontend_url, timeout=3)
|
|
||||||
if response.status_code == 200:
|
|
||||||
self.frontend_status = "Verbunden"
|
|
||||||
else:
|
|
||||||
self.frontend_status = f"Fehler: HTTP {response.status_code}"
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
self.frontend_status = f"Nicht erreichbar: {str(e)}"
|
|
||||||
|
|
||||||
self.logger.info(f"Verbindungsstatus - Backend: {self.backend_status}, Frontend: {self.frontend_status}")
|
|
||||||
|
|
||||||
def get_network_interfaces(self):
|
|
||||||
"""Gibt Informationen zu allen Netzwerkschnittstellen zurück."""
|
|
||||||
interfaces = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
for interface in netifaces.interfaces():
|
|
||||||
if interface.startswith(('lo', 'docker', 'br-')):
|
|
||||||
continue # Ignoriere Loopback und Docker-Interfaces
|
|
||||||
|
|
||||||
addresses = []
|
|
||||||
try:
|
|
||||||
addrs = netifaces.ifaddresses(interface)
|
|
||||||
if netifaces.AF_INET in addrs:
|
|
||||||
for addr in addrs[netifaces.AF_INET]:
|
|
||||||
if 'addr' in addr:
|
|
||||||
addresses.append(addr['addr'])
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Fehler beim Ermitteln der Adresse für Interface {interface}: {e}")
|
|
||||||
|
|
||||||
if addresses:
|
|
||||||
interfaces.append({
|
|
||||||
'name': interface,
|
|
||||||
'address': ', '.join(addresses)
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Fehler beim Ermitteln der Netzwerkschnittstellen: {e}")
|
|
||||||
|
|
||||||
return interfaces
|
|
||||||
|
|
||||||
# Helper-Funktion zum Testen von Netzwerkverbindungen
|
|
||||||
def test_connection(host, port, timeout=2):
|
|
||||||
"""Testet eine TCP-Verbindung zu einem Host und Port."""
|
|
||||||
try:
|
|
||||||
socket.setdefaulttimeout(timeout)
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.connect((host, port))
|
|
||||||
s.close()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Helper-Funktion zum Ping eines Hosts
|
|
||||||
def ping_host(host, count=1):
|
|
||||||
"""Pingt einen Host an."""
|
|
||||||
param = '-n' if platform.system().lower() == 'windows' else '-c'
|
|
||||||
command = ['ping', param, str(count), host]
|
|
||||||
return subprocess.call(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0
|
|
@ -1,86 +1,25 @@
|
|||||||
# === MYP Backend Dependencies ===
|
# MYP V2 - Python Dependencies
|
||||||
|
# Installiere mit: pip3.11 install -r requirements.txt
|
||||||
|
|
||||||
# Core Flask und Extensions
|
# Flask Framework und Extensions
|
||||||
Flask>=2.0.0,<3.0.0
|
Flask==3.0.0
|
||||||
Flask-SQLAlchemy>=3.0.0
|
Flask-Login==0.6.3
|
||||||
Flask-CORS>=4.0.0
|
|
||||||
Flask-JWT-Extended>=4.4.0
|
|
||||||
Flask-Login>=0.6.0
|
|
||||||
Flask-Migrate>=4.0.0
|
|
||||||
Flask-Assets>=2.1.0
|
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
SQLAlchemy>=2.0.0
|
SQLAlchemy==2.0.23
|
||||||
|
|
||||||
# API und Serialisierung
|
# Smart Plug Steuerung
|
||||||
Flask-RESTful>=0.3.10
|
PyP100==0.1.4
|
||||||
marshmallow>=3.19.0
|
|
||||||
flask-marshmallow>=0.15.0
|
|
||||||
marshmallow-sqlalchemy>=0.29.0
|
|
||||||
|
|
||||||
# HTTP Requests
|
# Passwort-Hashing (bereits in Flask enthalten, aber explizit für Klarheit)
|
||||||
requests>=2.31.0
|
Werkzeug==3.0.1
|
||||||
urllib3>=2.0.0
|
|
||||||
|
|
||||||
# Smart Home Integration (Tapo)
|
|
||||||
PyP100>=0.1.2
|
|
||||||
|
|
||||||
# System und Netzwerk Monitoring
|
|
||||||
psutil>=5.9.0
|
|
||||||
netifaces>=0.11.0
|
|
||||||
ping3>=4.0.0
|
|
||||||
speedtest-cli>=2.1.3
|
|
||||||
|
|
||||||
# Konfiguration und Umgebung
|
|
||||||
python-dotenv>=1.0.0
|
|
||||||
|
|
||||||
# Sicherheit
|
|
||||||
PyJWT>=2.8.0
|
|
||||||
bcrypt>=4.0.0
|
|
||||||
cryptography>=41.0.0
|
|
||||||
|
|
||||||
# CLI und Tools
|
|
||||||
click>=8.1.0
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
validators>=0.20.0
|
|
||||||
python-dateutil>=2.8.0
|
|
||||||
pytz>=2023.3
|
|
||||||
schedule>=1.2.0
|
|
||||||
watchdog>=3.0.0
|
|
||||||
filelock>=3.12.0
|
|
||||||
|
|
||||||
# Caching
|
|
||||||
redis>=4.6.0
|
|
||||||
cachetools>=5.3.0
|
|
||||||
|
|
||||||
# JSON und Data Processing
|
|
||||||
jsonschema>=4.19.0
|
|
||||||
pydantic>=2.4.0
|
|
||||||
|
|
||||||
# Production Server (optional)
|
|
||||||
gunicorn>=21.2.0
|
|
||||||
gevent>=23.7.0
|
|
||||||
supervisor>=4.2.0
|
|
||||||
|
|
||||||
# Entwicklung und Testing (optional)
|
# Entwicklung und Testing (optional)
|
||||||
pytest>=7.4.0
|
pytest==7.4.3
|
||||||
pytest-flask>=1.2.0
|
pytest-cov==4.1.0
|
||||||
coverage>=7.3.0
|
|
||||||
pytest-cov>=4.1.0
|
|
||||||
|
|
||||||
# Logging und Monitoring
|
# Produktions-Server (optional)
|
||||||
structlog>=23.1.0
|
gunicorn==21.2.0
|
||||||
colorlog>=6.7.0
|
|
||||||
|
|
||||||
# Performance und Memory
|
# Monitoring und Logging (optional)
|
||||||
memory-profiler>=0.61.0
|
psutil==5.9.6
|
||||||
line-profiler>=4.1.0
|
|
||||||
|
|
||||||
# WebSockets (für Real-time Updates)
|
|
||||||
Flask-SocketIO>=5.3.0
|
|
||||||
python-socketio>=5.8.0
|
|
||||||
|
|
||||||
# Task Queue (optional für Background Jobs)
|
|
||||||
celery>=5.3.0
|
|
||||||
kombu>=5.3.0
|
|
@ -1,220 +0,0 @@
|
|||||||
"""
|
|
||||||
Sicherheitsmodule und Middleware für die MYP Flask-Anwendung.
|
|
||||||
Implementiert CSRF-Schutz, Content Security Policy und weitere Sicherheitsmaßnahmen.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import request, jsonify, current_app
|
|
||||||
from flask_talisman import Talisman
|
|
||||||
from functools import wraps
|
|
||||||
import time
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
from collections import defaultdict, deque
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
class SecurityMiddleware:
|
|
||||||
"""Zentrale Sicherheits-Middleware für die Anwendung."""
|
|
||||||
|
|
||||||
def __init__(self, app=None):
|
|
||||||
self.app = app
|
|
||||||
self.rate_limits = defaultdict(lambda: deque())
|
|
||||||
self.failed_attempts = defaultdict(int)
|
|
||||||
self.blocked_ips = set()
|
|
||||||
|
|
||||||
if app is not None:
|
|
||||||
self.init_app(app)
|
|
||||||
|
|
||||||
def init_app(self, app):
|
|
||||||
"""Initialisiert die Sicherheits-Middleware mit der Flask-App."""
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
# Talisman für Content Security Policy und HTTPS-Enforcement
|
|
||||||
if not app.debug:
|
|
||||||
Talisman(
|
|
||||||
app,
|
|
||||||
force_https=False, # In Produktion auf True setzen, wenn HTTPS verfügbar
|
|
||||||
strict_transport_security=True,
|
|
||||||
content_security_policy={
|
|
||||||
'default-src': "'self'",
|
|
||||||
'script-src': "'self' 'unsafe-inline'",
|
|
||||||
'style-src': "'self' 'unsafe-inline'",
|
|
||||||
'img-src': "'self' data:",
|
|
||||||
'font-src': "'self'",
|
|
||||||
'connect-src': "'self'",
|
|
||||||
'form-action': "'self'"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Request-Hooks registrieren
|
|
||||||
app.before_request(self.before_request_security_check)
|
|
||||||
app.after_request(self.after_request_security_headers)
|
|
||||||
|
|
||||||
def before_request_security_check(self):
|
|
||||||
"""Sicherheitsüberprüfungen vor jeder Anfrage."""
|
|
||||||
client_ip = self.get_client_ip()
|
|
||||||
|
|
||||||
# Blocked IPs prüfen
|
|
||||||
if client_ip in self.blocked_ips:
|
|
||||||
current_app.logger.warning(f"Blockierte IP-Adresse versucht Zugriff: {client_ip}")
|
|
||||||
return jsonify({'message': 'Zugriff verweigert'}), 403
|
|
||||||
|
|
||||||
# Rate Limiting
|
|
||||||
if self.is_rate_limited(client_ip):
|
|
||||||
current_app.logger.warning(f"Rate Limit überschritten für IP: {client_ip}")
|
|
||||||
return jsonify({'message': 'Zu viele Anfragen'}), 429
|
|
||||||
|
|
||||||
# Content-Length prüfen (Schutz vor großen Payloads)
|
|
||||||
if request.content_length and request.content_length > 10 * 1024 * 1024: # 10MB
|
|
||||||
current_app.logger.warning(f"Payload zu groß von IP: {client_ip}")
|
|
||||||
return jsonify({'message': 'Payload zu groß'}), 413
|
|
||||||
|
|
||||||
def after_request_security_headers(self, response):
|
|
||||||
"""Fügt Sicherheits-Header zu jeder Antwort hinzu."""
|
|
||||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
|
||||||
response.headers['X-Frame-Options'] = 'DENY'
|
|
||||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
|
||||||
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
|
||||||
|
|
||||||
# Cache-Control für statische Ressourcen
|
|
||||||
if request.endpoint and 'static' in request.endpoint:
|
|
||||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
|
||||||
else:
|
|
||||||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
|
||||||
response.headers['Pragma'] = 'no-cache'
|
|
||||||
response.headers['Expires'] = '0'
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def get_client_ip(self):
|
|
||||||
"""Ermittelt die Client-IP-Adresse."""
|
|
||||||
if request.headers.get('X-Forwarded-For'):
|
|
||||||
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
|
|
||||||
elif request.headers.get('X-Real-IP'):
|
|
||||||
return request.headers.get('X-Real-IP')
|
|
||||||
else:
|
|
||||||
return request.remote_addr
|
|
||||||
|
|
||||||
def is_rate_limited(self, ip, max_requests=100, window_minutes=15):
|
|
||||||
"""
|
|
||||||
Überprüft Rate Limiting für eine IP-Adresse.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ip: Client-IP-Adresse
|
|
||||||
max_requests: Maximale Anzahl Requests pro Zeitfenster
|
|
||||||
window_minutes: Zeitfenster in Minuten
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True wenn Rate Limit überschritten
|
|
||||||
"""
|
|
||||||
now = datetime.now()
|
|
||||||
window_start = now - timedelta(minutes=window_minutes)
|
|
||||||
|
|
||||||
# Alte Einträge entfernen
|
|
||||||
while self.rate_limits[ip] and self.rate_limits[ip][0] < window_start:
|
|
||||||
self.rate_limits[ip].popleft()
|
|
||||||
|
|
||||||
# Neue Anfrage hinzufügen
|
|
||||||
self.rate_limits[ip].append(now)
|
|
||||||
|
|
||||||
# Rate Limit prüfen
|
|
||||||
if len(self.rate_limits[ip]) > max_requests:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def record_failed_login(self, ip):
|
|
||||||
"""
|
|
||||||
Zeichnet fehlgeschlagene Login-Versuche auf.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ip: Client-IP-Adresse
|
|
||||||
"""
|
|
||||||
self.failed_attempts[ip] += 1
|
|
||||||
|
|
||||||
# Nach 5 fehlgeschlagenen Versuchen temporär blockieren
|
|
||||||
if self.failed_attempts[ip] >= 5:
|
|
||||||
self.blocked_ips.add(ip)
|
|
||||||
current_app.logger.warning(f"IP-Adresse blockiert nach zu vielen fehlgeschlagenen Login-Versuchen: {ip}")
|
|
||||||
|
|
||||||
# Automatisches Entsperren nach 1 Stunde
|
|
||||||
def unblock_ip():
|
|
||||||
time.sleep(3600) # 1 Stunde
|
|
||||||
if ip in self.blocked_ips:
|
|
||||||
self.blocked_ips.remove(ip)
|
|
||||||
self.failed_attempts[ip] = 0
|
|
||||||
current_app.logger.info(f"IP-Adresse automatisch entsperrt: {ip}")
|
|
||||||
|
|
||||||
import threading
|
|
||||||
threading.Thread(target=unblock_ip, daemon=True).start()
|
|
||||||
|
|
||||||
def clear_failed_attempts(self, ip):
|
|
||||||
"""
|
|
||||||
Löscht fehlgeschlagene Login-Versuche für eine IP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ip: Client-IP-Adresse
|
|
||||||
"""
|
|
||||||
if ip in self.failed_attempts:
|
|
||||||
self.failed_attempts[ip] = 0
|
|
||||||
|
|
||||||
def require_api_key(f):
|
|
||||||
"""
|
|
||||||
Decorator für API-Endpunkte, die einen API-Key erfordern.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
f: Zu schützende Funktion
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Geschützte Funktion
|
|
||||||
"""
|
|
||||||
@wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
api_key = request.headers.get('X-API-Key')
|
|
||||||
expected_key = current_app.config.get('API_KEY')
|
|
||||||
|
|
||||||
if not expected_key:
|
|
||||||
# Kein API-Key konfiguriert, Zugriff erlauben
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
if not api_key:
|
|
||||||
return jsonify({'message': 'API-Key erforderlich'}), 401
|
|
||||||
|
|
||||||
# Sichere Vergleichsfunktion verwenden
|
|
||||||
if not hmac.compare_digest(api_key, expected_key):
|
|
||||||
current_app.logger.warning(f"Ungültiger API-Key von IP: {request.remote_addr}")
|
|
||||||
return jsonify({'message': 'Ungültiger API-Key'}), 401
|
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
def validate_csrf_token():
|
|
||||||
"""
|
|
||||||
Validiert CSRF-Token für POST/PUT/DELETE-Requests.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True wenn Token gültig ist
|
|
||||||
"""
|
|
||||||
if request.method in ['GET', 'HEAD', 'OPTIONS']:
|
|
||||||
return True
|
|
||||||
|
|
||||||
token = request.headers.get('X-CSRF-Token') or request.form.get('csrf_token')
|
|
||||||
session_token = request.cookies.get('csrf_token')
|
|
||||||
|
|
||||||
if not token or not session_token:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return hmac.compare_digest(token, session_token)
|
|
||||||
|
|
||||||
def generate_csrf_token():
|
|
||||||
"""
|
|
||||||
Generiert ein neues CSRF-Token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: CSRF-Token
|
|
||||||
"""
|
|
||||||
import secrets
|
|
||||||
return secrets.token_hex(32)
|
|
||||||
|
|
||||||
# Globale Sicherheits-Middleware-Instanz
|
|
||||||
security_middleware = SecurityMiddleware()
|
|
806
backend/setup_myp.sh
Executable file
806
backend/setup_myp.sh
Executable file
@ -0,0 +1,806 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# MYP V2 - Command Center
|
||||||
|
# Ein umfassendes Verwaltungsskript für MYP V2 (Manage Your Printer)
|
||||||
|
|
||||||
|
# Fehlerabbruch aktivieren
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Farben für bessere Lesbarkeit
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Funktion für Titel
|
||||||
|
print_header() {
|
||||||
|
clear
|
||||||
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
|
echo -e "${BLUE} MYP V2 - Manage Your Printer ${NC}"
|
||||||
|
echo -e "${BLUE} Command Center ${NC}"
|
||||||
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Überprüfen, ob der Benutzer Root-Rechte hat
|
||||||
|
check_root() {
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo -e "${RED}Dieses Skript muss mit Root-Rechten ausgeführt werden.${NC}"
|
||||||
|
echo -e "${YELLOW}Bitte mit 'sudo ./setup_myp.sh' neu starten.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Überprüfen, ob MYP installiert ist
|
||||||
|
is_myp_installed() {
|
||||||
|
if [ -d "/opt/myp" ]; then
|
||||||
|
return 0 # true
|
||||||
|
else
|
||||||
|
return 1 # false
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hauptmenü
|
||||||
|
show_main_menu() {
|
||||||
|
print_header
|
||||||
|
echo -e "${GREEN}Willkommen zum MYP V2 Command Center${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "Bitte wählen Sie eine Option:"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}INSTALLATION & KONFIGURATION${NC}"
|
||||||
|
echo "1) MYP V2 Standardinstallation"
|
||||||
|
echo "2) MYP V2 Kiosk-Modus Installation"
|
||||||
|
echo "3) MYP V2 deinstallieren"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}NETZWERK & SYSTEM${NC}"
|
||||||
|
echo "4) Netzwerkeinstellungen konfigurieren"
|
||||||
|
echo "5) DNS-Server konfigurieren"
|
||||||
|
echo "6) System-Status anzeigen"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}WARTUNG & HILFE${NC}"
|
||||||
|
echo "7) MYP-Dienst starten/stoppen/neustarten"
|
||||||
|
echo "8) Logs anzeigen"
|
||||||
|
echo "9) Dokumentation anzeigen"
|
||||||
|
echo ""
|
||||||
|
echo "q) Beenden"
|
||||||
|
echo ""
|
||||||
|
read -p "Ihre Auswahl: " main_option
|
||||||
|
process_main_menu "$main_option"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verarbeiten der Hauptmenü-Auswahl
|
||||||
|
process_main_menu() {
|
||||||
|
case $1 in
|
||||||
|
1)
|
||||||
|
standard_installation
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
kiosk_installation
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
uninstall_myp
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
network_configuration
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
dns_configuration
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
system_status
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
service_management
|
||||||
|
;;
|
||||||
|
8)
|
||||||
|
show_logs
|
||||||
|
;;
|
||||||
|
9)
|
||||||
|
show_documentation
|
||||||
|
;;
|
||||||
|
q|Q)
|
||||||
|
echo -e "${GREEN}Auf Wiedersehen!${NC}"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Ungültige Option.${NC}"
|
||||||
|
sleep 2
|
||||||
|
show_main_menu
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1) MYP V2 Standardinstallation
|
||||||
|
standard_installation() {
|
||||||
|
print_header
|
||||||
|
echo -e "${GREEN}MYP V2 Standardinstallation${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if is_myp_installed; then
|
||||||
|
echo -e "${YELLOW}MYP ist bereits installiert!${NC}"
|
||||||
|
read -p "Möchten Sie die Installation aktualisieren? (j/n): " update_option
|
||||||
|
if [[ "$update_option" != "j" && "$update_option" != "J" ]]; then
|
||||||
|
show_main_menu
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installiere MYP V2..."
|
||||||
|
|
||||||
|
# Benötigte System-Pakete installieren
|
||||||
|
echo "Installiere System-Abhängigkeiten..."
|
||||||
|
apt update
|
||||||
|
apt install -y python3.11 python3.11-pip python3.11-venv python3.11-dev \
|
||||||
|
build-essential git curl
|
||||||
|
|
||||||
|
# Verzeichnis für MYP erstellen/aktualisieren
|
||||||
|
mkdir -p /opt/myp
|
||||||
|
|
||||||
|
# Basispfad ermitteln
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
|
||||||
|
# Komplette Projektstruktur kopieren
|
||||||
|
echo "Kopiere Projektdateien..."
|
||||||
|
cp -r "$SCRIPT_DIR/app" /opt/myp/
|
||||||
|
cp -r "$SCRIPT_DIR/docs" /opt/myp/
|
||||||
|
cp -r "$SCRIPT_DIR/install" /opt/myp/
|
||||||
|
cp "$SCRIPT_DIR/requirements.txt" /opt/myp/
|
||||||
|
cp "$SCRIPT_DIR/README.md" /opt/myp/
|
||||||
|
cp "$SCRIPT_DIR/ROADMAP.md" /opt/myp/
|
||||||
|
cp "$SCRIPT_DIR/COMMON_ERRORS.md" /opt/myp/
|
||||||
|
|
||||||
|
# Log-Verzeichnisse erstellen
|
||||||
|
echo "Erstelle Log-Verzeichnisse..."
|
||||||
|
mkdir -p /opt/myp/logs/{app,auth,jobs,printers,scheduler}
|
||||||
|
|
||||||
|
# Datenbank-Verzeichnis erstellen
|
||||||
|
mkdir -p /opt/myp/data
|
||||||
|
|
||||||
|
# Python-Umgebung und Abhängigkeiten einrichten
|
||||||
|
echo "Richte Python-Umgebung ein..."
|
||||||
|
cd /opt/myp
|
||||||
|
if [ ! -d ".venv" ]; then
|
||||||
|
python3.11 -m venv .venv
|
||||||
|
fi
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Berechtigungen setzen
|
||||||
|
echo "Setze Berechtigungen..."
|
||||||
|
chown -R www-data:www-data /opt/myp
|
||||||
|
chmod -R 755 /opt/myp
|
||||||
|
chmod -R 775 /opt/myp/logs
|
||||||
|
chmod -R 775 /opt/myp/data
|
||||||
|
|
||||||
|
echo -e "${GREEN}Installation abgeschlossen.${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Nächste Schritte:${NC}"
|
||||||
|
echo "1. Konfiguration anpassen: /opt/myp/app/config/settings.py"
|
||||||
|
echo "2. MYP starten:"
|
||||||
|
echo " cd /opt/myp && source .venv/bin/activate && python3.11 app/app.py"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Oder verwenden Sie Option 7 für Dienst-Management${NC}"
|
||||||
|
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2) MYP V2 Kiosk-Modus Installation
|
||||||
|
kiosk_installation() {
|
||||||
|
print_header
|
||||||
|
echo -e "${GREEN}MYP V2 Kiosk-Modus Installation${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Benötigte Pakete installieren
|
||||||
|
echo "Installiere benötigte Pakete..."
|
||||||
|
apt update
|
||||||
|
apt install -y python3.11 python3.11-pip python3.11-venv python3.11-dev \
|
||||||
|
build-essential chromium-browser unclutter xdotool \
|
||||||
|
xscreensaver git curl nginx
|
||||||
|
|
||||||
|
# Verzeichnis für MYP erstellen und Projekt kopieren
|
||||||
|
echo "Kopiere MYP V2 nach /opt/myp..."
|
||||||
|
mkdir -p /opt/myp
|
||||||
|
|
||||||
|
# Basispfad ermitteln
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
|
||||||
|
# Komplette Projektstruktur kopieren
|
||||||
|
cp -r "$SCRIPT_DIR/app" /opt/myp/
|
||||||
|
cp -r "$SCRIPT_DIR/docs" /opt/myp/
|
||||||
|
cp -r "$SCRIPT_DIR/install" /opt/myp/
|
||||||
|
cp "$SCRIPT_DIR/requirements.txt" /opt/myp/
|
||||||
|
cp "$SCRIPT_DIR/README.md" /opt/myp/
|
||||||
|
cp "$SCRIPT_DIR/ROADMAP.md" /opt/myp/
|
||||||
|
cp "$SCRIPT_DIR/COMMON_ERRORS.md" /opt/myp/
|
||||||
|
|
||||||
|
# Log-Verzeichnisse erstellen
|
||||||
|
mkdir -p /opt/myp/logs/{app,auth,jobs,printers,scheduler}
|
||||||
|
|
||||||
|
# Datenbank-Verzeichnis erstellen
|
||||||
|
mkdir -p /opt/myp/data
|
||||||
|
|
||||||
|
# Python-Umgebung und Abhängigkeiten einrichten
|
||||||
|
echo "Richte Python-Umgebung ein..."
|
||||||
|
cd /opt/myp
|
||||||
|
python3.11 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install gunicorn # Für Produktionsserver
|
||||||
|
|
||||||
|
# Systemd-Dienst für Flask-Backend einrichten
|
||||||
|
echo "Richte Flask-Backend-Dienst ein..."
|
||||||
|
|
||||||
|
# Service-File erstellen
|
||||||
|
cat > /etc/systemd/system/myp.service << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=MYP V2 Flask Application
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=www-data
|
||||||
|
Group=www-data
|
||||||
|
WorkingDirectory=/opt/myp
|
||||||
|
Environment=PATH=/opt/myp/.venv/bin
|
||||||
|
ExecStart=/opt/myp/.venv/bin/python3.11 /opt/myp/app/app.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Berechtigungen setzen
|
||||||
|
chown -R www-data:www-data /opt/myp
|
||||||
|
chmod -R 755 /opt/myp
|
||||||
|
chmod -R 775 /opt/myp/logs
|
||||||
|
chmod -R 775 /opt/myp/data
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable myp.service
|
||||||
|
systemctl start myp.service
|
||||||
|
|
||||||
|
# Nginx-Konfiguration für Reverse Proxy
|
||||||
|
echo "Konfiguriere Nginx..."
|
||||||
|
cat > /etc/nginx/sites-available/myp << EOF
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:5000;
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static {
|
||||||
|
alias /opt/myp/app/static;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ln -sf /etc/nginx/sites-available/myp /etc/nginx/sites-enabled/
|
||||||
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
|
systemctl restart nginx
|
||||||
|
|
||||||
|
# Kiosk-Script einrichten
|
||||||
|
echo "Richte Kiosk-Script ein..."
|
||||||
|
|
||||||
|
cat > /opt/myp/kiosk.sh << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# MYP V2 Kiosk-Modus Script
|
||||||
|
|
||||||
|
# Bildschirmschoner deaktivieren
|
||||||
|
xset s off
|
||||||
|
xset -dpms
|
||||||
|
xset s noblank
|
||||||
|
|
||||||
|
# Mauszeiger verstecken
|
||||||
|
unclutter -idle 0.5 -root &
|
||||||
|
|
||||||
|
# Chromium im Kiosk-Modus starten
|
||||||
|
chromium-browser \
|
||||||
|
--noerrdialogs \
|
||||||
|
--disable-infobars \
|
||||||
|
--kiosk \
|
||||||
|
--disable-session-crashed-bubble \
|
||||||
|
--disable-restore-session-state \
|
||||||
|
--disable-background-timer-throttling \
|
||||||
|
--disable-backgrounding-occluded-windows \
|
||||||
|
--disable-renderer-backgrounding \
|
||||||
|
--disable-features=TranslateUI \
|
||||||
|
--disable-ipc-flooding-protection \
|
||||||
|
--disable-background-networking \
|
||||||
|
--disable-sync \
|
||||||
|
--disable-default-apps \
|
||||||
|
--no-first-run \
|
||||||
|
--fast \
|
||||||
|
--fast-start \
|
||||||
|
--disable-gpu \
|
||||||
|
--no-sandbox \
|
||||||
|
http://localhost/
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x /opt/myp/kiosk.sh
|
||||||
|
|
||||||
|
# Autostart für Kiosk-Modus einrichten
|
||||||
|
mkdir -p /home/pi/.config/autostart
|
||||||
|
cat > /home/pi/.config/autostart/myp-kiosk.desktop << EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=MYP Kiosk
|
||||||
|
Exec=/opt/myp/kiosk.sh
|
||||||
|
Hidden=false
|
||||||
|
NoDisplay=false
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown -R pi:pi /home/pi/.config
|
||||||
|
|
||||||
|
echo -e "${GREEN}Kiosk-Installation abgeschlossen.${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Das System wird beim nächsten Start automatisch im Kiosk-Modus starten.${NC}"
|
||||||
|
echo -e "${BLUE}MYP V2 ist unter http://localhost erreichbar${NC}"
|
||||||
|
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3) MYP V2 deinstallieren
|
||||||
|
uninstall_myp() {
|
||||||
|
print_header
|
||||||
|
echo -e "${RED}MYP V2 deinstallieren${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if ! is_myp_installed; then
|
||||||
|
echo -e "${YELLOW}MYP ist nicht installiert.${NC}"
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "Sind Sie sicher, dass Sie MYP komplett deinstallieren möchten? (j/n): " confirm
|
||||||
|
if [[ "$confirm" != "j" && "$confirm" != "J" ]]; then
|
||||||
|
show_main_menu
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Stoppe MYP-Dienste..."
|
||||||
|
systemctl stop myp.service 2>/dev/null || true
|
||||||
|
systemctl disable myp.service 2>/dev/null || true
|
||||||
|
rm -f /etc/systemd/system/myp.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
echo "Entferne Kiosk-Modus Komponenten..."
|
||||||
|
rm -f /home/pi/kiosk.sh
|
||||||
|
rm -f /home/pi/.config/autostart/myp-kiosk.desktop
|
||||||
|
|
||||||
|
echo "Entferne MYP-Dateien..."
|
||||||
|
rm -rf /opt/myp
|
||||||
|
|
||||||
|
echo -e "${GREEN}MYP wurde vollständig deinstalliert.${NC}"
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4) Netzwerkeinstellungen konfigurieren
|
||||||
|
network_configuration() {
|
||||||
|
print_header
|
||||||
|
echo -e "${GREEN}Netzwerkeinstellungen konfigurieren${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Netzwerkschnittstellen anzeigen
|
||||||
|
echo -e "${YELLOW}Verfügbare Netzwerkschnittstellen:${NC}"
|
||||||
|
ip -o link show | awk -F': ' '{print $2}'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Welche Schnittstelle möchten Sie konfigurieren? (z.B. eth0, wlan0): " interface
|
||||||
|
|
||||||
|
# Prüfen, ob die Schnittstelle existiert
|
||||||
|
if ! ip link show "$interface" &>/dev/null; then
|
||||||
|
echo -e "${RED}Die angegebene Schnittstelle existiert nicht.${NC}"
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Konfiguration für $interface:${NC}"
|
||||||
|
echo "1) DHCP (automatische IP-Adresse)"
|
||||||
|
echo "2) Statische IP-Adresse"
|
||||||
|
echo "3) WLAN konfigurieren (für wlan0)"
|
||||||
|
echo "4) Zurück zum Hauptmenü"
|
||||||
|
echo ""
|
||||||
|
read -p "Ihre Auswahl: " network_option
|
||||||
|
|
||||||
|
case $network_option in
|
||||||
|
1)
|
||||||
|
# DHCP konfigurieren
|
||||||
|
echo "Konfiguriere DHCP für $interface..."
|
||||||
|
cat > "/etc/network/interfaces.d/$interface" << EOF
|
||||||
|
auto $interface
|
||||||
|
iface $interface inet dhcp
|
||||||
|
EOF
|
||||||
|
if [ "$interface" == "wlan0" ]; then
|
||||||
|
echo "Für WLAN bitte auch SSID und Passwort konfigurieren."
|
||||||
|
network_configuration
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
systemctl restart networking
|
||||||
|
echo -e "${GREEN}DHCP wurde für $interface konfiguriert.${NC}"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
# Statische IP-Adresse konfigurieren
|
||||||
|
read -p "IP-Adresse (z.B. 192.168.1.100): " ip_address
|
||||||
|
read -p "Netzmaske (z.B. 255.255.255.0): " netmask
|
||||||
|
read -p "Gateway (z.B. 192.168.1.1): " gateway
|
||||||
|
|
||||||
|
cat > "/etc/network/interfaces.d/$interface" << EOF
|
||||||
|
auto $interface
|
||||||
|
iface $interface inet static
|
||||||
|
address $ip_address
|
||||||
|
netmask $netmask
|
||||||
|
gateway $gateway
|
||||||
|
EOF
|
||||||
|
systemctl restart networking
|
||||||
|
echo -e "${GREEN}Statische IP-Adresse wurde für $interface konfiguriert.${NC}"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
# WLAN konfigurieren
|
||||||
|
if [ "$interface" != "wlan0" ]; then
|
||||||
|
echo -e "${RED}Diese Option ist nur für wlan0 verfügbar.${NC}"
|
||||||
|
network_configuration
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "SSID (Netzwerkname): " ssid
|
||||||
|
read -s -p "Passwort: " password
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# wpa_supplicant.conf erstellen/aktualisieren
|
||||||
|
wpa_passphrase "$ssid" "$password" > /etc/wpa_supplicant/wpa_supplicant.conf
|
||||||
|
|
||||||
|
# Netzwerk-Interface konfigurieren
|
||||||
|
cat > "/etc/network/interfaces.d/wlan0" << EOF
|
||||||
|
auto wlan0
|
||||||
|
allow-hotplug wlan0
|
||||||
|
iface wlan0 inet dhcp
|
||||||
|
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# WLAN neustarten
|
||||||
|
systemctl restart networking
|
||||||
|
wpa_cli -i wlan0 reconfigure
|
||||||
|
|
||||||
|
echo -e "${GREEN}WLAN wurde konfiguriert und verbunden.${NC}"
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
show_main_menu
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Ungültige Option.${NC}"
|
||||||
|
network_configuration
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# 5) DNS-Server konfigurieren
|
||||||
|
dns_configuration() {
|
||||||
|
print_header
|
||||||
|
echo -e "${GREEN}DNS-Server konfigurieren${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Aktuelle DNS-Server anzeigen
|
||||||
|
echo -e "${YELLOW}Aktuelle DNS-Server:${NC}"
|
||||||
|
if [ -f "/etc/resolv.conf" ]; then
|
||||||
|
grep "nameserver" /etc/resolv.conf || echo "Keine DNS-Server konfiguriert."
|
||||||
|
else
|
||||||
|
echo "Die Datei /etc/resolv.conf existiert nicht."
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "1) DNS-Server automatisch beziehen (über DHCP)"
|
||||||
|
echo "2) Manuelle DNS-Server konfigurieren"
|
||||||
|
echo "3) Zurück zum Hauptmenü"
|
||||||
|
echo ""
|
||||||
|
read -p "Ihre Auswahl: " dns_option
|
||||||
|
|
||||||
|
case $dns_option in
|
||||||
|
1)
|
||||||
|
# Automatische DNS-Konfiguration
|
||||||
|
if [ -f "/etc/dhcp/dhclient.conf" ]; then
|
||||||
|
sed -i 's/^prepend domain-name-servers.*//' /etc/dhcp/dhclient.conf
|
||||||
|
echo -e "${GREEN}DNS-Server werden jetzt automatisch über DHCP bezogen.${NC}"
|
||||||
|
systemctl restart networking
|
||||||
|
else
|
||||||
|
echo -e "${RED}Die Datei /etc/dhcp/dhclient.conf existiert nicht.${NC}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
# Manuelle DNS-Konfiguration
|
||||||
|
read -p "Primärer DNS-Server (z.B. 8.8.8.8): " primary_dns
|
||||||
|
read -p "Sekundärer DNS-Server (z.B. 8.8.4.4, leer lassen wenn nicht benötigt): " secondary_dns
|
||||||
|
|
||||||
|
# resolv.conf direkt bearbeiten
|
||||||
|
echo "nameserver $primary_dns" > /etc/resolv.conf
|
||||||
|
if [ -n "$secondary_dns" ]; then
|
||||||
|
echo "nameserver $secondary_dns" >> /etc/resolv.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Für persistente Konfiguration
|
||||||
|
if [ -f "/etc/dhcp/dhclient.conf" ]; then
|
||||||
|
if [ -n "$secondary_dns" ]; then
|
||||||
|
sed -i "s/^prepend domain-name-servers.*/prepend domain-name-servers $primary_dns, $secondary_dns;/" /etc/dhcp/dhclient.conf
|
||||||
|
if ! grep -q "prepend domain-name-servers" /etc/dhcp/dhclient.conf; then
|
||||||
|
echo "prepend domain-name-servers $primary_dns, $secondary_dns;" >> /etc/dhcp/dhclient.conf
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
sed -i "s/^prepend domain-name-servers.*/prepend domain-name-servers $primary_dns;/" /etc/dhcp/dhclient.conf
|
||||||
|
if ! grep -q "prepend domain-name-servers" /etc/dhcp/dhclient.conf; then
|
||||||
|
echo "prepend domain-name-servers $primary_dns;" >> /etc/dhcp/dhclient.conf
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}DNS-Server wurden konfiguriert.${NC}"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
show_main_menu
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Ungültige Option.${NC}"
|
||||||
|
dns_configuration
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# 6) System-Status anzeigen
|
||||||
|
system_status() {
|
||||||
|
print_header
|
||||||
|
echo -e "${GREEN}System-Status${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# MYP-Status
|
||||||
|
echo -e "${YELLOW}MYP-Status:${NC}"
|
||||||
|
if is_myp_installed; then
|
||||||
|
echo "MYP ist installiert in /opt/myp"
|
||||||
|
if systemctl is-active --quiet myp.service; then
|
||||||
|
echo -e "MYP-Dienst: ${GREEN}Aktiv${NC}"
|
||||||
|
else
|
||||||
|
echo -e "MYP-Dienst: ${RED}Inaktiv${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "MYP ist ${RED}nicht installiert${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Netzwerkstatus
|
||||||
|
echo -e "${YELLOW}Netzwerkstatus:${NC}"
|
||||||
|
ip -o addr show | awk '$3 == "inet" {print $2 ": " $4}'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# DNS-Server
|
||||||
|
echo -e "${YELLOW}DNS-Server:${NC}"
|
||||||
|
grep "nameserver" /etc/resolv.conf 2>/dev/null || echo "Keine DNS-Server konfiguriert."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Systemressourcen
|
||||||
|
echo -e "${YELLOW}Systemressourcen:${NC}"
|
||||||
|
echo "CPU-Auslastung:"
|
||||||
|
top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "% genutzt"}'
|
||||||
|
echo "Speichernutzung:"
|
||||||
|
free -h | grep "Mem:" | awk '{print $3 " von " $2 " genutzt"}'
|
||||||
|
echo "Festplattenbelegung:"
|
||||||
|
df -h / | grep -v "Filesystem" | awk '{print $3 " von " $2 " genutzt (" $5 ")"}'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# 7) MYP-Dienst starten/stoppen/neustarten
|
||||||
|
service_management() {
|
||||||
|
print_header
|
||||||
|
echo -e "${GREEN}MYP-Dienst verwalten${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if ! is_myp_installed; then
|
||||||
|
echo -e "${YELLOW}MYP ist nicht installiert.${NC}"
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aktuellen Dienststatus anzeigen
|
||||||
|
echo -e "${YELLOW}Aktueller Status:${NC}"
|
||||||
|
if systemctl is-active --quiet myp.service; then
|
||||||
|
echo -e "MYP-Dienst: ${GREEN}Aktiv${NC}"
|
||||||
|
else
|
||||||
|
echo -e "MYP-Dienst: ${RED}Inaktiv${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "1) MYP-Dienst starten"
|
||||||
|
echo "2) MYP-Dienst stoppen"
|
||||||
|
echo "3) MYP-Dienst neustarten"
|
||||||
|
echo "4) Zurück zum Hauptmenü"
|
||||||
|
echo ""
|
||||||
|
read -p "Ihre Auswahl: " service_option
|
||||||
|
|
||||||
|
case $service_option in
|
||||||
|
1)
|
||||||
|
echo "Starte MYP-Dienst..."
|
||||||
|
systemctl start myp.service
|
||||||
|
echo -e "${GREEN}MYP-Dienst wurde gestartet.${NC}"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo "Stoppe MYP-Dienst..."
|
||||||
|
systemctl stop myp.service
|
||||||
|
echo -e "${GREEN}MYP-Dienst wurde gestoppt.${NC}"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo "Starte MYP-Dienst neu..."
|
||||||
|
systemctl restart myp.service
|
||||||
|
echo -e "${GREEN}MYP-Dienst wurde neugestartet.${NC}"
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
show_main_menu
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Ungültige Option.${NC}"
|
||||||
|
service_management
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# 8) Logs anzeigen
|
||||||
|
show_logs() {
|
||||||
|
print_header
|
||||||
|
echo -e "${GREEN}Logs anzeigen${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "1) MYP-Anwendungslogs"
|
||||||
|
echo "2) Systemd-Dienstlogs"
|
||||||
|
echo "3) Watchdog-Logs"
|
||||||
|
echo "4) Zurück zum Hauptmenü"
|
||||||
|
echo ""
|
||||||
|
read -p "Ihre Auswahl: " logs_option
|
||||||
|
|
||||||
|
case $logs_option in
|
||||||
|
1)
|
||||||
|
if [ -f "/opt/myp/myp.log" ]; then
|
||||||
|
echo -e "${YELLOW}MYP-Anwendungslogs (letzte 20 Zeilen):${NC}"
|
||||||
|
tail -n 20 /opt/myp/myp.log
|
||||||
|
else
|
||||||
|
echo -e "${RED}MYP-Logdatei nicht gefunden.${NC}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo -e "${YELLOW}Systemd-Dienstlogs für MYP:${NC}"
|
||||||
|
journalctl -u myp.service -n 20 --no-pager
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
if [ -f "/home/pi/myp-watchdog.log" ]; then
|
||||||
|
echo -e "${YELLOW}Watchdog-Logs (letzte 20 Zeilen):${NC}"
|
||||||
|
tail -n 20 /home/pi/myp-watchdog.log
|
||||||
|
else
|
||||||
|
echo -e "${RED}Watchdog-Logdatei nicht gefunden.${NC}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
show_main_menu
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Ungültige Option.${NC}"
|
||||||
|
show_logs
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# 9) Dokumentation anzeigen
|
||||||
|
show_documentation() {
|
||||||
|
print_header
|
||||||
|
echo -e "${GREEN}Dokumentation${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
|
||||||
|
echo "Verfügbare Dokumentation:"
|
||||||
|
echo "1) Allgemeine README"
|
||||||
|
echo "2) Kiosk-Modus Anleitung"
|
||||||
|
echo "3) Fehlerbehebung"
|
||||||
|
echo "4) Entwicklungsplan"
|
||||||
|
echo "5) Zurück zum Hauptmenü"
|
||||||
|
echo ""
|
||||||
|
read -p "Ihre Auswahl: " doc_option
|
||||||
|
|
||||||
|
case $doc_option in
|
||||||
|
1)
|
||||||
|
if [ -f "$SCRIPT_DIR/docs/README.md" ]; then
|
||||||
|
if command -v less &>/dev/null; then
|
||||||
|
less "$SCRIPT_DIR/docs/README.md"
|
||||||
|
else
|
||||||
|
cat "$SCRIPT_DIR/docs/README.md"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}README nicht gefunden.${NC}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
if [ -f "$SCRIPT_DIR/docs/KIOSK-SETUP.md" ]; then
|
||||||
|
if command -v less &>/dev/null; then
|
||||||
|
less "$SCRIPT_DIR/docs/KIOSK-SETUP.md"
|
||||||
|
else
|
||||||
|
cat "$SCRIPT_DIR/docs/KIOSK-SETUP.md"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}Kiosk-Anleitung nicht gefunden.${NC}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
if [ -f "$SCRIPT_DIR/docs/COMMON_ERRORS.md" ]; then
|
||||||
|
if command -v less &>/dev/null; then
|
||||||
|
less "$SCRIPT_DIR/docs/COMMON_ERRORS.md"
|
||||||
|
else
|
||||||
|
cat "$SCRIPT_DIR/docs/COMMON_ERRORS.md"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}Fehlerbehebungsdokumentation nicht gefunden.${NC}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
if [ -f "$SCRIPT_DIR/docs/ROADMAP.md" ]; then
|
||||||
|
if command -v less &>/dev/null; then
|
||||||
|
less "$SCRIPT_DIR/docs/ROADMAP.md"
|
||||||
|
else
|
||||||
|
cat "$SCRIPT_DIR/docs/ROADMAP.md"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}Entwicklungsplan nicht gefunden.${NC}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
show_main_menu
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Ungültige Option.${NC}"
|
||||||
|
show_documentation
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..."
|
||||||
|
show_main_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hauptprogramm
|
||||||
|
check_root
|
||||||
|
show_main_menu
|
@ -1,372 +0,0 @@
|
|||||||
# MYP Backend - Windows PowerShell Server Start
|
|
||||||
# Startet den Backend-Server vollstaendig unabhaengig vom Frontend
|
|
||||||
|
|
||||||
param(
|
|
||||||
[switch]$Production,
|
|
||||||
[switch]$Development,
|
|
||||||
[switch]$Logs,
|
|
||||||
[switch]$Help
|
|
||||||
)
|
|
||||||
|
|
||||||
# Farben fuer PowerShell
|
|
||||||
$Red = "Red"
|
|
||||||
$Green = "Green"
|
|
||||||
$Yellow = "Yellow"
|
|
||||||
$Blue = "Cyan"
|
|
||||||
|
|
||||||
function Write-Log {
|
|
||||||
param([string]$Message, [string]$Color = "White")
|
|
||||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
||||||
Write-Host "[$timestamp] $Message" -ForegroundColor $Color
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Success {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Log "SUCCESS: $Message" -Color $Green
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Warning {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Log "WARNING: $Message" -Color $Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Error {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Log "FEHLER: $Message" -Color $Red
|
|
||||||
}
|
|
||||||
|
|
||||||
# Banner
|
|
||||||
Write-Host "========================================" -ForegroundColor $Blue
|
|
||||||
Write-Host "MYP Backend - Standalone Server Start" -ForegroundColor $Blue
|
|
||||||
Write-Host "========================================" -ForegroundColor $Blue
|
|
||||||
|
|
||||||
if ($Help) {
|
|
||||||
Write-Host "Verwendung: .\start-backend-server.ps1 [OPTIONEN]"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "OPTIONEN:"
|
|
||||||
Write-Host " -Production Produktionsmodus (Gunicorn)"
|
|
||||||
Write-Host " -Development Entwicklungsmodus (Flask Dev Server)"
|
|
||||||
Write-Host " -Logs Zeige Live-Logs"
|
|
||||||
Write-Host " -Help Zeige diese Hilfe"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "BEISPIELE:"
|
|
||||||
Write-Host " .\start-backend-server.ps1 -Development"
|
|
||||||
Write-Host " .\start-backend-server.ps1 -Production"
|
|
||||||
Write-Host " .\start-backend-server.ps1 -Development -Logs"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bestimme Ausfuehrungsmodus
|
|
||||||
$RunMode = "development"
|
|
||||||
if ($Production) {
|
|
||||||
$RunMode = "production"
|
|
||||||
Write-Log "Produktionsmodus aktiviert" -Color $Blue
|
|
||||||
} elseif ($Development) {
|
|
||||||
$RunMode = "development"
|
|
||||||
Write-Log "Entwicklungsmodus aktiviert" -Color $Blue
|
|
||||||
} else {
|
|
||||||
$RunMode = "development"
|
|
||||||
Write-Log "Standard-Entwicklungsmodus aktiviert" -Color $Blue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Arbeitsverzeichnis pruefen
|
|
||||||
$CurrentDir = Get-Location
|
|
||||||
Write-Log "Arbeitsverzeichnis: $CurrentDir"
|
|
||||||
|
|
||||||
if (-not (Test-Path "app.py")) {
|
|
||||||
Write-Error "app.py nicht gefunden! Bitte im Backend-Verzeichnis ausfuehren."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Python-Installation pruefen
|
|
||||||
Write-Log "Pruefe Python-Installation..." -Color $Blue
|
|
||||||
|
|
||||||
try {
|
|
||||||
$PythonVersion = python --version 2>&1
|
|
||||||
Write-Log "Python-Version: $PythonVersion"
|
|
||||||
Write-Success "Python-Installation verifiziert"
|
|
||||||
} catch {
|
|
||||||
Write-Error "Python ist nicht installiert oder nicht im PATH!"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Verbesserte Funktion zum Parsen der Umgebungsvariablen
|
|
||||||
function Set-EnvironmentFromFile {
|
|
||||||
param([string]$FilePath)
|
|
||||||
|
|
||||||
if (-not (Test-Path $FilePath)) {
|
|
||||||
Write-Warning "$FilePath nicht gefunden"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log "Lade Backend-Umgebungsvariablen aus $FilePath..." -Color $Blue
|
|
||||||
|
|
||||||
try {
|
|
||||||
$EnvContent = Get-Content $FilePath -Raw
|
|
||||||
$Lines = $EnvContent -split "`r?`n"
|
|
||||||
|
|
||||||
foreach ($Line in $Lines) {
|
|
||||||
# Ueberspringe leere Zeilen und Kommentare
|
|
||||||
if ([string]::IsNullOrWhiteSpace($Line) -or $Line.TrimStart().StartsWith('#')) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Finde den ersten = Zeichen
|
|
||||||
$EqualIndex = $Line.IndexOf('=')
|
|
||||||
if ($EqualIndex -le 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extrahiere Key und Value
|
|
||||||
$Key = $Line.Substring(0, $EqualIndex).Trim()
|
|
||||||
$Value = $Line.Substring($EqualIndex + 1).Trim()
|
|
||||||
|
|
||||||
# Entferne umgebende Anfuehrungszeichen, falls vorhanden
|
|
||||||
if (($Value.StartsWith('"') -and $Value.EndsWith('"')) -or
|
|
||||||
($Value.StartsWith("'") -and $Value.EndsWith("'"))) {
|
|
||||||
$Value = $Value.Substring(1, $Value.Length - 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Setze Umgebungsvariable
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($Key)) {
|
|
||||||
[Environment]::SetEnvironmentVariable($Key, $Value, "Process")
|
|
||||||
Write-Log "Geladen: $Key" -Color $Blue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ueberschreibe FLASK_ENV mit dem gewaehlten Modus
|
|
||||||
[Environment]::SetEnvironmentVariable("FLASK_ENV", $RunMode, "Process")
|
|
||||||
Write-Success "Umgebungsvariablen erfolgreich geladen"
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
Write-Error "Fehler beim Laden der Umgebungsvariablen: $_"
|
|
||||||
Write-Warning "Verwende Standard-Umgebungsvariablen"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Umgebungsvariablen laden
|
|
||||||
Set-EnvironmentFromFile "env.backend"
|
|
||||||
|
|
||||||
# Notwendige Verzeichnisse erstellen
|
|
||||||
Write-Log "Pruefe Verzeichnisse..." -Color $Blue
|
|
||||||
|
|
||||||
$Directories = @("instance", "logs", "uploads")
|
|
||||||
foreach ($Dir in $Directories) {
|
|
||||||
if (-not (Test-Path $Dir)) {
|
|
||||||
New-Item -ItemType Directory -Path $Dir | Out-Null
|
|
||||||
Write-Log "Verzeichnis erstellt: $Dir"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Dependencies pruefen
|
|
||||||
Write-Log "Pruefe Python-Dependencies..." -Color $Blue
|
|
||||||
|
|
||||||
if (Test-Path "requirements.txt") {
|
|
||||||
try {
|
|
||||||
pip install -r requirements.txt --quiet
|
|
||||||
Write-Success "Dependencies aktualisiert"
|
|
||||||
} catch {
|
|
||||||
Write-Warning "Fehler beim Aktualisieren der Dependencies: $_"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Warning "requirements.txt nicht gefunden"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Datenbank initialisieren
|
|
||||||
Write-Log "Initialisiere Datenbank..." -Color $Blue
|
|
||||||
|
|
||||||
try {
|
|
||||||
$env:FLASK_APP = "app.py"
|
|
||||||
|
|
||||||
# Erstelle temporaere Python-Datei fuer Datenbank-Initialisierung
|
|
||||||
$TempInitFile = "temp_init.py"
|
|
||||||
$InitCode = "from app import create_app, init_db`n"
|
|
||||||
$InitCode += "app = create_app('$RunMode')`n"
|
|
||||||
$InitCode += "with app.app_context():`n"
|
|
||||||
$InitCode += " init_db()`n"
|
|
||||||
$InitCode += " print('Datenbank initialisiert')"
|
|
||||||
|
|
||||||
$InitCode | Out-File -FilePath $TempInitFile -Encoding UTF8
|
|
||||||
python $TempInitFile
|
|
||||||
Remove-Item $TempInitFile -Force
|
|
||||||
|
|
||||||
Write-Success "Datenbank initialisiert"
|
|
||||||
} catch {
|
|
||||||
Write-Error "Fehler bei der Datenbank-Initialisierung: $_"
|
|
||||||
if (Test-Path $TempInitFile) {
|
|
||||||
Remove-Item $TempInitFile -Force
|
|
||||||
}
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Port pruefen
|
|
||||||
$Port = 5000
|
|
||||||
if ($env:PORT) {
|
|
||||||
$Port = [int]$env:PORT
|
|
||||||
}
|
|
||||||
|
|
||||||
$PortInUse = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue
|
|
||||||
if ($PortInUse) {
|
|
||||||
Write-Warning "Port $Port ist bereits belegt!"
|
|
||||||
$Port = 5001
|
|
||||||
Write-Log "Verwende alternativen Port $Port..."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Server starten basierend auf Modus
|
|
||||||
if ($RunMode -eq "production") {
|
|
||||||
Write-Log "Starte Backend-Server im Produktionsmodus..." -Color $Blue
|
|
||||||
|
|
||||||
# Pruefe Gunicorn
|
|
||||||
try {
|
|
||||||
gunicorn --version | Out-Null
|
|
||||||
Write-Log "Verwende Gunicorn fuer Produktionsbetrieb"
|
|
||||||
} catch {
|
|
||||||
Write-Error "Gunicorn ist nicht installiert! Installiere mit: pip install gunicorn"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Gunicorn Konfiguration
|
|
||||||
$Workers = if ($env:WORKERS) { $env:WORKERS } else { 4 }
|
|
||||||
$BindAddress = if ($env:BIND_ADDRESS) { $env:BIND_ADDRESS } else { "0.0.0.0:$Port" }
|
|
||||||
$Timeout = if ($env:TIMEOUT) { $env:TIMEOUT } else { 30 }
|
|
||||||
|
|
||||||
Write-Log "Backend-Server startet auf $BindAddress mit $Workers Workern..."
|
|
||||||
|
|
||||||
# Starte Gunicorn
|
|
||||||
$GunicornCmd = "gunicorn --bind $BindAddress --workers $Workers --timeout $Timeout wsgi:application"
|
|
||||||
|
|
||||||
if ($Logs) {
|
|
||||||
Write-Log "Starte mit Live-Logs..." -Color $Blue
|
|
||||||
Invoke-Expression $GunicornCmd
|
|
||||||
} else {
|
|
||||||
Write-Log "Starte im Hintergrund..." -Color $Blue
|
|
||||||
Start-Process -FilePath "gunicorn" -ArgumentList "--bind", $BindAddress, "--workers", $Workers, "--timeout", $Timeout, "wsgi:application" -NoNewWindow
|
|
||||||
|
|
||||||
# Warte auf Server-Start
|
|
||||||
Write-Log "Warte auf Backend-Service..." -Color $Blue
|
|
||||||
$Counter = 0
|
|
||||||
$TimeoutSeconds = 60
|
|
||||||
|
|
||||||
do {
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
$Counter++
|
|
||||||
|
|
||||||
try {
|
|
||||||
$Response = Invoke-WebRequest -Uri "http://localhost:$Port/monitoring/health/simple" -Method GET -TimeoutSec 5 -UseBasicParsing
|
|
||||||
if ($Response.StatusCode -eq 200) {
|
|
||||||
Write-Success "Backend-Server ist bereit!"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
# Ignoriere Fehler waehrend der Startphase
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Counter % 10 -eq 0) {
|
|
||||||
$StatusMessage = "Warte auf Backend-Service... ($Counter/$TimeoutSeconds Sekunden)"
|
|
||||||
Write-Log $StatusMessage
|
|
||||||
}
|
|
||||||
} while ($Counter -lt $TimeoutSeconds)
|
|
||||||
|
|
||||||
if ($Counter -eq $TimeoutSeconds) {
|
|
||||||
Write-Error "Backend-Service konnte nicht gestartet werden!"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Write-Log "Starte Backend-Server im Entwicklungsmodus..." -Color $Blue
|
|
||||||
|
|
||||||
# Flask-Entwicklungsserver
|
|
||||||
$env:FLASK_APP = "app.py"
|
|
||||||
$env:FLASK_ENV = "development"
|
|
||||||
$env:FLASK_DEBUG = "1"
|
|
||||||
|
|
||||||
Write-Log "Backend-Server startet auf Port $Port..."
|
|
||||||
|
|
||||||
if ($Logs) {
|
|
||||||
Write-Log "Starte mit Live-Logs..." -Color $Blue
|
|
||||||
python -m flask run --host=0.0.0.0 --port=$Port
|
|
||||||
} else {
|
|
||||||
Write-Log "Starte im Hintergrund..." -Color $Blue
|
|
||||||
$FlaskProcess = Start-Process -FilePath "python" -ArgumentList "-m", "flask", "run", "--host=0.0.0.0", "--port=$Port" -NoNewWindow -PassThru
|
|
||||||
|
|
||||||
# Warte auf Server-Start
|
|
||||||
Write-Log "Warte auf Backend-Service..." -Color $Blue
|
|
||||||
$Counter = 0
|
|
||||||
$TimeoutSeconds = 60
|
|
||||||
|
|
||||||
do {
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
$Counter++
|
|
||||||
|
|
||||||
try {
|
|
||||||
$Response = Invoke-WebRequest -Uri "http://localhost:$Port/monitoring/health/simple" -Method GET -TimeoutSec 5 -UseBasicParsing
|
|
||||||
if ($Response.StatusCode -eq 200) {
|
|
||||||
Write-Success "Backend-Server ist bereit!"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
# Ignoriere Fehler waehrend der Startphase
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Counter % 10 -eq 0) {
|
|
||||||
$StatusMessage = "Warte auf Backend-Service... ($Counter/$TimeoutSeconds Sekunden)"
|
|
||||||
Write-Log $StatusMessage
|
|
||||||
}
|
|
||||||
} while ($Counter -lt $TimeoutSeconds)
|
|
||||||
|
|
||||||
if ($Counter -eq $TimeoutSeconds) {
|
|
||||||
Write-Error "Backend-Service konnte nicht gestartet werden!"
|
|
||||||
if ($FlaskProcess -and !$FlaskProcess.HasExited) {
|
|
||||||
$FlaskProcess.Kill()
|
|
||||||
}
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# URLs anzeigen
|
|
||||||
Write-Host ""
|
|
||||||
Write-Success "Backend-Server erfolgreich gestartet!"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Backend-API: http://localhost:$Port" -ForegroundColor $Green
|
|
||||||
Write-Host "Backend-Health: http://localhost:$Port/monitoring/health/simple" -ForegroundColor $Green
|
|
||||||
Write-Host "Backend-Test: http://localhost:$Port/api/test" -ForegroundColor $Green
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
if ($RunMode -eq "development") {
|
|
||||||
Write-Host "Entwicklungsmodus aktiv:" -ForegroundColor $Blue
|
|
||||||
Write-Host "- Debug-Modus ist aktiviert" -ForegroundColor $White
|
|
||||||
Write-Host "- Automatisches Neuladen bei Aenderungen" -ForegroundColor $White
|
|
||||||
Write-Host "- Detaillierte Fehlermeldungen" -ForegroundColor $White
|
|
||||||
} else {
|
|
||||||
Write-Host "Produktionsmodus aktiv:" -ForegroundColor $Blue
|
|
||||||
Write-Host "- Gunicorn mit $Workers Workern" -ForegroundColor $White
|
|
||||||
Write-Host "- Optimiert fuer Performance" -ForegroundColor $White
|
|
||||||
Write-Host "- Logging aktiviert" -ForegroundColor $White
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# Logs anzeigen (optional)
|
|
||||||
if ($Logs -and $RunMode -eq "development") {
|
|
||||||
Write-Log "Zeige Backend-Logs (Strg+C zum Beenden):" -Color $Blue
|
|
||||||
if (Test-Path "logs\myp.log") {
|
|
||||||
Get-Content "logs\myp.log" -Wait
|
|
||||||
} else {
|
|
||||||
Write-Warning "Keine Log-Datei gefunden"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$Logs) {
|
|
||||||
Write-Log "Verwende Strg+C um den Server zu stoppen" -Color $Blue
|
|
||||||
|
|
||||||
# Warte auf Benutzereingabe
|
|
||||||
try {
|
|
||||||
while ($true) {
|
|
||||||
Start-Sleep -Seconds 1
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Log "Server wird beendet..." -Color $Yellow
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 🚀 MYP Backend - Entwicklungs-Startskript
|
|
||||||
# Startet den Backend-Server mit hardgecodeter Konfiguration
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=== MYP Backend - Entwicklungsstart ==="
|
|
||||||
|
|
||||||
# Wechsel ins Backend-Verzeichnis
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
# Hardgecodete Umgebungsvariablen setzen
|
|
||||||
export FLASK_APP=app.py
|
|
||||||
export FLASK_ENV=development
|
|
||||||
export SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F
|
|
||||||
export DATABASE_PATH=instance/myp.db
|
|
||||||
export LOG_LEVEL=INFO
|
|
||||||
export JOB_CHECK_INTERVAL=60
|
|
||||||
export SOCKET_CHECK_INTERVAL=120
|
|
||||||
export PRINTERS='{"Drucker 1": {"ip": "192.168.0.100"}, "Drucker 2": {"ip": "192.168.0.101"}, "Drucker 3": {"ip": "192.168.0.102"}, "Drucker 4": {"ip": "192.168.0.103"}, "Drucker 5": {"ip": "192.168.0.104"}, "Drucker 6": {"ip": "192.168.0.106"}}'
|
|
||||||
export TAPO_USERNAME=till.tomczak@mercedes-benz.com
|
|
||||||
export TAPO_PASSWORD=744563017196A
|
|
||||||
export HOST=0.0.0.0
|
|
||||||
export PORT=5000
|
|
||||||
export BACKEND_URL=http://localhost:5000
|
|
||||||
export UPLOAD_FOLDER=uploads
|
|
||||||
export MAX_CONTENT_LENGTH=16777216
|
|
||||||
export DEBUG=true
|
|
||||||
export TESTING=false
|
|
||||||
export DEVELOPMENT=true
|
|
||||||
export PYTHONPATH=${PYTHONPATH}:$(pwd)
|
|
||||||
|
|
||||||
# Prüfe Entwicklungsmodus Parameter
|
|
||||||
if [ "$1" = "--production" ]; then
|
|
||||||
export FLASK_ENV=production
|
|
||||||
export DEBUG=false
|
|
||||||
export DEVELOPMENT=false
|
|
||||||
echo "🚀 Produktions-Modus aktiviert"
|
|
||||||
elif [ "$1" = "--debug" ]; then
|
|
||||||
export FLASK_ENV=development
|
|
||||||
export DEBUG=true
|
|
||||||
export DEVELOPMENT=true
|
|
||||||
echo "🔧 Debug-Modus aktiviert"
|
|
||||||
else
|
|
||||||
echo "🔧 Standard-Entwicklungsmodus"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Konfiguration:"
|
|
||||||
echo " - Flask Environment: $FLASK_ENV"
|
|
||||||
echo " - Debug Modus: $DEBUG"
|
|
||||||
echo " - Host: $HOST"
|
|
||||||
echo " - Port: $PORT"
|
|
||||||
echo " - Database: $DATABASE_PATH"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Erstelle notwendige Verzeichnisse
|
|
||||||
mkdir -p instance logs uploads
|
|
||||||
|
|
||||||
# Aktiviere virtuelle Umgebung falls vorhanden
|
|
||||||
if [ -d "venv" ]; then
|
|
||||||
echo "Aktiviere virtuelle Umgebung..."
|
|
||||||
source venv/bin/activate
|
|
||||||
else
|
|
||||||
echo "WARNUNG: Keine virtuelle Umgebung gefunden. Verwende System-Python."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prüfe ob App-Datei existiert
|
|
||||||
if [ ! -f "app.py" ]; then
|
|
||||||
echo "FEHLER: app.py nicht gefunden!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Starte Flask Development Server
|
|
||||||
echo "Starte Flask Development Server..."
|
|
||||||
echo "Backend verfügbar unter: http://localhost:$PORT"
|
|
||||||
echo "Health-Check: http://localhost:$PORT/health"
|
|
||||||
echo "API-Test: http://localhost:$PORT/api/test"
|
|
||||||
echo ""
|
|
||||||
echo "Zum Stoppen: Ctrl+C"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Starte den Server
|
|
||||||
if [ "$FLASK_ENV" = "production" ]; then
|
|
||||||
# Produktionsmodus mit Gunicorn
|
|
||||||
echo "Starte mit Gunicorn (Produktionsmodus)..."
|
|
||||||
exec gunicorn \
|
|
||||||
--bind=$HOST:$PORT \
|
|
||||||
--workers=2 \
|
|
||||||
--worker-class=sync \
|
|
||||||
--timeout=30 \
|
|
||||||
--keep-alive=5 \
|
|
||||||
--reload \
|
|
||||||
--access-logfile=logs/access.log \
|
|
||||||
--error-logfile=logs/error.log \
|
|
||||||
--log-level=info \
|
|
||||||
wsgi:application
|
|
||||||
else
|
|
||||||
# Entwicklungsmodus mit Flask
|
|
||||||
echo "Starte mit Flask (Entwicklungsmodus)..."
|
|
||||||
exec python3 -m flask run --host=$HOST --port=$PORT --debug
|
|
||||||
fi
|
|
@ -1,36 +0,0 @@
|
|||||||
@echo off
|
|
||||||
setlocal enabledelayedexpansion
|
|
||||||
|
|
||||||
echo [%date% %time%] Starte Backend-Debug-Server...
|
|
||||||
|
|
||||||
REM Pfad zum Debug-Server ermitteln
|
|
||||||
set "SCRIPT_DIR=%~dp0"
|
|
||||||
set "DEBUG_SERVER_DIR=%SCRIPT_DIR%debug-server"
|
|
||||||
|
|
||||||
REM Prüfe, ob das Debug-Server-Verzeichnis existiert
|
|
||||||
if not exist "%DEBUG_SERVER_DIR%" (
|
|
||||||
echo [%date% %time%] FEHLER: Debug-Server-Verzeichnis nicht gefunden: %DEBUG_SERVER_DIR%
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Prüfe, ob Python installiert ist
|
|
||||||
where python >nul 2>&1
|
|
||||||
if %ERRORLEVEL% neq 0 (
|
|
||||||
echo [%date% %time%] FEHLER: Python nicht gefunden. Bitte installieren Sie Python.
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Wechsle ins Debug-Server-Verzeichnis
|
|
||||||
cd "%DEBUG_SERVER_DIR%"
|
|
||||||
|
|
||||||
REM Installiere Abhängigkeiten, falls nötig
|
|
||||||
if exist "requirements.txt" (
|
|
||||||
echo [%date% %time%] Installiere Abhängigkeiten...
|
|
||||||
pip install -r requirements.txt
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Starte den Debug-Server
|
|
||||||
echo [%date% %time%] Starte Backend-Debug-Server...
|
|
||||||
python app.py
|
|
||||||
|
|
||||||
endlocal
|
|
@ -1,84 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 🔧 MYP Backend - Debug-Startskript
|
|
||||||
# Startet den Backend-Server im Debug-Modus mit maximaler Verbosity
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=== MYP Backend - Debug-Modus ==="
|
|
||||||
|
|
||||||
# Wechsel ins Backend-Verzeichnis
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
# Hardgecodete Debug-Umgebungsvariablen
|
|
||||||
export FLASK_APP=app.py
|
|
||||||
export FLASK_ENV=development
|
|
||||||
export FLASK_DEBUG=1
|
|
||||||
export SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F
|
|
||||||
export DATABASE_PATH=instance/myp.db
|
|
||||||
export LOG_LEVEL=DEBUG
|
|
||||||
export JOB_CHECK_INTERVAL=30
|
|
||||||
export SOCKET_CHECK_INTERVAL=60
|
|
||||||
export PRINTERS='{"Drucker 1": {"ip": "192.168.0.100"}, "Drucker 2": {"ip": "192.168.0.101"}, "Drucker 3": {"ip": "192.168.0.102"}, "Drucker 4": {"ip": "192.168.0.103"}, "Drucker 5": {"ip": "192.168.0.104"}, "Drucker 6": {"ip": "192.168.0.106"}}'
|
|
||||||
export TAPO_USERNAME=till.tomczak@mercedes-benz.com
|
|
||||||
export TAPO_PASSWORD=744563017196A
|
|
||||||
export HOST=0.0.0.0
|
|
||||||
export PORT=5000
|
|
||||||
export BACKEND_URL=http://localhost:5000
|
|
||||||
export UPLOAD_FOLDER=uploads
|
|
||||||
export MAX_CONTENT_LENGTH=16777216
|
|
||||||
export DEBUG=true
|
|
||||||
export TESTING=false
|
|
||||||
export DEVELOPMENT=true
|
|
||||||
export PYTHONPATH=${PYTHONPATH}:$(pwd)
|
|
||||||
|
|
||||||
echo "🔧 Debug-Konfiguration:"
|
|
||||||
echo " - Flask Environment: $FLASK_ENV"
|
|
||||||
echo " - Debug Modus: $FLASK_DEBUG"
|
|
||||||
echo " - Log Level: $LOG_LEVEL"
|
|
||||||
echo " - Host: $HOST"
|
|
||||||
echo " - Port: $PORT"
|
|
||||||
echo " - Database: $DATABASE_PATH"
|
|
||||||
echo " - Job Check Interval: $JOB_CHECK_INTERVAL Sekunden"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Erstelle notwendige Verzeichnisse
|
|
||||||
mkdir -p instance logs uploads
|
|
||||||
|
|
||||||
# Aktiviere virtuelle Umgebung falls vorhanden
|
|
||||||
if [ -d "venv" ]; then
|
|
||||||
echo "Aktiviere virtuelle Umgebung..."
|
|
||||||
source venv/bin/activate
|
|
||||||
else
|
|
||||||
echo "WARNUNG: Keine virtuelle Umgebung gefunden. Verwende System-Python."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prüfe ob App-Datei existiert
|
|
||||||
if [ ! -f "app.py" ]; then
|
|
||||||
echo "FEHLER: app.py nicht gefunden!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Starte Flask im Debug-Modus
|
|
||||||
echo "🚀 Starte Flask Debug-Server..."
|
|
||||||
echo "Backend verfügbar unter: http://localhost:$PORT"
|
|
||||||
echo "Health-Check: http://localhost:$PORT/health"
|
|
||||||
echo "API-Test: http://localhost:$PORT/api/test"
|
|
||||||
echo ""
|
|
||||||
echo "🔧 Debug-Features aktiviert:"
|
|
||||||
echo " - Auto-Reload bei Code-Änderungen"
|
|
||||||
echo " - Detaillierte Error-Pages"
|
|
||||||
echo " - Erweiterte Logging-Ausgabe"
|
|
||||||
echo " - Schnellere Job-Check-Intervalle"
|
|
||||||
echo ""
|
|
||||||
echo "Zum Stoppen: Ctrl+C"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Starte Flask mit Debug-Optionen
|
|
||||||
exec python3 -m flask run \
|
|
||||||
--host=$HOST \
|
|
||||||
--port=$PORT \
|
|
||||||
--debug \
|
|
||||||
--reload \
|
|
||||||
--debugger
|
|
@ -1,38 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM MYP Backend - Produktions-Startskript für Windows
|
|
||||||
REM Startet die Flask-Anwendung mit Waitress für den Produktionsbetrieb
|
|
||||||
|
|
||||||
echo === MYP Backend - Produktionsstart ===
|
|
||||||
|
|
||||||
REM Konfiguration
|
|
||||||
if not defined WORKERS set WORKERS=4
|
|
||||||
if not defined BIND_ADDRESS set BIND_ADDRESS=0.0.0.0
|
|
||||||
if not defined PORT set PORT=5000
|
|
||||||
if not defined THREADS set THREADS=6
|
|
||||||
|
|
||||||
REM Umgebungsvariablen
|
|
||||||
set FLASK_ENV=production
|
|
||||||
set PYTHONPATH=%PYTHONPATH%;%cd%
|
|
||||||
|
|
||||||
REM Log-Verzeichnis erstellen
|
|
||||||
if not exist logs mkdir logs
|
|
||||||
|
|
||||||
REM Prüfe, ob SECRET_KEY gesetzt ist
|
|
||||||
if not defined SECRET_KEY (
|
|
||||||
echo WARNUNG: SECRET_KEY ist nicht gesetzt. Verwende einen generierten Schlüssel.
|
|
||||||
for /f %%i in ('python -c "import secrets; print(secrets.token_hex(32))"') do set SECRET_KEY=%%i
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Produktionsparameter ausgeben
|
|
||||||
echo Konfiguration:
|
|
||||||
echo - Host: %BIND_ADDRESS%
|
|
||||||
echo - Port: %PORT%
|
|
||||||
echo - Threads: %THREADS%
|
|
||||||
echo - Environment: %FLASK_ENV%
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM Waitress starten (besser für Windows als Gunicorn)
|
|
||||||
echo Starte Waitress-Server...
|
|
||||||
python -m waitress --host=%BIND_ADDRESS% --port=%PORT% --threads=%THREADS% --call wsgi:application
|
|
||||||
|
|
||||||
pause
|
|
@ -1,81 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# MYP Backend - Produktions-Startskript
|
|
||||||
# Startet die Flask-Anwendung mit Gunicorn für den Produktionsbetrieb
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=== MYP Backend - Produktionsstart ==="
|
|
||||||
|
|
||||||
# Wechsel ins Backend-Verzeichnis
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
# Hardgecodete Produktions-Umgebungsvariablen
|
|
||||||
export FLASK_APP=app.py
|
|
||||||
export FLASK_ENV=production
|
|
||||||
export SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F
|
|
||||||
export DATABASE_PATH=instance/myp.db
|
|
||||||
export LOG_LEVEL=INFO
|
|
||||||
export JOB_CHECK_INTERVAL=60
|
|
||||||
export SOCKET_CHECK_INTERVAL=120
|
|
||||||
export PRINTERS='{"Drucker 1": {"ip": "192.168.0.100"}, "Drucker 2": {"ip": "192.168.0.101"}, "Drucker 3": {"ip": "192.168.0.102"}, "Drucker 4": {"ip": "192.168.0.103"}, "Drucker 5": {"ip": "192.168.0.104"}, "Drucker 6": {"ip": "192.168.0.106"}}'
|
|
||||||
export TAPO_USERNAME=till.tomczak@mercedes-benz.com
|
|
||||||
export TAPO_PASSWORD=744563017196A
|
|
||||||
export HOST=0.0.0.0
|
|
||||||
export PORT=5000
|
|
||||||
export BACKEND_URL=http://localhost:5000
|
|
||||||
export UPLOAD_FOLDER=uploads
|
|
||||||
export MAX_CONTENT_LENGTH=16777216
|
|
||||||
export DEBUG=false
|
|
||||||
export TESTING=false
|
|
||||||
export DEVELOPMENT=false
|
|
||||||
export PYTHONPATH=${PYTHONPATH}:$(pwd)
|
|
||||||
|
|
||||||
# Konfiguration
|
|
||||||
WORKERS=${WORKERS:-4}
|
|
||||||
BIND_ADDRESS=${BIND_ADDRESS:-"0.0.0.0:5000"}
|
|
||||||
TIMEOUT=${TIMEOUT:-30}
|
|
||||||
KEEP_ALIVE=${KEEP_ALIVE:-5}
|
|
||||||
MAX_REQUESTS=${MAX_REQUESTS:-1000}
|
|
||||||
MAX_REQUESTS_JITTER=${MAX_REQUESTS_JITTER:-100}
|
|
||||||
|
|
||||||
# Log-Verzeichnis erstellen
|
|
||||||
mkdir -p logs
|
|
||||||
|
|
||||||
# Produktionsparameter ausgeben
|
|
||||||
echo "Konfiguration:"
|
|
||||||
echo " - Workers: $WORKERS"
|
|
||||||
echo " - Bind: $BIND_ADDRESS"
|
|
||||||
echo " - Timeout: $TIMEOUT Sekunden"
|
|
||||||
echo " - Max Requests: $MAX_REQUESTS"
|
|
||||||
echo " - Environment: $FLASK_ENV"
|
|
||||||
echo " - Database: $DATABASE_PATH"
|
|
||||||
echo " - Log Level: $LOG_LEVEL"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Aktiviere virtuelle Umgebung falls vorhanden
|
|
||||||
if [ -d "venv" ]; then
|
|
||||||
echo "Aktiviere virtuelle Umgebung..."
|
|
||||||
source venv/bin/activate
|
|
||||||
else
|
|
||||||
echo "WARNUNG: Keine virtuelle Umgebung gefunden. Verwende System-Python."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Gunicorn starten
|
|
||||||
echo "Starte Gunicorn-Server..."
|
|
||||||
exec gunicorn \
|
|
||||||
--workers=$WORKERS \
|
|
||||||
--worker-class=sync \
|
|
||||||
--bind=$BIND_ADDRESS \
|
|
||||||
--timeout=$TIMEOUT \
|
|
||||||
--keep-alive=$KEEP_ALIVE \
|
|
||||||
--max-requests=$MAX_REQUESTS \
|
|
||||||
--max-requests-jitter=$MAX_REQUESTS_JITTER \
|
|
||||||
--preload \
|
|
||||||
--access-logfile=logs/access.log \
|
|
||||||
--error-logfile=logs/error.log \
|
|
||||||
--log-level=info \
|
|
||||||
--capture-output \
|
|
||||||
--enable-stdio-inheritance \
|
|
||||||
wsgi:application
|
|
12068
backend/static/css/bootstrap.css
vendored
12068
backend/static/css/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
6314
backend/static/js/bootstrap.bundle.js
vendored
6314
backend/static/js/bootstrap.bundle.js
vendored
File diff suppressed because it is too large
Load Diff
@ -1,169 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{% block title %}MYP API Tester{% endblock %}</title>
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.css') }}">
|
|
||||||
<style>
|
|
||||||
.sidebar {
|
|
||||||
min-height: calc(100vh - 56px);
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
.api-response {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: monospace;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.nav-link.active {
|
|
||||||
background-color: #0d6efd;
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand" href="/">MYP API Tester</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if active_page == 'home' %}active{% endif %}" href="/">Home</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if active_page == 'printers' %}active{% endif %}" href="/admin/printers">Drucker</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if active_page == 'jobs' %}active{% endif %}" href="/admin/jobs">Druckaufträge</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if active_page == 'users' %}active{% endif %}" href="/admin/users">Benutzer</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link {% if active_page == 'stats' %}active{% endif %}" href="/admin/stats">Statistiken</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="navbar-nav ms-auto">
|
|
||||||
{% if current_user %}
|
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
|
|
||||||
{{ current_user.username }}
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
|
||||||
<li><a class="dropdown-item" href="/logout">Abmelden</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/login">Anmelden</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container-fluid py-3">
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
||||||
{% if messages %}
|
|
||||||
{% for category, message in messages %}
|
|
||||||
<div class="alert alert-{{ category }}" role="alert">
|
|
||||||
{{ message }}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/bootstrap.bundle.js') }}"></script>
|
|
||||||
<script>
|
|
||||||
function formatJson(jsonString) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(jsonString);
|
|
||||||
return JSON.stringify(obj, null, 2);
|
|
||||||
} catch (e) {
|
|
||||||
return jsonString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Format all response areas
|
|
||||||
document.querySelectorAll('.api-response').forEach(function(el) {
|
|
||||||
if (el.textContent) {
|
|
||||||
el.textContent = formatJson(el.textContent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add event listener to show response areas
|
|
||||||
document.querySelectorAll('.api-form').forEach(function(form) {
|
|
||||||
form.addEventListener('submit', async function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const url = this.getAttribute('data-url');
|
|
||||||
const method = this.getAttribute('data-method') || 'GET';
|
|
||||||
const responseArea = document.getElementById(this.getAttribute('data-response'));
|
|
||||||
const formData = new FormData(this);
|
|
||||||
const data = {};
|
|
||||||
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
if (value) {
|
|
||||||
try {
|
|
||||||
// Try to parse as JSON if it looks like JSON
|
|
||||||
if (value.trim().startsWith('{') || value.trim().startsWith('[')) {
|
|
||||||
data[key] = JSON.parse(value);
|
|
||||||
} else {
|
|
||||||
data[key] = value;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
data[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
method: method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (method !== 'GET' && method !== 'HEAD') {
|
|
||||||
options.body = JSON.stringify(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
responseArea.textContent = 'Sending request...';
|
|
||||||
const response = await fetch(url, options);
|
|
||||||
const responseText = await response.text();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formatted = formatJson(responseText);
|
|
||||||
responseArea.textContent = formatted;
|
|
||||||
} catch (e) {
|
|
||||||
responseArea.textContent = responseText;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasAttribute('data-reload') && response.ok) {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
responseArea.textContent = 'Error: ' + err.message;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% block scripts %}{% endblock %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,304 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Dashboard - MYP API Tester{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="mb-0">Willkommen, {{ current_user.display_name }}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>Benutzerdetails:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>ID:</strong> {{ current_user.id }}</li>
|
|
||||||
<li><strong>Benutzername:</strong> {{ current_user.username }}</li>
|
|
||||||
<li><strong>E-Mail:</strong> {{ current_user.email or "Nicht angegeben" }}</li>
|
|
||||||
<li><strong>Rolle:</strong> {{ current_user.role }}</li>
|
|
||||||
</ul>
|
|
||||||
<div class="mt-3">
|
|
||||||
<a href="/admin/printers" class="btn btn-primary me-2">Drucker verwalten</a>
|
|
||||||
<a href="/admin/jobs" class="btn btn-success me-2">Druckaufträge verwalten</a>
|
|
||||||
{% if current_user.role == 'admin' %}
|
|
||||||
<a href="/admin/users" class="btn btn-info me-2">Benutzer verwalten</a>
|
|
||||||
<a href="/admin/stats" class="btn btn-secondary">Statistiken</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Aktive Druckaufträge</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="api-form mb-3" data-url="/api/jobs" data-method="GET" data-response="jobsResponse">
|
|
||||||
<button type="submit" class="btn btn-primary">Aktualisieren</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="activeJobsContainer">
|
|
||||||
<div class="alert alert-info">Lade Druckaufträge...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-none">
|
|
||||||
<h6>API-Antwort:</h6>
|
|
||||||
<pre class="api-response" id="jobsResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Verfügbare Drucker</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="api-form mb-3" data-url="/api/printers" data-method="GET" data-response="printersResponse">
|
|
||||||
<button type="submit" class="btn btn-primary">Aktualisieren</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="availablePrintersContainer">
|
|
||||||
<div class="alert alert-info">Lade Drucker...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-none">
|
|
||||||
<h6>API-Antwort:</h6>
|
|
||||||
<pre class="api-response" id="printersResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Job freischalten Modal -->
|
|
||||||
<div class="modal fade" id="approveJobModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Druckauftrag freischalten</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Möchten Sie diesen Druckauftrag jetzt freischalten und starten?</p>
|
|
||||||
<p><strong>Hinweis:</strong> Der Drucker muss verfügbar sein, damit der Auftrag gestartet werden kann.</p>
|
|
||||||
<form id="approveJobForm" class="api-form" data-method="POST" data-response="approveJobResponse" data-reload="true">
|
|
||||||
<input type="hidden" id="approveJobId" name="jobId">
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="approveJobResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
||||||
<button type="submit" form="approveJobForm" class="btn btn-success">Freischalten</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Aufträge und Drucker laden
|
|
||||||
document.querySelector('form[data-url="/api/jobs"]').dispatchEvent(new Event('submit'));
|
|
||||||
document.querySelector('form[data-url="/api/printers"]').dispatchEvent(new Event('submit'));
|
|
||||||
|
|
||||||
// Tabellen aktualisieren, wenn Daten geladen werden
|
|
||||||
const jobsResponse = document.getElementById('jobsResponse');
|
|
||||||
const printersResponse = document.getElementById('printersResponse');
|
|
||||||
|
|
||||||
// Observer für Jobs
|
|
||||||
const jobsObserver = new MutationObserver(function(mutations) {
|
|
||||||
try {
|
|
||||||
const jobs = JSON.parse(jobsResponse.textContent);
|
|
||||||
updateActiveJobs(jobs);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Parsen der Auftrags-Daten:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
jobsObserver.observe(jobsResponse, { childList: true, characterData: true, subtree: true });
|
|
||||||
|
|
||||||
// Observer für Drucker
|
|
||||||
const printersObserver = new MutationObserver(function(mutations) {
|
|
||||||
try {
|
|
||||||
const printers = JSON.parse(printersResponse.textContent);
|
|
||||||
updateAvailablePrinters(printers);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Parsen der Drucker-Daten:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
printersObserver.observe(printersResponse, { childList: true, characterData: true, subtree: true });
|
|
||||||
|
|
||||||
// Approve-Modal vorbereiten
|
|
||||||
document.getElementById('approveJobModal').addEventListener('show.bs.modal', function(event) {
|
|
||||||
const button = event.relatedTarget;
|
|
||||||
const jobId = button.getAttribute('data-job-id');
|
|
||||||
|
|
||||||
document.getElementById('approveJobId').value = jobId;
|
|
||||||
document.getElementById('approveJobForm').setAttribute('data-url', `/api/jobs/${jobId}/approve`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Automatische Aktualisierung alle 60 Sekunden
|
|
||||||
setInterval(() => {
|
|
||||||
document.querySelector('form[data-url="/api/jobs"]').dispatchEvent(new Event('submit'));
|
|
||||||
document.querySelector('form[data-url="/api/printers"]').dispatchEvent(new Event('submit'));
|
|
||||||
}, 60000);
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateActiveJobs(jobs) {
|
|
||||||
const container = document.getElementById('activeJobsContainer');
|
|
||||||
|
|
||||||
// Filter für aktive und wartende Jobs
|
|
||||||
const activeJobs = jobs.filter(job => !job.aborted && job.remainingMinutes > 0 && !job.waitingApproval);
|
|
||||||
const waitingJobs = jobs.filter(job => !job.aborted && job.waitingApproval);
|
|
||||||
|
|
||||||
if (activeJobs.length === 0 && waitingJobs.length === 0) {
|
|
||||||
container.innerHTML = '<div class="alert alert-info">Keine aktiven Druckaufträge vorhanden.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
// Aktive Jobs anzeigen
|
|
||||||
if (activeJobs.length > 0) {
|
|
||||||
html += '<h6 class="mt-3">Laufende Aufträge</h6>';
|
|
||||||
html += '<div class="list-group mb-3">';
|
|
||||||
|
|
||||||
activeJobs.forEach(job => {
|
|
||||||
// Prozentsatz der abgelaufenen Zeit berechnen
|
|
||||||
const totalDuration = job.durationInMinutes;
|
|
||||||
const elapsed = totalDuration - job.remainingMinutes;
|
|
||||||
const percentage = Math.round((elapsed / totalDuration) * 100);
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div class="list-group-item">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<strong>Job ${job.id.substring(0, 8)}...</strong> (${job.durationInMinutes} Min)
|
|
||||||
<div class="small text-muted">Verbleibend: ${job.remainingMinutes} Min</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-warning">Aktiv</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="progress mt-2" style="height: 10px;">
|
|
||||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: ${percentage}%;"
|
|
||||||
aria-valuenow="${percentage}"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">
|
|
||||||
${percentage}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wartende Jobs anzeigen
|
|
||||||
if (waitingJobs.length > 0) {
|
|
||||||
html += '<h6 class="mt-3">Wartende Aufträge</h6>';
|
|
||||||
html += '<div class="list-group">';
|
|
||||||
|
|
||||||
waitingJobs.forEach(job => {
|
|
||||||
html += `
|
|
||||||
<div class="list-group-item">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<strong>Job ${job.id.substring(0, 8)}...</strong> (${job.durationInMinutes} Min)
|
|
||||||
<div class="small text-muted">Drucker: ${job.socketId.substring(0, 8)}...</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-info">Wartet</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
<button type="button" class="btn btn-sm btn-success"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#approveJobModal"
|
|
||||||
data-job-id="${job.id}">
|
|
||||||
Freischalten
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAvailablePrinters(printers) {
|
|
||||||
const container = document.getElementById('availablePrintersContainer');
|
|
||||||
|
|
||||||
// Filter für verfügbare Drucker
|
|
||||||
const availablePrinters = printers.filter(printer => printer.status === 0);
|
|
||||||
|
|
||||||
if (availablePrinters.length === 0) {
|
|
||||||
container.innerHTML = '<div class="alert alert-warning">Keine verfügbaren Drucker gefunden.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '<div class="list-group">';
|
|
||||||
|
|
||||||
availablePrinters.forEach(printer => {
|
|
||||||
html += `
|
|
||||||
<div class="list-group-item">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<strong>${printer.name}</strong>
|
|
||||||
<div class="small text-muted">${printer.description}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="badge bg-success">Verfügbar</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
<a href="/admin/jobs" class="btn btn-sm btn-primary">Auftrag erstellen</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
container.innerHTML = html;
|
|
||||||
|
|
||||||
// Gesamtstatistik hinzufügen
|
|
||||||
const busyPrinters = printers.filter(printer => printer.status === 1).length;
|
|
||||||
const totalPrinters = printers.length;
|
|
||||||
|
|
||||||
if (totalPrinters > 0) {
|
|
||||||
const statsHtml = `
|
|
||||||
<div class="mt-3">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<small>Verfügbar: ${availablePrinters.length} / ${totalPrinters}</small>
|
|
||||||
<small>Belegt: ${busyPrinters} / ${totalPrinters}</small>
|
|
||||||
</div>
|
|
||||||
<div class="progress mt-1" style="height: 5px;">
|
|
||||||
<div class="progress-bar bg-success" style="width: ${(availablePrinters.length / totalPrinters) * 100}%"></div>
|
|
||||||
<div class="progress-bar bg-warning" style="width: ${(busyPrinters / totalPrinters) * 100}%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
container.innerHTML += statsHtml;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,443 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Druckaufträge - MYP API Tester{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h4 class="mb-0">Druckaufträge verwalten</h4>
|
|
||||||
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newJobForm">
|
|
||||||
Neuen Auftrag erstellen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="collapse" id="newJobForm">
|
|
||||||
<div class="card-body border-bottom">
|
|
||||||
<form class="api-form" data-url="/api/jobs" data-method="POST" data-response="createJobResponse" data-reload="true">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="jobPrinterId" class="form-label">Drucker</label>
|
|
||||||
<select class="form-control" id="jobPrinterId" name="printerId" required>
|
|
||||||
<option value="">Drucker auswählen...</option>
|
|
||||||
<!-- Wird dynamisch gefüllt -->
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="jobDuration" class="form-label">Dauer (Minuten)</label>
|
|
||||||
<input type="number" class="form-control" id="jobDuration" name="durationInMinutes" min="1" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="jobComments" class="form-label">Kommentare</label>
|
|
||||||
<textarea class="form-control" id="jobComments" name="comments" rows="3"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="allowQueuedJobs" name="allowQueuedJobs" value="true">
|
|
||||||
<label class="form-check-label" for="allowQueuedJobs">
|
|
||||||
Auftrag in Warteschlange erlauben (wenn Drucker belegt ist)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-success">Auftrag erstellen</button>
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="createJobResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="api-form mb-3" data-url="/api/jobs" data-method="GET" data-response="jobsResponse">
|
|
||||||
<button type="submit" class="btn btn-primary">Aufträge aktualisieren</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Drucker</th>
|
|
||||||
<th>Benutzer</th>
|
|
||||||
<th>Start</th>
|
|
||||||
<th>Dauer (Min)</th>
|
|
||||||
<th>Verbleibend (Min)</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Kommentare</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="jobsTableBody">
|
|
||||||
<!-- Wird dynamisch gefüllt -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h6>API-Antwort:</h6>
|
|
||||||
<pre class="api-response" id="jobsResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Job abbrechen Modal -->
|
|
||||||
<div class="modal fade" id="abortJobModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Auftrag abbrechen</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Möchten Sie den Auftrag wirklich abbrechen?</p>
|
|
||||||
<form id="abortJobForm" class="api-form" data-method="POST" data-response="abortJobResponse" data-reload="true">
|
|
||||||
<input type="hidden" id="abortJobId" name="jobId">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="abortReason" class="form-label">Abbruchgrund</label>
|
|
||||||
<textarea class="form-control" id="abortReason" name="reason" rows="3"></textarea>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="abortJobResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
||||||
<button type="submit" form="abortJobForm" class="btn btn-danger">Auftrag abbrechen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Job beenden Modal -->
|
|
||||||
<div class="modal fade" id="finishJobModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Auftrag beenden</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Möchten Sie den Auftrag als beendet markieren?</p>
|
|
||||||
<form id="finishJobForm" class="api-form" data-method="POST" data-response="finishJobResponse" data-reload="true">
|
|
||||||
<input type="hidden" id="finishJobId" name="jobId">
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="finishJobResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
||||||
<button type="submit" form="finishJobForm" class="btn btn-success">Auftrag beenden</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Job verlängern Modal -->
|
|
||||||
<div class="modal fade" id="extendJobModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Auftrag verlängern</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="extendJobForm" class="api-form" data-method="POST" data-response="extendJobResponse" data-reload="true">
|
|
||||||
<input type="hidden" id="extendJobId" name="jobId">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="extendHours" class="form-label">Stunden</label>
|
|
||||||
<input type="number" class="form-control" id="extendHours" name="hours" min="0" value="0">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="extendMinutes" class="form-label">Minuten</label>
|
|
||||||
<input type="number" class="form-control" id="extendMinutes" name="minutes" min="0" max="59" value="30">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="extendJobResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
||||||
<button type="submit" form="extendJobForm" class="btn btn-primary">Auftrag verlängern</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Job Kommentare bearbeiten Modal -->
|
|
||||||
<div class="modal fade" id="editCommentsModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Kommentare bearbeiten</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="editCommentsForm" class="api-form" data-method="PUT" data-response="editCommentsResponse" data-reload="true">
|
|
||||||
<input type="hidden" id="editCommentsJobId" name="jobId">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="editJobComments" class="form-label">Kommentare</label>
|
|
||||||
<textarea class="form-control" id="editJobComments" name="comments" rows="3"></textarea>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="editCommentsResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
||||||
<button type="submit" form="editCommentsForm" class="btn btn-primary">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Job freischalten Modal -->
|
|
||||||
<div class="modal fade" id="approveJobModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Druckauftrag freischalten</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Möchten Sie diesen Druckauftrag jetzt freischalten und starten?</p>
|
|
||||||
<p><strong>Hinweis:</strong> Der Drucker muss verfügbar sein, damit der Auftrag gestartet werden kann.</p>
|
|
||||||
<form id="approveJobForm" class="api-form" data-method="POST" data-response="approveJobResponse" data-reload="true">
|
|
||||||
<input type="hidden" id="approveJobId" name="jobId">
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="approveJobResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
||||||
<button type="submit" form="approveJobForm" class="btn btn-success">Freischalten</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Drucker für Dropdown laden
|
|
||||||
loadPrinters();
|
|
||||||
|
|
||||||
// Aufträge laden
|
|
||||||
document.querySelector('form[data-url="/api/jobs"]').dispatchEvent(new Event('submit'));
|
|
||||||
|
|
||||||
// Tabelle aktualisieren, wenn Aufträge geladen werden
|
|
||||||
const jobsResponse = document.getElementById('jobsResponse');
|
|
||||||
const observer = new MutationObserver(function(mutations) {
|
|
||||||
try {
|
|
||||||
const jobs = JSON.parse(jobsResponse.textContent);
|
|
||||||
updateJobsTable(jobs);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Parsen der Auftrags-Daten:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(jobsResponse, { childList: true, characterData: true, subtree: true });
|
|
||||||
|
|
||||||
// Abort-Modal vorbereiten
|
|
||||||
document.getElementById('abortJobModal').addEventListener('show.bs.modal', function(event) {
|
|
||||||
const button = event.relatedTarget;
|
|
||||||
const jobId = button.getAttribute('data-job-id');
|
|
||||||
|
|
||||||
document.getElementById('abortJobId').value = jobId;
|
|
||||||
document.getElementById('abortJobForm').setAttribute('data-url', `/api/jobs/${jobId}/abort`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Finish-Modal vorbereiten
|
|
||||||
document.getElementById('finishJobModal').addEventListener('show.bs.modal', function(event) {
|
|
||||||
const button = event.relatedTarget;
|
|
||||||
const jobId = button.getAttribute('data-job-id');
|
|
||||||
|
|
||||||
document.getElementById('finishJobId').value = jobId;
|
|
||||||
document.getElementById('finishJobForm').setAttribute('data-url', `/api/jobs/${jobId}/finish`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extend-Modal vorbereiten
|
|
||||||
document.getElementById('extendJobModal').addEventListener('show.bs.modal', function(event) {
|
|
||||||
const button = event.relatedTarget;
|
|
||||||
const jobId = button.getAttribute('data-job-id');
|
|
||||||
|
|
||||||
document.getElementById('extendJobId').value = jobId;
|
|
||||||
document.getElementById('extendJobForm').setAttribute('data-url', `/api/jobs/${jobId}/extend`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Edit-Comments-Modal vorbereiten
|
|
||||||
document.getElementById('editCommentsModal').addEventListener('show.bs.modal', function(event) {
|
|
||||||
const button = event.relatedTarget;
|
|
||||||
const jobId = button.getAttribute('data-job-id');
|
|
||||||
const comments = button.getAttribute('data-job-comments');
|
|
||||||
|
|
||||||
document.getElementById('editCommentsJobId').value = jobId;
|
|
||||||
document.getElementById('editCommentsForm').setAttribute('data-url', `/api/jobs/${jobId}/comments`);
|
|
||||||
document.getElementById('editJobComments').value = comments || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Approve-Modal vorbereiten
|
|
||||||
document.getElementById('approveJobModal').addEventListener('show.bs.modal', function(event) {
|
|
||||||
const button = event.relatedTarget;
|
|
||||||
const jobId = button.getAttribute('data-job-id');
|
|
||||||
|
|
||||||
document.getElementById('approveJobId').value = jobId;
|
|
||||||
document.getElementById('approveJobForm').setAttribute('data-url', `/api/jobs/${jobId}/approve`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadPrinters() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/printers');
|
|
||||||
const printers = await response.json();
|
|
||||||
|
|
||||||
const selectElement = document.getElementById('jobPrinterId');
|
|
||||||
selectElement.innerHTML = '<option value="">Drucker auswählen...</option>';
|
|
||||||
|
|
||||||
// Drucker anzeigen (alle, da man jetzt auch für belegte Drucker Jobs erstellen kann)
|
|
||||||
printers.forEach(printer => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = printer.id;
|
|
||||||
|
|
||||||
// Status-Information zum Drucker hinzufügen
|
|
||||||
const statusText = printer.status === 0 ? '(Verfügbar)' : '(Belegt)';
|
|
||||||
option.textContent = `${printer.name} - ${printer.description} ${statusText}`;
|
|
||||||
|
|
||||||
// Belegte Drucker visuell unterscheiden
|
|
||||||
if (printer.status !== 0) {
|
|
||||||
option.classList.add('text-muted');
|
|
||||||
}
|
|
||||||
|
|
||||||
selectElement.appendChild(option);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hinweis auf die Checkbox für Warteschlange anzeigen oder verstecken
|
|
||||||
const allowQueuedJobsCheckbox = document.getElementById('allowQueuedJobs');
|
|
||||||
const queueCheckboxContainer = allowQueuedJobsCheckbox.closest('.form-check');
|
|
||||||
|
|
||||||
// Prüfen, ob es belegte Drucker gibt
|
|
||||||
const hasBusyPrinters = printers.some(printer => printer.status !== 0);
|
|
||||||
queueCheckboxContainer.style.display = hasBusyPrinters ? 'block' : 'none';
|
|
||||||
|
|
||||||
// Event-Listener für die Druckerauswahl hinzufügen
|
|
||||||
selectElement.addEventListener('change', function() {
|
|
||||||
const selectedPrinterId = this.value;
|
|
||||||
const selectedPrinter = printers.find(p => p.id === selectedPrinterId);
|
|
||||||
|
|
||||||
if (selectedPrinter && selectedPrinter.status !== 0) {
|
|
||||||
// Wenn ein belegter Drucker ausgewählt wird, Checkbox für Warteschlange anzeigen
|
|
||||||
queueCheckboxContainer.style.display = 'block';
|
|
||||||
allowQueuedJobsCheckbox.checked = true;
|
|
||||||
} else if (selectedPrinter && selectedPrinter.status === 0) {
|
|
||||||
// Wenn ein verfügbarer Drucker ausgewählt wird, Checkbox für Warteschlange verstecken
|
|
||||||
allowQueuedJobsCheckbox.checked = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Laden der Drucker:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateJobsTable(jobs) {
|
|
||||||
const tableBody = document.getElementById('jobsTableBody');
|
|
||||||
tableBody.innerHTML = '';
|
|
||||||
|
|
||||||
jobs.forEach(job => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
|
|
||||||
const startDate = new Date(job.startAt);
|
|
||||||
const formattedStart = startDate.toLocaleString();
|
|
||||||
|
|
||||||
const isActive = !job.aborted && job.remainingMinutes > 0 && !job.waitingApproval;
|
|
||||||
const isWaiting = !job.aborted && job.waitingApproval;
|
|
||||||
|
|
||||||
let statusText = '';
|
|
||||||
let statusClass = '';
|
|
||||||
|
|
||||||
if (job.aborted) {
|
|
||||||
statusText = 'Abgebrochen';
|
|
||||||
statusClass = 'text-danger';
|
|
||||||
} else if (job.waitingApproval) {
|
|
||||||
statusText = 'Wartet auf Freischaltung';
|
|
||||||
statusClass = 'text-info';
|
|
||||||
} else if (job.remainingMinutes <= 0) {
|
|
||||||
statusText = 'Abgeschlossen';
|
|
||||||
statusClass = 'text-success';
|
|
||||||
} else {
|
|
||||||
statusText = 'Aktiv';
|
|
||||||
statusClass = 'text-warning';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zeige die verbleibende Zeit an
|
|
||||||
const remainingTime = job.waitingApproval ? '-' : job.remainingMinutes;
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${job.id}</td>
|
|
||||||
<td>${job.printerId}</td>
|
|
||||||
<td>${job.userId}</td>
|
|
||||||
<td>${formattedStart}</td>
|
|
||||||
<td>${job.durationInMinutes}</td>
|
|
||||||
<td>${remainingTime}</td>
|
|
||||||
<td><span class="${statusClass}">${statusText}</span></td>
|
|
||||||
<td>${job.comments || '-'}</td>
|
|
||||||
<td>
|
|
||||||
${isActive ? `
|
|
||||||
<button type="button" class="btn btn-sm btn-danger mb-1"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#abortJobModal"
|
|
||||||
data-job-id="${job.id}">
|
|
||||||
Abbrechen
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-success mb-1"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#finishJobModal"
|
|
||||||
data-job-id="${job.id}">
|
|
||||||
Beenden
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-primary mb-1"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#extendJobModal"
|
|
||||||
data-job-id="${job.id}">
|
|
||||||
Verlängern
|
|
||||||
</button>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
${isWaiting ? `
|
|
||||||
<button type="button" class="btn btn-sm btn-success mb-1"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#approveJobModal"
|
|
||||||
data-job-id="${job.id}">
|
|
||||||
Freischalten
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-danger mb-1"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#abortJobModal"
|
|
||||||
data-job-id="${job.id}">
|
|
||||||
Abbrechen
|
|
||||||
</button>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-secondary mb-1"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#editCommentsModal"
|
|
||||||
data-job-id="${job.id}"
|
|
||||||
data-job-comments="${job.comments || ''}">
|
|
||||||
Kommentare
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
tableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,37 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Anmelden - MYP API Tester{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="mb-0">Anmelden</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="api-form" data-url="/auth/login" data-method="POST" data-response="loginResponse">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="username" class="form-label">Benutzername</label>
|
|
||||||
<input type="text" class="form-control" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Passwort</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Anmelden</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="mt-3">
|
|
||||||
<p>Noch kein Konto? <a href="/register">Registrieren</a></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3">
|
|
||||||
<h5>Antwort:</h5>
|
|
||||||
<pre class="api-response" id="loginResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,119 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>MYP - Netzwerkkonfiguration</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
input[type="text"] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
}
|
|
||||||
.message {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 10px 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.success {
|
|
||||||
background-color: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
border: 1px solid #c3e6cb;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
background-color: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
border: 1px solid #f5c6cb;
|
|
||||||
}
|
|
||||||
.status-box {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 15px;
|
|
||||||
margin-top: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
color: #2c3e50;
|
|
||||||
font-size: 1.2em;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>MYP - Netzwerkkonfiguration</h1>
|
|
||||||
|
|
||||||
{% if message %}
|
|
||||||
<div class="message {{ message_type }}">
|
|
||||||
{{ message }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form method="POST" action="/admin/network-config">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="backend_hostname">Backend Hostname/IP:</label>
|
|
||||||
<input type="text" id="backend_hostname" name="backend_hostname" value="{{ config.backend_hostname }}" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="backend_port">Backend Port:</label>
|
|
||||||
<input type="text" id="backend_port" name="backend_port" value="{{ config.backend_port }}" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="frontend_hostname">Frontend Hostname/IP:</label>
|
|
||||||
<input type="text" id="frontend_hostname" name="frontend_hostname" value="{{ config.frontend_hostname }}" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="frontend_port">Frontend Port:</label>
|
|
||||||
<input type="text" id="frontend_port" name="frontend_port" value="{{ config.frontend_port }}" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit">Konfiguration speichern</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<h2>Aktuelle Verbindungsstatus</h2>
|
|
||||||
<div class="status-box">
|
|
||||||
<p><strong>Backend-Status:</strong> {{ backend_status }}</p>
|
|
||||||
<p><strong>Frontend-Status:</strong> {{ frontend_status }}</p>
|
|
||||||
<p><strong>Letzte Prüfung:</strong> {{ last_check }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Netzwerkschnittstellen</h2>
|
|
||||||
<div class="status-box">
|
|
||||||
{% for interface in network_interfaces %}
|
|
||||||
<p><strong>{{ interface.name }}:</strong> {{ interface.address }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,280 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Drucker - MYP API Tester{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h4 class="mb-0">Drucker verwalten</h4>
|
|
||||||
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newPrinterForm">
|
|
||||||
Neuen Drucker hinzufügen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="collapse" id="newPrinterForm">
|
|
||||||
<div class="card-body border-bottom">
|
|
||||||
<form class="api-form" data-url="/api/printers" data-method="POST" data-response="createPrinterResponse" data-reload="true">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="printerName" class="form-label">Name</label>
|
|
||||||
<input type="text" class="form-control" id="printerName" name="name" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="printerDescription" class="form-label">Beschreibung</label>
|
|
||||||
<textarea class="form-control" id="printerDescription" name="description" rows="3" required></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="printerStatus" class="form-label">Status</label>
|
|
||||||
<select class="form-control" id="printerStatus" name="status">
|
|
||||||
<option value="0">Verfügbar (0)</option>
|
|
||||||
<option value="1">Besetzt (1)</option>
|
|
||||||
<option value="2">Wartung (2)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="printerIpAddress" class="form-label">IP-Adresse (Tapo Steckdose)</label>
|
|
||||||
<input type="text" class="form-control" id="printerIpAddress" name="ipAddress" placeholder="z.B. 192.168.1.100">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-success">Drucker erstellen</button>
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="createPrinterResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="api-form mb-3" data-url="/api/printers" data-method="GET" data-response="printersResponse">
|
|
||||||
<button type="submit" class="btn btn-primary">Drucker aktualisieren</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Beschreibung</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>IP-Adresse</th>
|
|
||||||
<th>Aktueller Job</th>
|
|
||||||
<th>Wartende Jobs</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="printersTableBody">
|
|
||||||
<!-- Wird dynamisch gefüllt -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h6>API-Antwort:</h6>
|
|
||||||
<pre class="api-response" id="printersResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Drucker bearbeiten Modal -->
|
|
||||||
<div class="modal fade" id="editPrinterModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Drucker bearbeiten</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="editPrinterForm" class="api-form" data-method="PUT" data-response="editPrinterResponse" data-reload="true">
|
|
||||||
<input type="hidden" id="editPrinterId" name="printerId">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="editPrinterName" class="form-label">Name</label>
|
|
||||||
<input type="text" class="form-control" id="editPrinterName" name="name" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="editPrinterDescription" class="form-label">Beschreibung</label>
|
|
||||||
<textarea class="form-control" id="editPrinterDescription" name="description" rows="3" required></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="editPrinterStatus" class="form-label">Status</label>
|
|
||||||
<select class="form-control" id="editPrinterStatus" name="status">
|
|
||||||
<option value="0">Verfügbar (0)</option>
|
|
||||||
<option value="1">Besetzt (1)</option>
|
|
||||||
<option value="2">Wartung (2)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="editPrinterIpAddress" class="form-label">IP-Adresse (Tapo Steckdose)</label>
|
|
||||||
<input type="text" class="form-control" id="editPrinterIpAddress" name="ipAddress" placeholder="z.B. 192.168.1.100">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="editPrinterResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
||||||
<button type="submit" form="editPrinterForm" class="btn btn-primary">Änderungen speichern</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Drucker löschen Modal -->
|
|
||||||
<div class="modal fade" id="deletePrinterModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Drucker löschen</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Möchten Sie den Drucker <span id="deletePrinterName"></span> wirklich löschen?</p>
|
|
||||||
<form id="deletePrinterForm" class="api-form" data-method="DELETE" data-response="deletePrinterResponse" data-reload="true">
|
|
||||||
<input type="hidden" id="deletePrinterId" name="printerId">
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="deletePrinterResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
||||||
<button type="submit" form="deletePrinterForm" class="btn btn-danger">Löschen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Drucker laden
|
|
||||||
document.querySelector('form[data-url="/api/printers"]').dispatchEvent(new Event('submit'));
|
|
||||||
|
|
||||||
// Tabelle aktualisieren, wenn Drucker geladen werden
|
|
||||||
const printersResponse = document.getElementById('printersResponse');
|
|
||||||
const observer = new MutationObserver(function(mutations) {
|
|
||||||
try {
|
|
||||||
const printers = JSON.parse(printersResponse.textContent);
|
|
||||||
updatePrintersTable(printers);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Parsen der Drucker-Daten:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(printersResponse, { childList: true, characterData: true, subtree: true });
|
|
||||||
|
|
||||||
// Edit-Modal vorbereiten
|
|
||||||
document.getElementById('editPrinterModal').addEventListener('show.bs.modal', function(event) {
|
|
||||||
const button = event.relatedTarget;
|
|
||||||
const printerId = button.getAttribute('data-printer-id');
|
|
||||||
const printerName = button.getAttribute('data-printer-name');
|
|
||||||
const printerDescription = button.getAttribute('data-printer-description');
|
|
||||||
const printerStatus = button.getAttribute('data-printer-status');
|
|
||||||
const printerIpAddress = button.getAttribute('data-printer-ip');
|
|
||||||
|
|
||||||
document.getElementById('editPrinterId').value = printerId;
|
|
||||||
document.getElementById('editPrinterForm').setAttribute('data-url', `/api/printers/${printerId}`);
|
|
||||||
document.getElementById('editPrinterName').value = printerName;
|
|
||||||
document.getElementById('editPrinterDescription').value = printerDescription;
|
|
||||||
document.getElementById('editPrinterStatus').value = printerStatus;
|
|
||||||
document.getElementById('editPrinterIpAddress').value = printerIpAddress || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete-Modal vorbereiten
|
|
||||||
document.getElementById('deletePrinterModal').addEventListener('show.bs.modal', function(event) {
|
|
||||||
const button = event.relatedTarget;
|
|
||||||
const printerId = button.getAttribute('data-printer-id');
|
|
||||||
const printerName = button.getAttribute('data-printer-name');
|
|
||||||
|
|
||||||
document.getElementById('deletePrinterId').value = printerId;
|
|
||||||
document.getElementById('deletePrinterForm').setAttribute('data-url', `/api/printers/${printerId}`);
|
|
||||||
document.getElementById('deletePrinterName').textContent = printerName;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function updatePrintersTable(printers) {
|
|
||||||
const tableBody = document.getElementById('printersTableBody');
|
|
||||||
tableBody.innerHTML = '';
|
|
||||||
|
|
||||||
printers.forEach(printer => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
|
|
||||||
const statusText = {
|
|
||||||
0: 'Verfügbar',
|
|
||||||
1: 'Besetzt',
|
|
||||||
2: 'Wartung'
|
|
||||||
}[printer.status] || 'Unbekannt';
|
|
||||||
|
|
||||||
const statusClass = {
|
|
||||||
0: 'text-success',
|
|
||||||
1: 'text-warning',
|
|
||||||
2: 'text-danger'
|
|
||||||
}[printer.status] || '';
|
|
||||||
|
|
||||||
// Informationen zum aktuellen Job
|
|
||||||
let currentJobInfo = '-';
|
|
||||||
if (printer.latestJob && printer.status === 1) {
|
|
||||||
// Verbleibende Zeit berechnen
|
|
||||||
const remainingTime = printer.latestJob.remainingMinutes || 0;
|
|
||||||
currentJobInfo = `
|
|
||||||
<div class="small">
|
|
||||||
<strong>ID:</strong> ${printer.latestJob.id.substring(0, 8)}...<br>
|
|
||||||
<strong>Dauer:</strong> ${printer.latestJob.durationInMinutes} Min<br>
|
|
||||||
<strong>Verbleibend:</strong> ${remainingTime} Min
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wartende Jobs anzeigen
|
|
||||||
let waitingJobsInfo = '-';
|
|
||||||
if (printer.waitingJobs && printer.waitingJobs.length > 0) {
|
|
||||||
const waitingJobsCount = printer.waitingJobs.length;
|
|
||||||
waitingJobsInfo = `
|
|
||||||
<div class="small">
|
|
||||||
<strong>${waitingJobsCount} Job${waitingJobsCount !== 1 ? 's' : ''} in Warteschlange</strong><br>
|
|
||||||
${printer.waitingJobs.map((job, index) =>
|
|
||||||
`<span>${index + 1}. Job ${job.id.substring(0, 8)}... (${job.durationInMinutes} Min)</span>`
|
|
||||||
).join('<br>')}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${printer.id}</td>
|
|
||||||
<td>${printer.name}</td>
|
|
||||||
<td>${printer.description}</td>
|
|
||||||
<td><span class="${statusClass}">${statusText} (${printer.status})</span></td>
|
|
||||||
<td>${printer.ipAddress || '-'}</td>
|
|
||||||
<td>${currentJobInfo}</td>
|
|
||||||
<td>${waitingJobsInfo}</td>
|
|
||||||
<td>
|
|
||||||
<button type="button" class="btn btn-sm btn-primary"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#editPrinterModal"
|
|
||||||
data-printer-id="${printer.id}"
|
|
||||||
data-printer-name="${printer.name}"
|
|
||||||
data-printer-description="${printer.description}"
|
|
||||||
data-printer-status="${printer.status}"
|
|
||||||
data-printer-ip="${printer.ipAddress || ''}">
|
|
||||||
Bearbeiten
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#deletePrinterModal"
|
|
||||||
data-printer-id="${printer.id}"
|
|
||||||
data-printer-name="${printer.name}">
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
tableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,45 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Registrieren - MYP API Tester{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="mb-0">Registrieren</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="api-form" data-url="/auth/register" data-method="POST" data-response="registerResponse">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="username" class="form-label">Benutzername</label>
|
|
||||||
<input type="text" class="form-control" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Passwort</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="displayName" class="form-label">Anzeigename</label>
|
|
||||||
<input type="text" class="form-control" id="displayName" name="displayName">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="email" class="form-label">E-Mail</label>
|
|
||||||
<input type="email" class="form-control" id="email" name="email">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Registrieren</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="mt-3">
|
|
||||||
<p>Bereits registriert? <a href="/login">Anmelden</a></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3">
|
|
||||||
<h5>Antwort:</h5>
|
|
||||||
<pre class="api-response" id="registerResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,395 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Statistiken - MYP API Tester{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="mb-0">Systemstatistiken</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="api-form mb-3" data-url="/api/stats" data-method="GET" data-response="statsResponse">
|
|
||||||
<button type="submit" class="btn btn-primary">Statistiken aktualisieren</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="row" id="statsContainer">
|
|
||||||
<!-- Wird dynamisch gefüllt -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Problem-Drucker-Bereich -->
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header bg-warning text-dark">
|
|
||||||
<h5 class="mb-0">Drucker mit Verbindungsproblemen</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="problemPrintersContainer">
|
|
||||||
<div class="alert alert-info">Keine Verbindungsprobleme festgestellt.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Uptime-Grafik -->
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header bg-dark text-white">
|
|
||||||
<h5 class="mb-0">Steckdosen-Verfügbarkeit</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="api-form mb-3" data-url="/api/uptime" data-method="GET" data-response="uptimeResponse">
|
|
||||||
<button type="submit" class="btn btn-primary">Uptime-Daten laden</button>
|
|
||||||
</form>
|
|
||||||
<canvas id="uptimeChart" width="100%" height="300"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API-Antworten -->
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h6>Stats API-Antwort:</h6>
|
|
||||||
<pre class="api-response" id="statsResponse"></pre>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h6>Uptime API-Antwort:</h6>
|
|
||||||
<pre class="api-response" id="uptimeResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<!-- Chart.js für Diagramme -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
||||||
<script>
|
|
||||||
let uptimeChart;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Statistiken laden
|
|
||||||
document.querySelector('form[data-url="/api/stats"]').dispatchEvent(new Event('submit'));
|
|
||||||
document.querySelector('form[data-url="/api/uptime"]').dispatchEvent(new Event('submit'));
|
|
||||||
|
|
||||||
// Statistiken aktualisieren, wenn API-Antwort geladen wird
|
|
||||||
const statsResponse = document.getElementById('statsResponse');
|
|
||||||
const statsObserver = new MutationObserver(function(mutations) {
|
|
||||||
try {
|
|
||||||
const stats = JSON.parse(statsResponse.textContent);
|
|
||||||
updateStatsDisplay(stats);
|
|
||||||
updateProblemPrinters(stats);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Parsen der Statistik-Daten:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
statsObserver.observe(statsResponse, { childList: true, characterData: true, subtree: true });
|
|
||||||
|
|
||||||
// Uptime-Daten aktualisieren, wenn API-Antwort geladen wird
|
|
||||||
const uptimeResponse = document.getElementById('uptimeResponse');
|
|
||||||
const uptimeObserver = new MutationObserver(function(mutations) {
|
|
||||||
try {
|
|
||||||
const uptime = JSON.parse(uptimeResponse.textContent);
|
|
||||||
updateUptimeChart(uptime);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Parsen der Uptime-Daten:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
uptimeObserver.observe(uptimeResponse, { childList: true, characterData: true, subtree: true });
|
|
||||||
|
|
||||||
// Periodische Aktualisierung
|
|
||||||
setInterval(function() {
|
|
||||||
document.querySelector('form[data-url="/api/stats"]').dispatchEvent(new Event('submit'));
|
|
||||||
document.querySelector('form[data-url="/api/uptime"]').dispatchEvent(new Event('submit'));
|
|
||||||
}, 60000); // Alle 60 Sekunden aktualisieren
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateStatsDisplay(stats) {
|
|
||||||
const container = document.getElementById('statsContainer');
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
// Drucker-Statistiken
|
|
||||||
const printerStats = document.createElement('div');
|
|
||||||
printerStats.className = 'col-md-4 mb-3';
|
|
||||||
printerStats.innerHTML = `
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-header bg-primary text-white">
|
|
||||||
<h5 class="mb-0">Drucker</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Gesamt:</span>
|
|
||||||
<span>${stats.printers.total}</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Verfügbar:</span>
|
|
||||||
<span>${stats.printers.available}</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Auslastung:</span>
|
|
||||||
<span>${Math.round(stats.printers.utilization_rate * 100)}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="progress mt-3 mb-3">
|
|
||||||
<div class="progress-bar" role="progressbar"
|
|
||||||
style="width: ${Math.round(stats.printers.utilization_rate * 100)}%">
|
|
||||||
${Math.round(stats.printers.utilization_rate * 100)}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Online:</span>
|
|
||||||
<span>${stats.printers.online}</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Offline:</span>
|
|
||||||
<span>${stats.printers.offline}</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Verbindungsrate:</span>
|
|
||||||
<span>${Math.round(stats.printers.connectivity_rate * 100)}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="progress mt-3">
|
|
||||||
<div class="progress-bar bg-success" role="progressbar"
|
|
||||||
style="width: ${Math.round(stats.printers.connectivity_rate * 100)}%">
|
|
||||||
${Math.round(stats.printers.connectivity_rate * 100)}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Job-Statistiken
|
|
||||||
const jobStats = document.createElement('div');
|
|
||||||
jobStats.className = 'col-md-4 mb-3';
|
|
||||||
jobStats.innerHTML = `
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-header bg-success text-white">
|
|
||||||
<h5 class="mb-0">Druckaufträge</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Gesamt:</span>
|
|
||||||
<span>${stats.jobs.total}</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Aktiv:</span>
|
|
||||||
<span>${stats.jobs.active}</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Abgeschlossen:</span>
|
|
||||||
<span>${stats.jobs.completed}</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Durchschnittliche Dauer:</span>
|
|
||||||
<span>${stats.jobs.avg_duration} Minuten</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Benutzer- und Uptime-Statistiken
|
|
||||||
const userStats = document.createElement('div');
|
|
||||||
userStats.className = 'col-md-4 mb-3';
|
|
||||||
userStats.innerHTML = `
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-header bg-info text-white">
|
|
||||||
<h5 class="mb-0">System</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Benutzer:</span>
|
|
||||||
<span>${stats.users.total}</span>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Verbindungsausfälle (7 Tage):</span>
|
|
||||||
<span>${stats.uptime.outages_last_7_days}</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span>Aktuelle Probleme:</span>
|
|
||||||
<span>${stats.uptime.problem_printers.length}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
container.appendChild(printerStats);
|
|
||||||
container.appendChild(jobStats);
|
|
||||||
container.appendChild(userStats);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProblemPrinters(stats) {
|
|
||||||
const container = document.getElementById('problemPrintersContainer');
|
|
||||||
const problemPrinters = stats.uptime.problem_printers;
|
|
||||||
|
|
||||||
if (problemPrinters.length === 0) {
|
|
||||||
container.innerHTML = '<div class="alert alert-info">Keine Verbindungsprobleme festgestellt.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '<div class="table-responsive"><table class="table table-striped">';
|
|
||||||
html += '<thead><tr><th>Drucker</th><th>Status</th><th>Offline seit</th><th>Dauer</th></tr></thead>';
|
|
||||||
html += '<tbody>';
|
|
||||||
|
|
||||||
problemPrinters.forEach(printer => {
|
|
||||||
let offlineSince = 'Unbekannt';
|
|
||||||
let duration = 'Unbekannt';
|
|
||||||
|
|
||||||
if (printer.last_seen) {
|
|
||||||
try {
|
|
||||||
const lastSeen = new Date(printer.last_seen);
|
|
||||||
const now = new Date();
|
|
||||||
const diffSeconds = Math.floor((now - lastSeen) / 1000);
|
|
||||||
const hours = Math.floor(diffSeconds / 3600);
|
|
||||||
const minutes = Math.floor((diffSeconds % 3600) / 60);
|
|
||||||
|
|
||||||
offlineSince = lastSeen.toLocaleString();
|
|
||||||
duration = `${hours}h ${minutes}m`;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Berechnen der Offline-Zeit:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `<tr>
|
|
||||||
<td>${printer.name}</td>
|
|
||||||
<td><span class="badge bg-danger">Offline</span></td>
|
|
||||||
<td>${offlineSince}</td>
|
|
||||||
<td>${duration}</td>
|
|
||||||
</tr>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
html += '</tbody></table></div>';
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUptimeChart(uptimeData) {
|
|
||||||
// Wenn keine Daten vorhanden sind, nichts tun
|
|
||||||
if (!uptimeData || !uptimeData.sockets || uptimeData.sockets.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Daten für das Diagramm vorbereiten
|
|
||||||
const socketNames = [];
|
|
||||||
const datasets = [];
|
|
||||||
const colors = {
|
|
||||||
online: 'rgba(40, 167, 69, 0.7)',
|
|
||||||
offline: 'rgba(220, 53, 69, 0.7)',
|
|
||||||
unknown: 'rgba(108, 117, 125, 0.7)'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Zeitraum für das Diagramm (letzten 7 Tage)
|
|
||||||
const endDate = new Date();
|
|
||||||
const startDate = new Date();
|
|
||||||
startDate.setDate(startDate.getDate() - 7);
|
|
||||||
|
|
||||||
// Für jede Steckdose
|
|
||||||
uptimeData.sockets.forEach(socket => {
|
|
||||||
socketNames.push(socket.name);
|
|
||||||
|
|
||||||
// Sortiere Ereignisse nach Zeitstempel
|
|
||||||
if (socket.events) {
|
|
||||||
socket.events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
||||||
|
|
||||||
// Erstelle einen Datensatz für diese Steckdose
|
|
||||||
const data = [];
|
|
||||||
|
|
||||||
// Füge Ereignisse zum Datensatz hinzu
|
|
||||||
socket.events.forEach(event => {
|
|
||||||
data.push({
|
|
||||||
x: new Date(event.timestamp),
|
|
||||||
y: event.status === 'online' ? 1 : 0,
|
|
||||||
status: event.status,
|
|
||||||
duration: event.duration_seconds ?
|
|
||||||
formatDuration(event.duration_seconds) : null
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Füge aktuellen Status hinzu
|
|
||||||
if (socket.current_status) {
|
|
||||||
data.push({
|
|
||||||
x: new Date(),
|
|
||||||
y: socket.current_status.connection_status === 'online' ? 1 : 0,
|
|
||||||
status: socket.current_status.connection_status,
|
|
||||||
duration: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
datasets.push({
|
|
||||||
label: socket.name,
|
|
||||||
data: data,
|
|
||||||
stepped: true,
|
|
||||||
borderColor: colors[socket.current_status?.connection_status || 'unknown'],
|
|
||||||
backgroundColor: colors[socket.current_status?.connection_status || 'unknown'],
|
|
||||||
fill: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Chart.js Konfiguration
|
|
||||||
const ctx = document.getElementById('uptimeChart').getContext('2d');
|
|
||||||
|
|
||||||
// Wenn Chart bereits existiert, zerstöre ihn
|
|
||||||
if (uptimeChart) {
|
|
||||||
uptimeChart.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erstelle neuen Chart
|
|
||||||
uptimeChart = new Chart(ctx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
datasets: datasets
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
label: function(context) {
|
|
||||||
const point = context.raw;
|
|
||||||
let label = context.dataset.label || '';
|
|
||||||
label += ': ' + (point.status === 'online' ? 'Online' : 'Offline');
|
|
||||||
if (point.duration) {
|
|
||||||
label += ' (Dauer: ' + point.duration + ')';
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
time: {
|
|
||||||
unit: 'day'
|
|
||||||
},
|
|
||||||
min: startDate,
|
|
||||||
max: endDate
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
min: -0.1,
|
|
||||||
max: 1.1,
|
|
||||||
ticks: {
|
|
||||||
callback: function(value) {
|
|
||||||
return value === 0 ? 'Offline' : value === 1 ? 'Online' : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(seconds) {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
return `${hours}h ${minutes}m`;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,238 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Benutzer - MYP API Tester{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h4 class="mb-0">Benutzer verwalten</h4>
|
|
||||||
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newUserForm">
|
|
||||||
Neuen Benutzer hinzufügen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="collapse" id="newUserForm">
|
|
||||||
<div class="card-body border-bottom">
|
|
||||||
<form class="api-form" data-url="/auth/register" data-method="POST" data-response="createUserResponse" data-reload="true">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="userName" class="form-label">Benutzername</label>
|
|
||||||
<input type="text" class="form-control" id="userName" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="userPassword" class="form-label">Passwort</label>
|
|
||||||
<input type="password" class="form-control" id="userPassword" name="password" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="userDisplayName" class="form-label">Anzeigename</label>
|
|
||||||
<input type="text" class="form-control" id="userDisplayName" name="displayName">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="userEmail" class="form-label">E-Mail</label>
|
|
||||||
<input type="email" class="form-control" id="userEmail" name="email">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-success">Benutzer erstellen</button>
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="createUserResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="api-form mb-3" data-url="/api/users" data-method="GET" data-response="usersResponse">
|
|
||||||
<button type="submit" class="btn btn-primary">Benutzer aktualisieren</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Benutzername</th>
|
|
||||||
<th>Anzeigename</th>
|
|
||||||
<th>E-Mail</th>
|
|
||||||
<th>Rolle</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="usersTableBody">
|
|
||||||
<!-- Wird dynamisch gefüllt -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h6>API-Antwort:</h6>
|
|
||||||
<pre class="api-response" id="usersResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Benutzer bearbeiten Modal -->
|
|
||||||
<div class="modal fade" id="editUserModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Benutzer bearbeiten</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="editUserForm" class="api-form" data-method="PUT" data-response="editUserResponse" data-reload="true">
|
|
||||||
<input type="hidden" id="editUserId" name="userId">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="editUserName" class="form-label">Benutzername</label>
|
|
||||||
<input type="text" class="form-control" id="editUserName" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="editUserDisplayName" class="form-label">Anzeigename</label>
|
|
||||||
<input type="text" class="form-control" id="editUserDisplayName" name="displayName">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="editUserEmail" class="form-label">E-Mail</label>
|
|
||||||
<input type="email" class="form-control" id="editUserEmail" name="email">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="editUserRole" class="form-label">Rolle</label>
|
|
||||||
<select class="form-control" id="editUserRole" name="role">
|
|
||||||
<option value="user">Benutzer</option>
|
|
||||||
<option value="admin">Administrator</option>
|
|
||||||
<option value="guest">Gast</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="editUserResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
||||||
<button type="submit" form="editUserForm" class="btn btn-primary">Änderungen speichern</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Benutzer löschen Modal -->
|
|
||||||
<div class="modal fade" id="deleteUserModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Benutzer löschen</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Möchten Sie den Benutzer <span id="deleteUserName"></span> wirklich löschen?</p>
|
|
||||||
<form id="deleteUserForm" class="api-form" data-method="DELETE" data-response="deleteUserResponse" data-reload="true">
|
|
||||||
<input type="hidden" id="deleteUserId" name="userId">
|
|
||||||
</form>
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Antwort:</h6>
|
|
||||||
<pre class="api-response" id="deleteUserResponse"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
||||||
<button type="submit" form="deleteUserForm" class="btn btn-danger">Löschen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Benutzer laden
|
|
||||||
document.querySelector('form[data-url="/api/users"]').dispatchEvent(new Event('submit'));
|
|
||||||
|
|
||||||
// Tabelle aktualisieren, wenn Benutzer geladen werden
|
|
||||||
const usersResponse = document.getElementById('usersResponse');
|
|
||||||
const observer = new MutationObserver(function(mutations) {
|
|
||||||
try {
|
|
||||||
const users = JSON.parse(usersResponse.textContent);
|
|
||||||
updateUsersTable(users);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fehler beim Parsen der Benutzer-Daten:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(usersResponse, { childList: true, characterData: true, subtree: true });
|
|
||||||
|
|
||||||
// Edit-Modal vorbereiten
|
|
||||||
document.getElementById('editUserModal').addEventListener('show.bs.modal', function(event) {
|
|
||||||
const button = event.relatedTarget;
|
|
||||||
const userId = button.getAttribute('data-user-id');
|
|
||||||
const userName = button.getAttribute('data-user-name');
|
|
||||||
const userDisplayName = button.getAttribute('data-user-displayname');
|
|
||||||
const userEmail = button.getAttribute('data-user-email');
|
|
||||||
const userRole = button.getAttribute('data-user-role');
|
|
||||||
|
|
||||||
document.getElementById('editUserId').value = userId;
|
|
||||||
document.getElementById('editUserForm').setAttribute('data-url', `/api/users/${userId}`);
|
|
||||||
document.getElementById('editUserName').value = userName;
|
|
||||||
document.getElementById('editUserDisplayName').value = userDisplayName || '';
|
|
||||||
document.getElementById('editUserEmail').value = userEmail || '';
|
|
||||||
document.getElementById('editUserRole').value = userRole;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete-Modal vorbereiten
|
|
||||||
document.getElementById('deleteUserModal').addEventListener('show.bs.modal', function(event) {
|
|
||||||
const button = event.relatedTarget;
|
|
||||||
const userId = button.getAttribute('data-user-id');
|
|
||||||
const userName = button.getAttribute('data-user-name');
|
|
||||||
|
|
||||||
document.getElementById('deleteUserId').value = userId;
|
|
||||||
document.getElementById('deleteUserForm').setAttribute('data-url', `/api/users/${userId}`);
|
|
||||||
document.getElementById('deleteUserName').textContent = userName;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateUsersTable(users) {
|
|
||||||
const tableBody = document.getElementById('usersTableBody');
|
|
||||||
tableBody.innerHTML = '';
|
|
||||||
|
|
||||||
users.forEach(user => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
|
|
||||||
const roleClass = {
|
|
||||||
'admin': 'text-danger',
|
|
||||||
'user': 'text-primary',
|
|
||||||
'guest': 'text-secondary'
|
|
||||||
}[user.role] || '';
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${user.id}</td>
|
|
||||||
<td>${user.username}</td>
|
|
||||||
<td>${user.displayName || user.username}</td>
|
|
||||||
<td>${user.email || '-'}</td>
|
|
||||||
<td><span class="${roleClass}">${user.role}</span></td>
|
|
||||||
<td>
|
|
||||||
<button type="button" class="btn btn-sm btn-primary"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#editUserModal"
|
|
||||||
data-user-id="${user.id}"
|
|
||||||
data-user-name="${user.username}"
|
|
||||||
data-user-displayname="${user.displayName || ''}"
|
|
||||||
data-user-email="${user.email || ''}"
|
|
||||||
data-user-role="${user.role}">
|
|
||||||
Bearbeiten
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#deleteUserModal"
|
|
||||||
data-user-id="${user.id}"
|
|
||||||
data-user-name="${user.username}">
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
tableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,286 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test-Skript für MYP Backend-Setup
|
|
||||||
Überprüft, ob die neue Produktions-Konfiguration korrekt funktioniert
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import importlib.util
|
|
||||||
|
|
||||||
def test_python_environment():
|
|
||||||
"""Teste Python-Umgebung und Dependencies"""
|
|
||||||
print("🐍 Teste Python-Umgebung...")
|
|
||||||
|
|
||||||
# Python-Version prüfen
|
|
||||||
python_version = sys.version_info
|
|
||||||
print(f" Python-Version: {python_version.major}.{python_version.minor}.{python_version.micro}")
|
|
||||||
|
|
||||||
if python_version < (3, 8):
|
|
||||||
print(" ❌ Python-Version ist zu alt! Benötigt wird mindestens Python 3.8")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(" ✅ Python-Version ist kompatibel")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def test_dependencies():
|
|
||||||
"""Teste erforderliche Python-Pakete"""
|
|
||||||
print("📦 Teste Python-Dependencies...")
|
|
||||||
|
|
||||||
required_packages = {
|
|
||||||
'flask': 'flask',
|
|
||||||
'flask_cors': 'flask_cors',
|
|
||||||
'werkzeug': 'werkzeug',
|
|
||||||
'jwt': 'PyJWT', # PyJWT wird als 'jwt' importiert
|
|
||||||
'dotenv': 'python-dotenv', # python-dotenv wird als 'dotenv' importiert
|
|
||||||
'gunicorn': 'gunicorn'
|
|
||||||
}
|
|
||||||
|
|
||||||
missing_packages = []
|
|
||||||
|
|
||||||
for import_name, package_name in required_packages.items():
|
|
||||||
try:
|
|
||||||
__import__(import_name)
|
|
||||||
print(f" ✅ {package_name}")
|
|
||||||
except ImportError:
|
|
||||||
print(f" ❌ {package_name} fehlt")
|
|
||||||
missing_packages.append(package_name)
|
|
||||||
|
|
||||||
if missing_packages:
|
|
||||||
print(f" Fehlende Pakete: {', '.join(missing_packages)}")
|
|
||||||
print(" Installiere mit: pip install -r requirements.txt")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
print(" ✅ Alle Dependencies verfügbar")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def test_configuration():
|
|
||||||
"""Teste Konfigurationsklassen"""
|
|
||||||
print("⚙️ Teste Konfiguration...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Importiere Konfiguration
|
|
||||||
from config import config, DevelopmentConfig, ProductionConfig, TestingConfig
|
|
||||||
|
|
||||||
print(" ✅ Konfigurationsklassen importiert")
|
|
||||||
|
|
||||||
# Teste verschiedene Konfigurationen
|
|
||||||
dev_config = DevelopmentConfig()
|
|
||||||
prod_config = ProductionConfig()
|
|
||||||
test_config = TestingConfig()
|
|
||||||
|
|
||||||
print(f" ✅ Development-Config: DEBUG={dev_config.DEBUG}")
|
|
||||||
print(f" ✅ Production-Config: DEBUG={prod_config.DEBUG}")
|
|
||||||
print(f" ✅ Testing-Config: TESTING={test_config.TESTING}")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Konfigurationsfehler: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_application_factory():
|
|
||||||
"""Teste Application Factory Pattern"""
|
|
||||||
print("🏭 Teste Application Factory...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from app import create_app
|
|
||||||
|
|
||||||
# Teste verschiedene Konfigurationen
|
|
||||||
configs = ['development', 'production', 'testing']
|
|
||||||
|
|
||||||
for config_name in configs:
|
|
||||||
try:
|
|
||||||
app = create_app(config_name)
|
|
||||||
if app and hasattr(app, 'config'):
|
|
||||||
print(f" ✅ {config_name.title()}-Config erfolgreich geladen")
|
|
||||||
else:
|
|
||||||
print(f" ❌ {config_name.title()}-Config fehlgeschlagen")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ {config_name.title()}-Config Fehler: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(" ✅ Application Factory funktioniert")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Application Factory Fehler: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_database_functions():
|
|
||||||
"""Teste Datenbankfunktionen"""
|
|
||||||
print("🗄️ Teste Datenbankfunktionen...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.environ['SECRET_KEY'] = 'test_secret_key'
|
|
||||||
os.environ['DATABASE_PATH'] = ':memory:'
|
|
||||||
|
|
||||||
from app import create_app, init_db, get_db
|
|
||||||
|
|
||||||
app = create_app('testing')
|
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
# Initialisiere Test-Datenbank
|
|
||||||
init_db()
|
|
||||||
|
|
||||||
# Teste Datenbankverbindung
|
|
||||||
db = get_db()
|
|
||||||
result = db.execute('SELECT 1').fetchone()
|
|
||||||
|
|
||||||
if result:
|
|
||||||
print(" ✅ Datenbankverbindung funktioniert")
|
|
||||||
print(" ✅ Tabellen wurden erstellt")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(" ❌ Datenbankverbindung fehlgeschlagen")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Datenbankfehler: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_environment_variables():
|
|
||||||
"""Teste Umgebungsvariablen"""
|
|
||||||
print("🌍 Teste Umgebungsvariablen...")
|
|
||||||
|
|
||||||
# Lade env.backend falls vorhanden
|
|
||||||
if os.path.exists('env.backend'):
|
|
||||||
print(" ✅ env.backend gefunden")
|
|
||||||
|
|
||||||
with open('env.backend', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
required_vars = [
|
|
||||||
'FLASK_APP',
|
|
||||||
'FLASK_ENV',
|
|
||||||
'SECRET_KEY',
|
|
||||||
'DATABASE_PATH'
|
|
||||||
]
|
|
||||||
|
|
||||||
found_vars = []
|
|
||||||
for line in lines:
|
|
||||||
if '=' in line and not line.strip().startswith('#'):
|
|
||||||
var_name = line.split('=')[0].strip()
|
|
||||||
if var_name in required_vars:
|
|
||||||
found_vars.append(var_name)
|
|
||||||
|
|
||||||
missing_vars = set(required_vars) - set(found_vars)
|
|
||||||
|
|
||||||
if missing_vars:
|
|
||||||
print(f" ❌ Fehlende Umgebungsvariablen: {', '.join(missing_vars)}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
print(f" ✅ Alle erforderlichen Variablen gefunden: {', '.join(found_vars)}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(" ❌ env.backend nicht gefunden")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_wsgi():
|
|
||||||
"""Teste WSGI-Konfiguration"""
|
|
||||||
print("🔧 Teste WSGI-Setup...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from wsgi import application
|
|
||||||
|
|
||||||
if application:
|
|
||||||
print(" ✅ WSGI-Application erfolgreich importiert")
|
|
||||||
print(f" ✅ App-Name: {application.name}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(" ❌ WSGI-Application ist None")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ WSGI-Fehler: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_health_endpoint():
|
|
||||||
"""Teste Health-Check-Endpoint"""
|
|
||||||
print("🏥 Teste Health-Check...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Verwende Test-Konfiguration um Logging-Probleme zu vermeiden
|
|
||||||
from app import create_app
|
|
||||||
app = create_app('testing')
|
|
||||||
|
|
||||||
with app.test_client() as client:
|
|
||||||
# Teste sowohl /health als auch /monitoring/health und /monitoring/health/simple
|
|
||||||
endpoints_to_test = ['/health', '/monitoring/health/simple', '/monitoring/health']
|
|
||||||
|
|
||||||
for endpoint in endpoints_to_test:
|
|
||||||
response = client.get(endpoint)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.get_json()
|
|
||||||
if data and (data.get('status') == 'healthy' or data.get('status') == 'ok'):
|
|
||||||
print(f" ✅ Health-Check funktioniert ({endpoint})")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f" ❌ Health-Check-Response ungültig ({endpoint}): {data}")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
print(f" ❌ Health-Check fehlgeschlagen ({endpoint}): {response.status_code}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Wenn alle Endpoints fehlschlagen, zeige verfügbare Routen
|
|
||||||
print(" 📋 Verfügbare Health-Routen:")
|
|
||||||
for rule in app.url_map.iter_rules():
|
|
||||||
if 'health' in rule.rule.lower():
|
|
||||||
print(f" - {rule.rule} [{', '.join(rule.methods)}]")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Health-Check-Fehler: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Haupttest-Funktion"""
|
|
||||||
print("=" * 50)
|
|
||||||
print("🧪 MYP Backend - Konfigurationstest")
|
|
||||||
print("=" * 50)
|
|
||||||
print()
|
|
||||||
|
|
||||||
tests = [
|
|
||||||
test_python_environment,
|
|
||||||
test_dependencies,
|
|
||||||
test_configuration,
|
|
||||||
test_application_factory,
|
|
||||||
test_database_functions,
|
|
||||||
test_environment_variables,
|
|
||||||
test_wsgi,
|
|
||||||
test_health_endpoint
|
|
||||||
]
|
|
||||||
|
|
||||||
passed = 0
|
|
||||||
failed = 0
|
|
||||||
|
|
||||||
for test in tests:
|
|
||||||
try:
|
|
||||||
if test():
|
|
||||||
passed += 1
|
|
||||||
else:
|
|
||||||
failed += 1
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Test-Fehler: {e}")
|
|
||||||
failed += 1
|
|
||||||
print()
|
|
||||||
|
|
||||||
print("=" * 50)
|
|
||||||
print(f"📊 Test-Ergebnisse: {passed} ✅ | {failed} ❌")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
if failed == 0:
|
|
||||||
print("🎉 Alle Tests bestanden! Backend ist bereit.")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("⚠️ Einige Tests fehlgeschlagen. Bitte Konfiguration prüfen.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
success = main()
|
|
||||||
sys.exit(0 if success else 1)
|
|
@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
WSGI-Konfiguration für MYP Backend
|
|
||||||
Lädt die Flask-Anwendung für Produktions-Deployment mit Gunicorn
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Füge das Backend-Verzeichnis zum Python-Pfad hinzu
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
from app import create_app, init_db, init_printers, setup_frontend_v2
|
|
||||||
|
|
||||||
# Bestimme die Umgebung
|
|
||||||
flask_env = os.environ.get('FLASK_ENV', 'production')
|
|
||||||
|
|
||||||
# Erstelle die Flask-Anwendung
|
|
||||||
application = create_app(flask_env)
|
|
||||||
|
|
||||||
# Initialisierung für WSGI-Server
|
|
||||||
with application.app_context():
|
|
||||||
# Datenbank initialisieren
|
|
||||||
init_db()
|
|
||||||
|
|
||||||
# Drucker initialisieren (falls konfiguriert)
|
|
||||||
printers_config = application.config.get('PRINTERS', {})
|
|
||||||
if printers_config:
|
|
||||||
init_printers()
|
|
||||||
|
|
||||||
# Frontend V2 einrichten
|
|
||||||
setup_frontend_v2()
|
|
||||||
|
|
||||||
# Logging für WSGI-Start
|
|
||||||
if hasattr(application, 'logger'):
|
|
||||||
application.logger.info(f'MYP Backend WSGI application started in {flask_env} mode')
|
|
||||||
|
|
||||||
# Export für Gunicorn
|
|
||||||
app = application
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Für direkte Ausführung (Debug)
|
|
||||||
application.run(host='0.0.0.0', port=5000, debug=(flask_env == 'development'))
|
|
@ -1,110 +0,0 @@
|
|||||||
# 🔧 MYP Entwicklungsumgebung - Docker Compose
|
|
||||||
# Erweiterte Konfiguration für lokale Entwicklung
|
|
||||||
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# Backend-Entwicklung mit Hot Reload
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile.dev
|
|
||||||
environment:
|
|
||||||
- FLASK_ENV=development
|
|
||||||
- FLASK_DEBUG=true
|
|
||||||
- PYTHONUNBUFFERED=1
|
|
||||||
- WATCHDOG_ENABLED=true
|
|
||||||
volumes:
|
|
||||||
- ./backend:/app
|
|
||||||
- /app/__pycache__
|
|
||||||
- backend_logs:/app/logs
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
- "5555:5555" # Debug-Server
|
|
||||||
command: flask run --host=0.0.0.0 --port=5000 --reload
|
|
||||||
|
|
||||||
# Frontend-Entwicklung mit Hot Reload
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile.dev
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- NEXT_TELEMETRY_DISABLED=1
|
|
||||||
- WATCHPACK_POLLING=true
|
|
||||||
volumes:
|
|
||||||
- ./frontend:/app
|
|
||||||
- /app/node_modules
|
|
||||||
- /app/.next
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
- "8081:8081" # Debug-Server
|
|
||||||
command: pnpm dev
|
|
||||||
|
|
||||||
# Monitoring Services
|
|
||||||
prometheus:
|
|
||||||
image: prom/prometheus:latest
|
|
||||||
container_name: myp-prometheus
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "9090:9090"
|
|
||||||
volumes:
|
|
||||||
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
|
||||||
- prometheus_data:/prometheus
|
|
||||||
command:
|
|
||||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
|
||||||
- '--storage.tsdb.path=/prometheus'
|
|
||||||
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
|
||||||
- '--web.console.templates=/etc/prometheus/consoles'
|
|
||||||
- '--storage.tsdb.retention.time=200h'
|
|
||||||
- '--web.enable-lifecycle'
|
|
||||||
networks:
|
|
||||||
- myp-network
|
|
||||||
|
|
||||||
grafana:
|
|
||||||
image: grafana/grafana:latest
|
|
||||||
container_name: myp-grafana
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "3001:3000"
|
|
||||||
volumes:
|
|
||||||
- grafana_data:/var/lib/grafana
|
|
||||||
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning
|
|
||||||
environment:
|
|
||||||
- GF_SECURITY_ADMIN_USER=admin
|
|
||||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
|
||||||
- GF_USERS_ALLOW_SIGN_UP=false
|
|
||||||
networks:
|
|
||||||
- myp-network
|
|
||||||
|
|
||||||
# Datenbank-Viewer (Adminer)
|
|
||||||
adminer:
|
|
||||||
image: adminer:latest
|
|
||||||
container_name: myp-adminer
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
networks:
|
|
||||||
- myp-network
|
|
||||||
|
|
||||||
# Redis für Caching (optional)
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: myp-redis
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- myp-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
backend_logs:
|
|
||||||
prometheus_data:
|
|
||||||
grafana_data:
|
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
myp-network:
|
|
||||||
external: true
|
|
@ -1,236 +0,0 @@
|
|||||||
# 🏭 MYP - Manage your Printer (Produktionsumgebung)
|
|
||||||
# Hauptkonfiguration für Container-Orchestrierung
|
|
||||||
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# === BACKEND SERVICE ===
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
- BUILDKIT_INLINE_CACHE=1
|
|
||||||
image: myp/backend:latest
|
|
||||||
container_name: myp-backend
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
environment:
|
|
||||||
# Flask-Konfiguration
|
|
||||||
- FLASK_APP=app.py
|
|
||||||
- FLASK_ENV=${FLASK_ENV:-production}
|
|
||||||
- PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
# Datenbank
|
|
||||||
- DATABASE_PATH=${DATABASE_PATH:-instance/myp.db}
|
|
||||||
|
|
||||||
# Sicherheit
|
|
||||||
- SECRET_KEY=${SECRET_KEY:-7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F}
|
|
||||||
- JWT_SECRET=${JWT_SECRET:-secure-jwt-secret}
|
|
||||||
|
|
||||||
# Drucker-Konfiguration
|
|
||||||
- "PRINTERS=${PRINTERS:-{\"Drucker 1\": {\"ip\": \"192.168.0.100\"}, \"Drucker 2\": {\"ip\": \"192.168.0.101\"}, \"Drucker 3\": {\"ip\": \"192.168.0.102\"}, \"Drucker 4\": {\"ip\": \"192.168.0.103\"}, \"Drucker 5\": {\"ip\": \"192.168.0.104\"}, \"Drucker 6\": {\"ip\": \"192.168.0.106\"}}}"
|
|
||||||
|
|
||||||
# TAPO Smart Plug
|
|
||||||
- TAPO_USERNAME=${TAPO_USERNAME:-till.tomczak@mercedes-benz.com}
|
|
||||||
- TAPO_PASSWORD=${TAPO_PASSWORD:-744563017196A}
|
|
||||||
|
|
||||||
# Netzwerk
|
|
||||||
- HOST=0.0.0.0
|
|
||||||
- PORT=5000
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- backend_instance:/app/instance
|
|
||||||
- backend_logs:/app/logs
|
|
||||||
- backend_migrations:/app/migrations
|
|
||||||
|
|
||||||
networks:
|
|
||||||
myp-network:
|
|
||||||
ipv4_address: 192.168.0.5
|
|
||||||
|
|
||||||
expose:
|
|
||||||
- "5000"
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.backend.rule=PathPrefix(`/api`)"
|
|
||||||
- "traefik.http.services.backend.loadbalancer.server.port=5000"
|
|
||||||
|
|
||||||
# === FRONTEND SERVICE ===
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
- BUILDKIT_INLINE_CACHE=1
|
|
||||||
- NODE_ENV=${NODE_ENV:-production}
|
|
||||||
image: myp/frontend:latest
|
|
||||||
container_name: myp-frontend
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=${NODE_ENV:-production}
|
|
||||||
- NEXT_TELEMETRY_DISABLED=1
|
|
||||||
- NEXT_PUBLIC_API_URL=${API_BASE_URL:-/api}
|
|
||||||
- PORT=3000
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- frontend_data:/app/.next
|
|
||||||
- frontend_db:/app/db
|
|
||||||
|
|
||||||
networks:
|
|
||||||
- myp-network
|
|
||||||
|
|
||||||
expose:
|
|
||||||
- "3000"
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
depends_on:
|
|
||||||
backend:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.frontend.rule=PathPrefix(`/`)"
|
|
||||||
- "traefik.http.services.frontend.loadbalancer.server.port=3000"
|
|
||||||
|
|
||||||
# === REVERSE PROXY SERVICE ===
|
|
||||||
caddy:
|
|
||||||
image: caddy:2.7-alpine
|
|
||||||
container_name: myp-caddy
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
- "2019:2019" # Admin API
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- ./proxy/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
||||||
- caddy_data:/data
|
|
||||||
- caddy_config:/config
|
|
||||||
- caddy_logs:/var/log/caddy
|
|
||||||
|
|
||||||
networks:
|
|
||||||
- myp-network
|
|
||||||
|
|
||||||
extra_hosts:
|
|
||||||
- "host.docker.internal:host-gateway"
|
|
||||||
|
|
||||||
environment:
|
|
||||||
- CADDY_HOST=${CADDY_HOST:-53.37.211.254}
|
|
||||||
- CADDY_DOMAIN=${CADDY_DOMAIN:-m040tbaraspi001.de040.corpintra.net}
|
|
||||||
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- frontend
|
|
||||||
- backend
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "caddy", "validate", "--config", "/etc/caddy/Caddyfile"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=false"
|
|
||||||
|
|
||||||
# === PERSISTENTE VOLUMES ===
|
|
||||||
volumes:
|
|
||||||
# Backend-Volumes
|
|
||||||
backend_instance:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: none
|
|
||||||
o: bind
|
|
||||||
device: ./backend/instance
|
|
||||||
|
|
||||||
backend_logs:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: none
|
|
||||||
o: bind
|
|
||||||
device: ./logs
|
|
||||||
|
|
||||||
backend_migrations:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: none
|
|
||||||
o: bind
|
|
||||||
device: ./backend/migrations
|
|
||||||
|
|
||||||
# Frontend-Volumes
|
|
||||||
frontend_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
frontend_db:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: none
|
|
||||||
o: bind
|
|
||||||
device: ./frontend/db
|
|
||||||
|
|
||||||
# Proxy-Volumes
|
|
||||||
caddy_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
caddy_config:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
caddy_logs:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
# === NETZWERK-KONFIGURATION ===
|
|
||||||
networks:
|
|
||||||
myp-network:
|
|
||||||
driver: bridge
|
|
||||||
ipam:
|
|
||||||
driver: default
|
|
||||||
config:
|
|
||||||
- subnet: 192.168.0.0/24
|
|
||||||
gateway: 192.168.0.1
|
|
||||||
driver_opts:
|
|
||||||
com.docker.network.enable_ipv6: "false"
|
|
||||||
com.docker.network.bridge.enable_ip_masquerade: "true"
|
|
||||||
com.docker.network.bridge.enable_icc: "true"
|
|
||||||
com.docker.network.bridge.host_binding_ipv4: "0.0.0.0"
|
|
||||||
labels:
|
|
||||||
- "description=MYP Anwendungs-Netzwerk"
|
|
||||||
- "project=myp"
|
|
||||||
- "environment=${NODE_ENV:-production}"
|
|
||||||
|
|
||||||
# === KONFIGURATIONSEXTENSIONEN ===
|
|
||||||
x-logging: &default-logging
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
labels: "service,environment"
|
|
||||||
|
|
||||||
x-restart-policy: &default-restart-policy
|
|
||||||
unless-stopped
|
|
||||||
|
|
||||||
x-healthcheck-defaults: &default-healthcheck
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
@ -1,53 +0,0 @@
|
|||||||
- HTTP Broadcast funktioniert nicht -> können IP nicht finden
|
|
||||||
- Backend im Unternehmen, Frontend auf GitHub funktioniert nicht -> Frontend hat HTTPS und Backend nur HTTP, self-signed certificate würde Fehler werfen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Knowledge Base - [PG Netzwerk] DHCP/DNS-Services: Anfragen und Änderungen von IP-Adressen/DNS (mercedes-benz.com)](https://servicenow.i.mercedes-benz.com/esc?id=kb_article&table=kb_knowledge&sysparm_article=KB0426678)
|
|
||||||
|
|
||||||
Statische IP beantragen
|
|
||||||
Alias Zuordnung (druckerapp.xyz.corpintra.net)
|
|
||||||
|
|
||||||
!!! SSL Zertifikat???
|
|
||||||
-> [Server-Zertifikat - Employee Center (mercedes-benz.com)](https://servicenow.i.mercedes-benz.com/esc?id=sc_cat_item&table=sc_cat_item&sys_id=7ef47a1c1b0d5450c4f43113dd4bcbf5)
|
|
||||||
8€ (wahrscheinlich einmalig)
|
|
||||||
benötigt Anwenderservice
|
|
||||||
[Application Service (Erstellen) - Employee Center (mercedes-benz.com)](https://servicenow.i.mercedes-benz.com/esc?id=sc_cat_item&table=sc_cat_item&sys_id=ab095def1bf68810c4f43113dd4bcb06)
|
|
||||||
|
|
||||||
|
|
||||||
-> [ServiceNow-Berechtigungen - Employee Center (mercedes-benz.com)](https://servicenow.i.mercedes-benz.com/esc?id=sc_cat_item&table=sc_cat_item&sys_id=8d645a30db3dc4501754ccd40596192c)
|
|
||||||
|
|
||||||
Sollten Sie für Ihre Applikation / Ihr Endgerät eine statisch zugewiesene IP Adresse (S-DHCP), einen manuellen IP-Pool (M-DHCP) oder eine entsprechende neue oder angepasste DNS-Zuordnung benötigen, nutzen Sie bitte die nachfolgenden SNOW-Templates (Hinweis zur Nutzung der Ticket-Templates benötigen Sie die **Service Now - Rechte eines "Agent Fulfilers"**. Die Beantragung für diese Rechte ist über das SNOW Employee Service Portal im Social Intranet möglich: [ServiceNow Permissions - Employee Service Center (mercedes-benz.com))](https://servicenow.i.mercedes-benz.com/esc?id=sc_cat_item&table=sc_cat_item&sys_id=8d645a30db3dc4501754ccd40596192c).
|
|
||||||
-> Martin diesbezüglich Fragen, ob er Sie für sich bestellt?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- Martin Rechte SNOW
|
|
||||||
- Martin Termin Volker Otto
|
|
||||||
|
|
||||||
[Examples · Diagrams (mingrammer.com)](https://diagrams.mingrammer.com/docs/getting-started/examples)
|
|
||||||
|
|
||||||
Hier ist eine Antwort, die ich mit Microsoft Copilot erhalten habe, der weltweit ersten KI-gestützten Antwort-Engine. Wählen Sie diese Option aus, um die vollständige Antwort anzuzeigen, oder probieren Sie sie selbst aus. https://sl.bing.net/iv24ZLcukjk
|
|
||||||
|
|
||||||
- use devcontainer !!!
|
|
||||||
|
|
||||||
https://x.com/tremorlabs/status/1779873516533547064?s=46
|
|
||||||
|
|
||||||
use mix of shadcn ui and tremor.so blocks
|
|
||||||
|
|
||||||
|
|
||||||
Bestellliste:
|
|
||||||
- 1x Switch
|
|
||||||
- 1x wlan access point
|
|
||||||
- 2x Raspberry Pi (4B / 5)
|
|
||||||
(- Xx LAN-Kabel)
|
|
||||||
- Adapter für 3D-Drucker oder Schaltbare Steckdosen
|
|
||||||
|
|
||||||
-> Datensicherung?!?!?!
|
|
||||||
make it a user thing
|
|
||||||
download db copy
|
|
||||||
import/export
|
|
||||||
OR Zugang zu CORP O über Anwendungsservice??
|
|
||||||
|
|
||||||
|
|
||||||
Move TRILLUM to GITHUB WIKI
|
|
@ -1,51 +0,0 @@
|
|||||||
# Aufräumarbeiten MYP-Projekt (19.05.2025)
|
|
||||||
|
|
||||||
## Durchgeführte Änderungen
|
|
||||||
|
|
||||||
### Verzeichnisstruktur
|
|
||||||
|
|
||||||
- Skriptdateien in logische Kategorien reorganisiert:
|
|
||||||
- `scripts/setup/`: Einrichtungsskripte
|
|
||||||
- `scripts/deployment/`: Bereitstellungsskripte für Raspberry Pi
|
|
||||||
- Neue Verzeichnisse erstellt:
|
|
||||||
- `logs/`: Für Fehlerprotokolle und Logdateien
|
|
||||||
- `config/secure/`: Für sensible Konfigurationsdaten
|
|
||||||
|
|
||||||
### Dokumentation
|
|
||||||
|
|
||||||
- Zentrale `Dokumentation.md` aktualisiert und erweitert
|
|
||||||
- Entwicklungsrichtlinien von `CLAUDE.md` nach `docs/Entwicklungsrichtlinien.md` verschoben
|
|
||||||
- README.md korrigiert und Git-Konfliktmarkierungen entfernt
|
|
||||||
|
|
||||||
### Sicherheit
|
|
||||||
|
|
||||||
- Sensible Daten (CREDENTIALS) in `config/secure/` verschoben
|
|
||||||
- `.gitignore` aktualisiert, um sensible Dateien und temporäre Dateien auszuschließen
|
|
||||||
|
|
||||||
### Dateiorganisation
|
|
||||||
|
|
||||||
- Fehlerlogs in `logs/` verschoben
|
|
||||||
- Temporäre Dateien bereinigt
|
|
||||||
- Skript-Dateien in sinnvolle Kategorien einsortiert
|
|
||||||
|
|
||||||
## Projektstruktur nach Aufräumarbeiten
|
|
||||||
|
|
||||||
```
|
|
||||||
Projektarbeit-MYP/
|
|
||||||
├── backend/ # Flask-Backend
|
|
||||||
├── config/
|
|
||||||
│ └── secure/ # Sensible Konfigurationen
|
|
||||||
├── docs/ # Projektdokumentation
|
|
||||||
├── frontend/
|
|
||||||
├── logs/ # Fehlerprotokolle
|
|
||||||
└── scripts/
|
|
||||||
├── deployment/ # Raspberry Pi Deployment
|
|
||||||
└── setup/ # Einrichtungsskripte
|
|
||||||
```
|
|
||||||
|
|
||||||
## Empfehlungen für zukünftige Arbeiten
|
|
||||||
|
|
||||||
- Gemeinsames Datenbankmodell zwischen Backend und Frontend überarbeiten
|
|
||||||
- Weitere Dokumentation der API-Schnittstellen erstellen
|
|
||||||
- Testabdeckung erhöhen
|
|
||||||
- Deployment-Prozess automatisieren
|
|
@ -1,59 +0,0 @@
|
|||||||
# MYP Project Development Guidelines
|
|
||||||
|
|
||||||
## System Architecture
|
|
||||||
|
|
||||||
- **Frontend**:
|
|
||||||
|
|
||||||
- Located in `frontend`
|
|
||||||
- Runs on a Raspberry Pi connected to company network
|
|
||||||
- Has internet access on one interface
|
|
||||||
- Connected via LAN to an offline network
|
|
||||||
- Serves as the user interface
|
|
||||||
- Developed by another apprentice as part of IHK project work
|
|
||||||
- **Backend**:
|
|
||||||
|
|
||||||
- Located in `backend` directory
|
|
||||||
- Flask application running on a separate Raspberry Pi
|
|
||||||
- Connected only to the offline network
|
|
||||||
- Communicates with WiFi smart plugs
|
|
||||||
- Part of my IHK project work for digital networking qualification
|
|
||||||
- **Printers/Smart Plugs**:
|
|
||||||
|
|
||||||
- Printers can only be controlled (on/off) via WiFi smart plugs
|
|
||||||
- No other control mechanisms available
|
|
||||||
- Smart plugs and printers are equivalent in the system context
|
|
||||||
|
|
||||||
## Build/Run Commands
|
|
||||||
|
|
||||||
- Backend: `cd backend && source venv/bin/activate && python app.py`
|
|
||||||
- Frontend: `cd frontend && pnpm dev`
|
|
||||||
- Run tests: `cd backend && python -m unittest development/tests/tests.py`
|
|
||||||
- Run single test: `cd backend && python -m unittest development.tests.tests.MYPBackendTestCase.test_name`
|
|
||||||
- Check jobs manually: `cd backend && source venv/bin/activate && flask check-jobs`
|
|
||||||
- Lint frontend: `cd frontend && pnpm lint`
|
|
||||||
- Format frontend: `cd frontend && npx @biomejs/biome format --write ./src`
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
- **Python Backend**:
|
|
||||||
|
|
||||||
- Use PEP 8 conventions, 4-space indentation
|
|
||||||
- Line width: 100 characters max
|
|
||||||
- Add docstrings to functions and classes
|
|
||||||
- Error handling: Use try/except with specific exceptions
|
|
||||||
- Naming: snake_case for functions/variables, PascalCase for classes
|
|
||||||
- **Frontend (Next.js/TypeScript)**:
|
|
||||||
|
|
||||||
- Use Biome for formatting and linting (line width: 120 chars)
|
|
||||||
- Organize imports automatically with Biome
|
|
||||||
- Use TypeScript types for all code
|
|
||||||
- Use React hooks for state management
|
|
||||||
- Naming: camelCase for functions/variables, PascalCase for components
|
|
||||||
|
|
||||||
## Work Guidelines
|
|
||||||
|
|
||||||
- All changes must be committed to git
|
|
||||||
- Work efficiently and cost-effectively
|
|
||||||
- Don't repeatedly try the same solution if it doesn't work
|
|
||||||
- Create and check notes when encountering issues
|
|
||||||
- Clearly communicate if something is not possible so I can handle it manually
|
|
@ -1,175 +0,0 @@
|
|||||||
# 🏗️ MYP Projektstruktur - Perfektionierte Architektur
|
|
||||||
|
|
||||||
## 📋 Übersicht
|
|
||||||
|
|
||||||
MYP (Manage your Printer) ist ein containerbasiertes Microservice-System mit klarer Trennung zwischen Frontend und Backend.
|
|
||||||
|
|
||||||
## 🗂️ Verzeichnisstruktur
|
|
||||||
|
|
||||||
```
|
|
||||||
Projektarbeit-MYP/
|
|
||||||
├── 🖥️ backend/ # Flask API Server (Port 5000)
|
|
||||||
│ ├── 📁 src/ # Hauptanwendungslogik
|
|
||||||
│ │ ├── 📁 api/ # API Endpunkte
|
|
||||||
│ │ ├── 📁 models/ # Datenmodelle
|
|
||||||
│ │ ├── 📁 services/ # Geschäftslogik
|
|
||||||
│ │ ├── 📁 auth/ # Authentifizierungslogik
|
|
||||||
│ │ └── 📁 utils/ # Hilfsfunktionen
|
|
||||||
│ ├── 📁 tests/ # Unit- und Integrationstests
|
|
||||||
│ ├── 📁 migrations/ # Datenbankmigrationen
|
|
||||||
│ ├── 📁 instance/ # SQLite Datenbank (gitignore)
|
|
||||||
│ ├── 📁 logs/ # Anwendungslogs (gitignore)
|
|
||||||
│ ├── 📁 config/ # Konfigurationsdateien
|
|
||||||
│ ├── 📄 Dockerfile # Backend Container
|
|
||||||
│ ├── 📄 requirements.txt # Python Abhängigkeiten
|
|
||||||
│ ├── 📄 app.py # Flask Anwendung Einstiegspunkt
|
|
||||||
│ └── 📄 .env.example # Umgebungsvariablen Vorlage
|
|
||||||
│
|
|
||||||
├── 🌐 frontend/ # Next.js Web Interface (Port 3000)
|
|
||||||
│ ├── 📁 src/ # Hauptanwendungslogik
|
|
||||||
│ │ ├── 📁 app/ # Next.js App Router
|
|
||||||
│ │ ├── 📁 components/ # React Komponenten
|
|
||||||
│ │ ├── 📁 lib/ # Bibliotheken und Utilities
|
|
||||||
│ │ └── 📁 types/ # TypeScript Typdefinitionen
|
|
||||||
│ ├── 📁 public/ # Statische Assets
|
|
||||||
│ ├── 📁 drizzle/ # Frontend Datenbankschema
|
|
||||||
│ ├── 📁 tests/ # Frontend Tests
|
|
||||||
│ ├── 📄 Dockerfile # Frontend Container
|
|
||||||
│ ├── 📄 package.json # Node.js Abhängigkeiten
|
|
||||||
│ ├── 📄 next.config.mjs # Next.js Konfiguration
|
|
||||||
│ └── 📄 .env.example # Umgebungsvariablen Vorlage
|
|
||||||
│
|
|
||||||
├── 🔄 proxy/ # Caddy Reverse Proxy (Port 80/443)
|
|
||||||
│ ├── 📄 Caddyfile # Proxy Konfiguration
|
|
||||||
│ └── 📄 docker-compose.caddy.yml # Caddy Service Definition
|
|
||||||
│
|
|
||||||
├── 📊 monitoring/ # Überwachung und Logging
|
|
||||||
│ ├── 📁 grafana/ # Dashboards
|
|
||||||
│ ├── 📁 prometheus/ # Metriken
|
|
||||||
│ └── 📄 docker-compose.monitoring.yml
|
|
||||||
│
|
|
||||||
├── 🔧 infrastructure/ # Infrastruktur-Konfiguration
|
|
||||||
│ ├── 📁 scripts/ # Automatisierungsskripte
|
|
||||||
│ │ ├── 📄 start.sh # Linux/MacOS Start
|
|
||||||
│ │ ├── 📄 start.ps1 # Windows Start
|
|
||||||
│ │ ├── 📄 cleanup.sh # Linux/MacOS Bereinigung
|
|
||||||
│ │ └── 📄 cleanup.ps1 # Windows Bereinigung
|
|
||||||
│ ├── 📁 environments/ # Umgebungskonfigurationen
|
|
||||||
│ │ ├── 📄 development.env # Entwicklungsumgebung
|
|
||||||
│ │ ├── 📄 production.env # Produktionsumgebung
|
|
||||||
│ │ └── 📄 testing.env # Testumgebung
|
|
||||||
│ └── 📁 ssl/ # SSL Zertifikate (gitignore)
|
|
||||||
│
|
|
||||||
├── 📚 docs/ # Projektdokumentation
|
|
||||||
│ ├── 📄 API.md # API Dokumentation
|
|
||||||
│ ├── 📄 DEPLOYMENT.md # Deployment Anweisungen
|
|
||||||
│ ├── 📄 DEVELOPMENT.md # Entwicklungsrichtlinien
|
|
||||||
│ └── 📄 ARCHITECTURE.md # Systemarchitektur
|
|
||||||
│
|
|
||||||
├── 🧪 tests/ # Übergreifende Tests
|
|
||||||
│ ├── 📁 integration/ # Integrationstests
|
|
||||||
│ ├── 📁 e2e/ # End-to-End Tests
|
|
||||||
│ └── 📄 docker-compose.test.yml # Test Environment
|
|
||||||
│
|
|
||||||
├── 📄 docker-compose.yml # Hauptkomposition (Prod)
|
|
||||||
├── 📄 docker-compose.dev.yml # Entwicklungsumgebung
|
|
||||||
├── 📄 docker-compose.override.yml # Lokale Overrides
|
|
||||||
├── 📄 .gitignore # Git Ignorierte Dateien
|
|
||||||
├── 📄 .dockerignore # Docker Ignorierte Dateien
|
|
||||||
├── 📄 README.md # Projekt Hauptdokumentation
|
|
||||||
└── 📄 PROJECT_STRUCTURE.md # Diese Strukturdokumentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔌 Service-Kommunikation
|
|
||||||
|
|
||||||
### Interne Container-Kommunikation
|
|
||||||
- **Frontend → Backend**: `http://backend:5000/api`
|
|
||||||
- **Proxy → Frontend**: `http://frontend:3000`
|
|
||||||
- **Proxy → Backend**: `http://backend:5000`
|
|
||||||
|
|
||||||
### Externe Zugriffe
|
|
||||||
- **Web Interface**: `https://localhost` (über Caddy Proxy)
|
|
||||||
- **API Direct**: `http://localhost:5000` (nur Entwicklung)
|
|
||||||
- **Frontend Direct**: `http://localhost:3000` (nur Entwicklung)
|
|
||||||
|
|
||||||
## 🌐 Netzwerk-Architektur
|
|
||||||
|
|
||||||
```
|
|
||||||
Internet/Intranet
|
|
||||||
↓
|
|
||||||
[Caddy Proxy] (80/443)
|
|
||||||
↓
|
|
||||||
┌─────────┬─────────┐
|
|
||||||
↓ ↓ ↓
|
|
||||||
[Frontend] [Backend] [Monitoring]
|
|
||||||
(3000) (5000) (9090/3001)
|
|
||||||
↓ ↓
|
|
||||||
[SQLite] [SQLite]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔐 Sicherheitskonzept
|
|
||||||
|
|
||||||
### Umgebungsvariablen
|
|
||||||
- Sensible Daten nur über `.env` Dateien
|
|
||||||
- Produktionsgeheimnisse über sichere Umgebungsvariablen
|
|
||||||
- Keine Geheimnisse in Git-Repository
|
|
||||||
|
|
||||||
### Netzwerksicherheit
|
|
||||||
- Container-isolierte Netzwerke
|
|
||||||
- Nur notwendige Ports exponiert
|
|
||||||
- HTTPS-Verschlüsselung über Caddy
|
|
||||||
- Firewall-kompatible Konfiguration
|
|
||||||
|
|
||||||
### Authentifizierung
|
|
||||||
- JWT-Token für API-Authentifizierung
|
|
||||||
- Session-basierte Frontend-Authentifizierung
|
|
||||||
- OAuth2 Integration vorbereitet
|
|
||||||
|
|
||||||
## 🚀 Deployment-Strategien
|
|
||||||
|
|
||||||
### Entwicklung
|
|
||||||
```bash
|
|
||||||
# Linux/MacOS
|
|
||||||
./infrastructure/scripts/start.sh dev
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
.\infrastructure\scripts\start.ps1 dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Produktion
|
|
||||||
```bash
|
|
||||||
# Linux/MacOS
|
|
||||||
./infrastructure/scripts/start.sh prod
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
.\infrastructure\scripts\start.ps1 prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📦 Container-Optimierungen
|
|
||||||
|
|
||||||
### Multi-Stage Builds
|
|
||||||
- Frontend: Node.js Build → Minimales Runtime Image
|
|
||||||
- Backend: Python Requirements → Optimiertes Laufzeit-Image
|
|
||||||
- Reduzierte Image-Größen und Sicherheitsoberfläche
|
|
||||||
|
|
||||||
### Health Checks
|
|
||||||
- Automatische Gesundheitsprüfungen für alle Services
|
|
||||||
- Graceful Restart bei Fehlern
|
|
||||||
- Monitoring-Integration
|
|
||||||
|
|
||||||
### Volume Management
|
|
||||||
- Persistente Datenbank-Volumes
|
|
||||||
- Log-Rotation und -Management
|
|
||||||
- Backup-freundliche Struktur
|
|
||||||
|
|
||||||
## 🔄 CI/CD Integration
|
|
||||||
|
|
||||||
### Git Hooks
|
|
||||||
- Pre-commit Linting und Testing
|
|
||||||
- Automatische Dependency Updates
|
|
||||||
- Security Scanning
|
|
||||||
|
|
||||||
### Container Registry
|
|
||||||
- Automatisches Image Building
|
|
||||||
- Versionierung über Git Tags
|
|
||||||
- Multi-Architecture Support (x86_64, ARM64)
|
|
@ -1,80 +0,0 @@
|
|||||||
# MYP OAuth Konfigurationsanleitung
|
|
||||||
|
|
||||||
Dieses Dokument beschreibt, wie die OAuth-Konfiguration für das MYP-Projekt eingerichtet wird.
|
|
||||||
|
|
||||||
## Überblick
|
|
||||||
|
|
||||||
Das MYP Frontend verwendet GitHub OAuth zur Authentifizierung. Die Konfiguration erfolgt über
|
|
||||||
Umgebungsvariablen, die in der Datei `/srv/myp-env/github.env` gespeichert werden.
|
|
||||||
|
|
||||||
## Konfiguration mit configure-oauth.sh
|
|
||||||
|
|
||||||
Wir haben ein Skript erstellt, um die OAuth-Konfiguration zu vereinfachen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ./configure-oauth.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Das Skript führt folgende Aktionen aus:
|
|
||||||
|
|
||||||
1. Erfasst die benötigten OAuth-Konfigurationsinformationen interaktiv
|
|
||||||
2. Speichert diese in `/srv/myp-env/github.env`
|
|
||||||
3. Aktualisiert einen laufenden Docker-Container (falls vorhanden)
|
|
||||||
4. Bietet die Option, den Container neu zu starten
|
|
||||||
|
|
||||||
## Benötigte Informationen
|
|
||||||
|
|
||||||
Für die Konfiguration werden folgende Informationen benötigt:
|
|
||||||
|
|
||||||
1. **GitHub-Typ**: GitHub Enterprise (git.i.mercedes-benz.com) oder GitHub.com
|
|
||||||
2. **OAuth Callback URL**: Die URL, zu der GitHub nach der Authentifizierung zurückleitet
|
|
||||||
- Standard für Unternehmensumgebung: `http://m040tbaraspi001.de040.corpintra.net/auth/login/callback`
|
|
||||||
- Standard für lokale Entwicklung: `http://localhost:3000/auth/login/callback`
|
|
||||||
3. **GitHub OAuth Client ID**: Von der GitHub OAuth App-Konfiguration
|
|
||||||
4. **GitHub OAuth Client Secret**: Von der GitHub OAuth App-Konfiguration
|
|
||||||
|
|
||||||
## Konfiguration der GitHub OAuth App
|
|
||||||
|
|
||||||
1. Navigieren Sie zu Ihren GitHub-Einstellungen (Organisationseinstellungen für Enterprise)
|
|
||||||
2. Wählen Sie "OAuth Apps" oder "Developer Settings" > "OAuth Apps"
|
|
||||||
3. Erstellen Sie eine neue OAuth App mit:
|
|
||||||
- **Name**: MYP (Manage Your Printer)
|
|
||||||
- **Homepage URL**: `http://m040tbaraspi001.de040.corpintra.net` (oder Ihre lokale URL)
|
|
||||||
- **Authorization callback URL**: Exakt die URL, die Sie auch im Skript konfigurieren
|
|
||||||
- **Description**: Optional
|
|
||||||
|
|
||||||
## Umgebungsvariablen
|
|
||||||
|
|
||||||
Die folgenden Umgebungsvariablen werden vom Skript konfiguriert:
|
|
||||||
|
|
||||||
```
|
|
||||||
# OAuth Callback URL
|
|
||||||
NEXT_PUBLIC_OAUTH_CALLBACK_URL=http://m040tbaraspi001.de040.corpintra.net/auth/login/callback
|
|
||||||
OAUTH_CALLBACK_URL=http://m040tbaraspi001.de040.corpintra.net/auth/login/callback
|
|
||||||
|
|
||||||
# GitHub OAuth Credentials
|
|
||||||
AUTH_GITHUB_CLIENT_ID=your_client_id
|
|
||||||
AUTH_GITHUB_CLIENT_SECRET=your_client_secret
|
|
||||||
|
|
||||||
# Kompatibilitäts-Variablen
|
|
||||||
OAUTH_CLIENT_ID=your_client_id
|
|
||||||
OAUTH_CLIENT_SECRET=your_client_secret
|
|
||||||
|
|
||||||
# GitHub Server-Konfiguration
|
|
||||||
GITHUB_ENTERPRISE=true
|
|
||||||
GITHUB_DOMAIN=https://git.i.mercedes-benz.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment mit OAuth-Konfiguration
|
|
||||||
|
|
||||||
Die Deployment-Skripte `raspi-frontend-deploy.sh` sind so konfiguriert, dass sie die OAuth-Konfiguration aus
|
|
||||||
`/srv/myp-env/github.env` laden und dem Container zur Verfügung stellen.
|
|
||||||
|
|
||||||
## Fehlerbehebung
|
|
||||||
|
|
||||||
Falls die OAuth-Authentifizierung nicht funktioniert:
|
|
||||||
|
|
||||||
1. Prüfen Sie, ob die Callback-URL exakt mit der in GitHub konfigurierten URL übereinstimmt
|
|
||||||
2. Stellen Sie sicher, dass der Docker-Container die Umgebungsvariablen korrekt übernimmt
|
|
||||||
3. Überprüfen Sie die Netzwerkerreichbarkeit zwischen dem Frontend und dem GitHub-Server
|
|
||||||
4. Prüfen Sie die Frontend-Logs auf OAuth-bezogene Fehlermeldungen
|
|
@ -1,287 +0,0 @@
|
|||||||
# 🏗️ MYP - Separate Server Architektur
|
|
||||||
|
|
||||||
## Übersicht
|
|
||||||
|
|
||||||
Das MYP-System wurde in **zwei vollständig unabhängige Server** aufgeteilt:
|
|
||||||
|
|
||||||
- **🏭 Backend-Server** (Port 5000): Flask-API für Geschäftslogik und Datenmanagement
|
|
||||||
- **🎨 Frontend-Server** (Port 3000): Next.js-Anwendung für Benutzeroberfläche
|
|
||||||
|
|
||||||
## 🔗 Server-Kommunikation
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐ HTTP/API ┌─────────────────┐
|
|
||||||
│ Frontend │◄───────────────►│ Backend │
|
|
||||||
│ (Next.js) │ │ (Flask) │
|
|
||||||
│ Port: 3000 │ │ Port: 5000 │
|
|
||||||
└─────────────────┘ └─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Separate Server starten
|
|
||||||
|
|
||||||
### Backend-Server (unabhängig)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
./start-backend-server.sh
|
|
||||||
|
|
||||||
# Alternative mit Logs
|
|
||||||
./start-backend-server.sh --logs
|
|
||||||
|
|
||||||
# Vollständige Neuinstallation
|
|
||||||
./start-backend-server.sh --clean
|
|
||||||
```
|
|
||||||
|
|
||||||
**Backend verfügbar unter:**
|
|
||||||
|
|
||||||
- 📡 Backend-API: http://localhost:5000
|
|
||||||
- 🔧 Health-Check: http://localhost:5000/health
|
|
||||||
- 📋 API-Docs: http://localhost:5000/swagger
|
|
||||||
|
|
||||||
### Frontend-Server (unabhängig)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
./start-frontend-server.sh
|
|
||||||
|
|
||||||
# Alternative mit Logs
|
|
||||||
./start-frontend-server.sh --logs
|
|
||||||
|
|
||||||
# Vollständige Neuinstallation
|
|
||||||
./start-frontend-server.sh --clean
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend verfügbar unter:**
|
|
||||||
|
|
||||||
- 🌐 Web-App: http://localhost:3000
|
|
||||||
- 🔧 Health-Check: http://localhost:3000/health
|
|
||||||
- 📦 CDN-Assets: http://localhost:8080
|
|
||||||
|
|
||||||
## ⚙️ Konfiguration
|
|
||||||
|
|
||||||
### Backend-Konfiguration
|
|
||||||
|
|
||||||
**Datei:** `backend/env.backend`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend-Server
|
|
||||||
PORT=5000
|
|
||||||
BACKEND_URL=http://localhost:5000
|
|
||||||
|
|
||||||
# CORS für Frontend-Zugriff
|
|
||||||
CORS_ORIGINS=http://localhost:3000,https://frontend.myp.local
|
|
||||||
|
|
||||||
# Datenbank
|
|
||||||
DATABASE_PATH=instance/myp.db
|
|
||||||
|
|
||||||
# Cache
|
|
||||||
REDIS_HOST=localhost
|
|
||||||
REDIS_PORT=6379
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend-Konfiguration
|
|
||||||
|
|
||||||
**Datei:** `frontend/env.frontend`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Frontend-Server
|
|
||||||
PORT=3000
|
|
||||||
FRONTEND_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# Backend-API Verbindung
|
|
||||||
BACKEND_API_URL=http://localhost:5000/api
|
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:5000/api
|
|
||||||
|
|
||||||
# Cache
|
|
||||||
FRONTEND_REDIS_PORT=6380
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐋 Docker-Container
|
|
||||||
|
|
||||||
### Backend-Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
docker-compose -f docker-compose.backend.yml up -d
|
|
||||||
|
|
||||||
# Services:
|
|
||||||
# - myp-backend-standalone (Port 5000)
|
|
||||||
# - myp-backend-db (PostgreSQL, Port 5432)
|
|
||||||
# - myp-backend-cache (Redis, Port 6379)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend-Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
docker-compose -f docker-compose.frontend.yml up -d
|
|
||||||
|
|
||||||
# Services:
|
|
||||||
# - myp-frontend-standalone (Port 3000)
|
|
||||||
# - myp-frontend-cache (Redis, Port 6380)
|
|
||||||
# - myp-frontend-cdn (Nginx, Port 8080)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 Sicherheit
|
|
||||||
|
|
||||||
### CORS-Konfiguration
|
|
||||||
|
|
||||||
Das Backend ist für **explizite Frontend-Origins** konfiguriert:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Backend: app.py
|
|
||||||
CORS(app,
|
|
||||||
origins=['http://localhost:3000', 'https://frontend.myp.local'],
|
|
||||||
supports_credentials=True,
|
|
||||||
allow_headers=['Content-Type', 'Authorization', 'X-Requested-With'],
|
|
||||||
methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
|
|
||||||
```
|
|
||||||
|
|
||||||
### API-Authentifizierung
|
|
||||||
|
|
||||||
- **JWT-Tokens** für API-Zugriff
|
|
||||||
- **Session-basierte** Authentifizierung für Web-UI
|
|
||||||
- **Separate Secrets** für Frontend und Backend
|
|
||||||
|
|
||||||
## 📊 Monitoring
|
|
||||||
|
|
||||||
### Health-Checks
|
|
||||||
|
|
||||||
**Backend:** GET http://localhost:5000/health
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "healthy",
|
|
||||||
"service": "myp-backend",
|
|
||||||
"database": "connected",
|
|
||||||
"timestamp": "2024-01-15T10:30:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend:** GET http://localhost:3000/health
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "healthy",
|
|
||||||
"service": "myp-frontend",
|
|
||||||
"backend": {
|
|
||||||
"url": "http://localhost:5000",
|
|
||||||
"status": "connected"
|
|
||||||
},
|
|
||||||
"timestamp": "2024-01-15T10:30:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logs verfolgen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend-Logs
|
|
||||||
cd backend
|
|
||||||
docker-compose -f docker-compose.backend.yml logs -f
|
|
||||||
|
|
||||||
# Frontend-Logs
|
|
||||||
cd frontend
|
|
||||||
docker-compose -f docker-compose.frontend.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Troubleshooting
|
|
||||||
|
|
||||||
### Backend startet nicht
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Prüfe Ports
|
|
||||||
netstat -an | grep 5000
|
|
||||||
|
|
||||||
# 2. Prüfe Backend-Logs
|
|
||||||
cd backend
|
|
||||||
docker-compose -f docker-compose.backend.yml logs backend
|
|
||||||
|
|
||||||
# 3. Datenbank-Migration
|
|
||||||
docker-compose -f docker-compose.backend.yml exec backend flask db upgrade
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend kann Backend nicht erreichen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Prüfe Backend-Verfügbarkeit
|
|
||||||
curl http://localhost:5000/health
|
|
||||||
|
|
||||||
# 2. Prüfe CORS-Konfiguration
|
|
||||||
curl -H "Origin: http://localhost:3000" \
|
|
||||||
-H "Access-Control-Request-Method: GET" \
|
|
||||||
-H "Access-Control-Request-Headers: Content-Type" \
|
|
||||||
-X OPTIONS http://localhost:5000/api/printers
|
|
||||||
|
|
||||||
# 3. Prüfe Frontend-Umgebungsvariablen
|
|
||||||
cd frontend
|
|
||||||
grep BACKEND env.frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port-Konflikte
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Standard-Ports ändern
|
|
||||||
# Backend: PORT=5001 in env.backend
|
|
||||||
# Frontend: PORT=3001 in env.frontend
|
|
||||||
|
|
||||||
# Neue URLs in beiden Konfigurationen anpassen
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 Deployment
|
|
||||||
|
|
||||||
### Produktions-Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend (Server 1)
|
|
||||||
cd backend
|
|
||||||
FLASK_ENV=production ./start-backend-server.sh
|
|
||||||
|
|
||||||
# Frontend (Server 2)
|
|
||||||
cd frontend
|
|
||||||
NODE_ENV=production BACKEND_API_URL=https://api.myp.de ./start-frontend-server.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reverse Proxy (Optional)
|
|
||||||
|
|
||||||
Für Produktion können beide Server hinter einem Reverse Proxy laufen:
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
||||||
│ Client │────►│ Nginx/Caddy │────►│ Frontend │
|
|
||||||
│ │ │ │ ┌─►│ :3000 │
|
|
||||||
└─────────────┘ │ /api/* ─────┤──┘ └─────────────┘
|
|
||||||
│ │ ┌─►┌─────────────┐
|
|
||||||
│ /api/* ─────┤──┘ │ Backend │
|
|
||||||
└─────────────┘ │ :5000 │
|
|
||||||
└─────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Dateistruktur
|
|
||||||
|
|
||||||
```
|
|
||||||
Projektarbeit-MYP/
|
|
||||||
├── backend/ # 🏭 Backend-Server
|
|
||||||
│ ├── docker-compose.backend.yml # Docker-Konfiguration
|
|
||||||
│ ├── start-backend-server.sh # Start-Skript
|
|
||||||
│ ├── env.backend # Umgebungsvariablen
|
|
||||||
│ └── app.py # Flask-Anwendung
|
|
||||||
├── frontend/ # 🎨 Frontend-Server
|
|
||||||
│ ├── docker-compose.frontend.yml # Docker-Konfiguration
|
|
||||||
│ ├── start-frontend-server.sh # Start-Skript
|
|
||||||
│ ├── env.frontend # Umgebungsvariablen
|
|
||||||
│ └── src/ # Next.js-Anwendung
|
|
||||||
└── SEPARATE_SERVERS_GUIDE.md # Diese Dokumentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✅ Vorteile der Trennung
|
|
||||||
|
|
||||||
1. **🔄 Unabhängige Skalierung** - Frontend und Backend können getrennt skaliert werden
|
|
||||||
2. **🚀 Separate Deployments** - Updates können unabhängig deployed werden
|
|
||||||
3. **🛡️ Verbesserte Sicherheit** - Klare API-Grenzen und CORS-Kontrolle
|
|
||||||
4. **🔧 Technologie-Flexibilität** - Frontend und Backend können verschiedene Technologien verwenden
|
|
||||||
5. **📊 Besseres Monitoring** - Separate Health-Checks und Logs
|
|
||||||
6. **🏗️ Microservice-Ready** - Vorbereitung für Microservice-Architektur
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Hinweis:** Die alte gekoppelte Konfiguration ist weiterhin in `docker-compose.yml` verfügbar, wird aber für neue Deployments nicht empfohlen.
|
|
Loading…
x
Reference in New Issue
Block a user