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:
Till Tomczak 2025-05-24 17:47:05 +02:00
parent d2f23d589a
commit ead75ae451
98 changed files with 3917 additions and 35610 deletions

View File

@ -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
View File

@ -1,475 +0,0 @@
# 🖨️ MYP - Manage your Printer
[![Docker](https://img.shields.io/badge/Docker-Ready-blue?logo=docker)](https://docker.com)
[![Linux](https://img.shields.io/badge/Linux-Compatible-green?logo=linux)](https://linux.org)
[![Windows](https://img.shields.io/badge/Windows-Compatible-blue?logo=windows)](https://windows.com)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)
[![Version](https://img.shields.io/badge/Version-2.0.0-brightgreen.svg)](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**

View File

@ -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.

View File

@ -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
View File

@ -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
View 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.

View File

@ -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"]

View File

@ -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"]

View File

@ -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
View 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

View File

@ -1 +0,0 @@

217
backend/ROADMAP.md Normal file
View 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

File diff suppressed because it is too large Load Diff

1001
backend/app/app.py Normal file

File diff suppressed because it is too large Load Diff

View 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
View 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()

View File

@ -0,0 +1 @@
# Utils package for MYP

View 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()

View 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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

File diff suppressed because one or more lines are too long

View File

@ -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">&times;</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

View File

@ -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)';
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>&copy; 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>

View File

@ -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

View File

@ -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 ""

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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 |

View 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
View 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

View File

@ -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
```

View File

@ -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
View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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!"

View File

@ -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 "$@"

View 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
View 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/ &

View 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

View 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
View 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
View 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

View File

@ -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

View File

@ -1 +0,0 @@
Single-database configuration for Flask.

View File

@ -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

View File

@ -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()

View File

@ -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"}

View File

@ -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}")

View File

@ -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 ###

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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)

View File

@ -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'))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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.