"feat: Added debug server and related components for improved development experience"
This commit is contained in:
parent
d457a8d86b
commit
9f6219832c
47
.gitattributes
vendored
47
.gitattributes
vendored
@ -1,47 +0,0 @@
|
|||||||
# Standard: alles als Text behandeln und LF speichern
|
|
||||||
* text=auto eol=lf
|
|
||||||
|
|
||||||
# Beispiel für Ausnahmen
|
|
||||||
*.png binary
|
|
||||||
*.jpg binary
|
|
||||||
|
|
||||||
|
|
||||||
# Shell-Skripte: immer LF
|
|
||||||
*.sh text eol=lf
|
|
||||||
|
|
||||||
# Windows-Batch-Dateien: immer CRLF
|
|
||||||
*.bat text eol=crlf
|
|
||||||
*.cmd text eol=crlf
|
|
||||||
|
|
||||||
# Python, JS, HTML etc.: bevorzugt LF (aber nicht erzwungen)
|
|
||||||
*.py text eol=lf
|
|
||||||
*.js text eol=lf
|
|
||||||
*.ts text eol=lf
|
|
||||||
*.html text eol=lf
|
|
||||||
*.css text eol=lf
|
|
||||||
*.json text eol=lf
|
|
||||||
*.yml text eol=lf
|
|
||||||
*.yaml text eol=lf
|
|
||||||
*.md text eol=lf
|
|
||||||
|
|
||||||
# Konfigurationsdateien
|
|
||||||
*.env text eol=lf
|
|
||||||
*.conf text eol=lf
|
|
||||||
*.ini text eol=lf
|
|
||||||
|
|
||||||
# Binärdateien
|
|
||||||
*.png binary
|
|
||||||
*.jpg binary
|
|
||||||
*.jpeg binary
|
|
||||||
*.gif binary
|
|
||||||
*.webp binary
|
|
||||||
*.pdf binary
|
|
||||||
*.ico binary
|
|
||||||
*.ttf binary
|
|
||||||
*.woff binary
|
|
||||||
*.zip binary
|
|
||||||
*.tar binary
|
|
||||||
*.gz binary
|
|
||||||
*.7z binary
|
|
||||||
*.exe binary
|
|
||||||
*.dll binary
|
|
457
.gitignore
vendored
457
.gitignore
vendored
@ -1,87 +1,398 @@
|
|||||||
# MYP - Manage your Printer .gitignore
|
# 📦 MYP - Manage your Printer .gitignore
|
||||||
|
# Umfassende Git-Ignore-Konfiguration für Microservice-Architektur
|
||||||
|
|
||||||
# Betriebssystem-spezifische Dateien
|
# ========================================================================================
|
||||||
.DS_Store
|
# 🏗️ INFRASTRUKTUR UND CONTAINER
|
||||||
Thumbs.db
|
# ========================================================================================
|
||||||
desktop.ini
|
|
||||||
|
|
||||||
# Sensible Daten und Konfigurationen
|
# Docker
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.override.yml
|
||||||
|
**/.docker/
|
||||||
|
**/Dockerfile.local
|
||||||
|
**/*.dockerfile.local
|
||||||
|
|
||||||
|
# Container-Volumes und -Daten
|
||||||
|
volumes/
|
||||||
|
data/
|
||||||
|
**/instance/
|
||||||
|
**/logs/
|
||||||
|
caddy_data/
|
||||||
|
caddy_config/
|
||||||
|
|
||||||
|
# Monitoring-Daten
|
||||||
|
monitoring/prometheus/data/
|
||||||
|
monitoring/grafana/data/
|
||||||
|
monitoring/grafana/logs/
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# 🔐 SICHERHEIT UND GEHEIMNISSE
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
# Umgebungsvariablen und Geheimnisse
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
!**/.env.example
|
||||||
|
**/*.pem
|
||||||
|
**/*.key
|
||||||
|
**/*.cer
|
||||||
|
**/*.crt
|
||||||
|
**/*.p12
|
||||||
|
**/*.pfx
|
||||||
|
|
||||||
|
# Sichere Konfigurationen
|
||||||
config/secure/
|
config/secure/
|
||||||
*.env
|
infrastructure/ssl/
|
||||||
*.pem
|
**/secrets/
|
||||||
*.key
|
**/private/
|
||||||
*.cer
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# Python spezifische Dateien
|
# SSH-Schlüssel
|
||||||
__pycache__/
|
**/.ssh/
|
||||||
*.py[cod]
|
**/id_rsa*
|
||||||
*$py.class
|
**/id_ed25519*
|
||||||
*.so
|
|
||||||
.Python
|
|
||||||
env/
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
|
|
||||||
# JavaScript/Node spezifische Dateien
|
# ========================================================================================
|
||||||
node_modules/
|
# 🐍 PYTHON/FLASK BACKEND
|
||||||
npm-debug.log*
|
# ========================================================================================
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
.next/
|
|
||||||
out/
|
|
||||||
.vercel
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# IDE Dateien
|
# Python-Bytecode
|
||||||
.idea/
|
**/__pycache__/
|
||||||
|
**/*.py[cod]
|
||||||
|
**/*$py.class
|
||||||
|
**/*.so
|
||||||
|
|
||||||
|
# Verteilung / Paketierung
|
||||||
|
backend/build/
|
||||||
|
backend/develop-eggs/
|
||||||
|
backend/dist/
|
||||||
|
backend/downloads/
|
||||||
|
backend/eggs/
|
||||||
|
backend/.eggs/
|
||||||
|
backend/lib/
|
||||||
|
backend/lib64/
|
||||||
|
backend/parts/
|
||||||
|
backend/sdist/
|
||||||
|
backend/var/
|
||||||
|
backend/wheels/
|
||||||
|
backend/share/python-wheels/
|
||||||
|
backend/*.egg-info/
|
||||||
|
backend/.installed.cfg
|
||||||
|
backend/*.egg
|
||||||
|
backend/MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
backend/*.manifest
|
||||||
|
backend/*.spec
|
||||||
|
|
||||||
|
# Unit-Test / Coverage-Berichte
|
||||||
|
backend/htmlcov/
|
||||||
|
backend/.tox/
|
||||||
|
backend/.nox/
|
||||||
|
backend/.coverage
|
||||||
|
backend/.coverage.*
|
||||||
|
backend/.cache
|
||||||
|
backend/nosetests.xml
|
||||||
|
backend/coverage.xml
|
||||||
|
backend/*.cover
|
||||||
|
backend/*.py,cover
|
||||||
|
backend/.hypothesis/
|
||||||
|
backend/.pytest_cache/
|
||||||
|
backend/cover/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
backend/.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
backend/profile_default/
|
||||||
|
backend/ipython_config.py
|
||||||
|
|
||||||
|
# Umgebungen
|
||||||
|
backend/.env
|
||||||
|
backend/.venv
|
||||||
|
backend/env/
|
||||||
|
backend/venv/
|
||||||
|
backend/ENV/
|
||||||
|
backend/env.bak/
|
||||||
|
backend/venv.bak/
|
||||||
|
|
||||||
|
# Spyder-Projekt-Einstellungen
|
||||||
|
backend/.spyderproject
|
||||||
|
backend/.spyproject
|
||||||
|
|
||||||
|
# Rope-Projekt-Einstellungen
|
||||||
|
backend/.ropeproject
|
||||||
|
|
||||||
|
# mkdocs-Dokumentation
|
||||||
|
backend/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
backend/.mypy_cache/
|
||||||
|
backend/.dmypy.json
|
||||||
|
backend/dmypy.json
|
||||||
|
|
||||||
|
# Pyre Type Checker
|
||||||
|
backend/.pyre/
|
||||||
|
|
||||||
|
# pytype Static Type Analyzer
|
||||||
|
backend/.pytype/
|
||||||
|
|
||||||
|
# Cython Debug-Symbole
|
||||||
|
backend/cython_debug/
|
||||||
|
|
||||||
|
# Spezifische Backend-Dateien
|
||||||
|
backend/instance/
|
||||||
|
backend/logs/
|
||||||
|
backend/*.db
|
||||||
|
backend/*.sqlite
|
||||||
|
backend/*.sqlite3
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# 📱 NODE.JS/NEXT.JS FRONTEND
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
# Abhängigkeiten
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/.pnp
|
||||||
|
frontend/.pnp.js
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
frontend/coverage/
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
frontend/.next/
|
||||||
|
frontend/out/
|
||||||
|
|
||||||
|
# Produktions-Build
|
||||||
|
frontend/build
|
||||||
|
|
||||||
|
# Verschiedenes
|
||||||
|
frontend/.DS_Store
|
||||||
|
frontend/*.tsbuildinfo
|
||||||
|
frontend/next-env.d.ts
|
||||||
|
|
||||||
|
# Debug-Logs
|
||||||
|
frontend/npm-debug.log*
|
||||||
|
frontend/yarn-debug.log*
|
||||||
|
frontend/yarn-error.log*
|
||||||
|
frontend/.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Lokale Umgebungsdateien
|
||||||
|
frontend/.env
|
||||||
|
frontend/.env.local
|
||||||
|
frontend/.env.development.local
|
||||||
|
frontend/.env.test.local
|
||||||
|
frontend/.env.production.local
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
frontend/.vercel
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
frontend/*.tsbuildinfo
|
||||||
|
|
||||||
|
# Storybook-Build-Ausgaben
|
||||||
|
frontend/storybook-static
|
||||||
|
|
||||||
|
# Datenbank
|
||||||
|
frontend/db/
|
||||||
|
frontend/*.db
|
||||||
|
frontend/*.sqlite
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# 💻 ENTWICKLUNGSUMGEBUNG UND IDE
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# JetBrains IDEs
|
||||||
|
.idea/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# Sublime Text
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Vim
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
.project
|
.vimrc.local
|
||||||
.classpath
|
|
||||||
.settings/
|
|
||||||
|
|
||||||
# Logs und temporäre Dateien
|
# Emacs
|
||||||
logs/
|
*~
|
||||||
*.log
|
\#*\#
|
||||||
npm-debug.log*
|
/.emacs.desktop
|
||||||
yarn-debug.log*
|
/.emacs.desktop.lock
|
||||||
yarn-error.log*
|
*.elc
|
||||||
temp/
|
auto-save-list
|
||||||
|
tramp
|
||||||
|
.\#*
|
||||||
|
|
||||||
|
# Eclipse
|
||||||
|
.metadata
|
||||||
|
bin/
|
||||||
tmp/
|
tmp/
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*~.nib
|
||||||
|
local.properties
|
||||||
|
.settings/
|
||||||
|
.loadpath
|
||||||
|
.recommenders
|
||||||
|
|
||||||
# Datenbanken und SQLite Dateien
|
# NetBeans
|
||||||
*.sqlite
|
/nbproject/private/
|
||||||
*.sqlite3
|
/nbbuild/
|
||||||
*.db
|
/dist/
|
||||||
instance/
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
# Kompilierte Dateien und Binaries
|
# ========================================================================================
|
||||||
*.com
|
# 🖥️ BETRIEBSSYSTEM
|
||||||
*.class
|
# ========================================================================================
|
||||||
*.dll
|
|
||||||
*.exe
|
# Windows
|
||||||
*.o
|
Thumbs.db
|
||||||
*.a
|
Thumbs.db:encryptable
|
||||||
*.so
|
ehthumbs.db
|
||||||
*.dylib
|
ehthumbs_vista.db
|
||||||
|
*.stackdump
|
||||||
|
[Dd]esktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon
|
||||||
|
._*
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
*~
|
||||||
|
.fuse_hidden*
|
||||||
|
.directory
|
||||||
|
.Trash-*
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# 📊 MONITORING UND LOGGING
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
# Log-Dateien
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
**/*.log
|
||||||
|
**/*.log.*
|
||||||
|
|
||||||
|
# Monitoring-Daten
|
||||||
|
prometheus/data/
|
||||||
|
grafana/data/
|
||||||
|
grafana/logs/
|
||||||
|
|
||||||
|
# Backup-Dateien
|
||||||
|
*.backup
|
||||||
|
*.bak
|
||||||
|
backups/
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# 🧪 TESTING UND QUALITÄTSSICHERUNG
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
# Test-Ergebnisse
|
||||||
|
test-results/
|
||||||
|
test-reports/
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Jest
|
||||||
|
**/.jest/
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
**/cypress/videos/
|
||||||
|
**/cypress/screenshots/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
playwright/.cache/
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# 📦 PAKETIERUNG UND VERTEILUNG
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
# Tar-Archive
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
|
*.tar.bz2
|
||||||
|
*.tar.xz
|
||||||
|
|
||||||
|
# Komprimierte Dateien
|
||||||
|
*.zip
|
||||||
|
*.rar
|
||||||
|
*.7z
|
||||||
|
|
||||||
|
# Build-Artefakte
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# 🔄 TEMPORÄRE UND CACHE-DATEIEN
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
# Allgemeine temporäre Dateien
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
.tmp/
|
||||||
|
.temp/
|
||||||
|
|
||||||
|
# Cache-Verzeichnisse
|
||||||
|
.cache/
|
||||||
|
**/.cache/
|
||||||
|
.eslintcache
|
||||||
|
.parcel-cache/
|
||||||
|
|
||||||
|
# Lock-Dateien (falls gewünscht - auskommentieren)
|
||||||
|
# package-lock.json
|
||||||
|
# yarn.lock
|
||||||
|
# pnpm-lock.yaml
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# 🏭 PRODUKTIONSSPEZIFISCHE DATEIEN
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
# Produktions-Konfigurationen
|
||||||
|
docker-compose.prod.yml
|
||||||
|
docker-compose.production.yml
|
||||||
|
production.env
|
||||||
|
|
||||||
|
# SSL-Zertifikate für Produktion
|
||||||
|
ssl/
|
||||||
|
certificates/
|
||||||
|
certs/
|
||||||
|
|
||||||
|
# Backup-Skripte und -Daten
|
||||||
|
backup/
|
||||||
|
snapshots/
|
1
PROJECT_STRUCTURE.md
Normal file
1
PROJECT_STRUCTURE.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
176
backend/app.py
176
backend/app.py
@ -1,5 +1,4 @@
|
|||||||
from flask import Flask, request, jsonify, g, redirect, url_for, session as flask_session, render_template, flash
|
from flask import Flask, request, jsonify, g, redirect, url_for, session as flask_session, render_template, flash, send_from_directory
|
||||||
from flask_cors import CORS
|
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
import secrets # Für bessere Salt-Generierung
|
import secrets # Für bessere Salt-Generierung
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@ -17,20 +16,82 @@ from datetime import timedelta
|
|||||||
from PyP100 import PyP100
|
from PyP100 import PyP100
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Importiere Konfiguration
|
||||||
|
from config import config
|
||||||
|
|
||||||
# Importiere Netzwerkkonfiguration
|
# Importiere Netzwerkkonfiguration
|
||||||
from network_config import NetworkConfig
|
from network_config import NetworkConfig
|
||||||
|
|
||||||
|
# Importiere Frontend V2 Blueprint
|
||||||
|
from frontend_v2_routes import frontend_v2, set_app_functions
|
||||||
|
|
||||||
# Lade Umgebungsvariablen
|
# Lade Umgebungsvariablen
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Initialisierung
|
def create_app(config_name=None):
|
||||||
|
"""
|
||||||
|
Application Factory Pattern für die Flask-Anwendung.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_name: Name der zu verwendenden Konfiguration ('development', 'production', 'testing')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Flask: Konfigurierte Flask-Anwendung
|
||||||
|
"""
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Bestimme Konfiguration
|
||||||
|
if config_name is None:
|
||||||
|
config_name = os.environ.get('FLASK_ENV', 'development')
|
||||||
|
|
||||||
|
# Lade Konfiguration
|
||||||
|
config_object = config.get(config_name, config['default'])
|
||||||
|
app.config.from_object(config_object)
|
||||||
|
|
||||||
|
# Initialisiere Konfiguration
|
||||||
|
config_object.init_app(app)
|
||||||
|
|
||||||
|
# Initialisiere Netzwerkkonfiguration
|
||||||
|
network_config = NetworkConfig(app)
|
||||||
|
|
||||||
|
# Registriere Blueprint
|
||||||
|
app.register_blueprint(frontend_v2, url_prefix='/frontend_v2')
|
||||||
|
|
||||||
|
# Konfiguriere statische Dateien für Frontend v2
|
||||||
|
@app.route('/frontend_v2/static/<path:filename>')
|
||||||
|
def frontend_v2_static(filename):
|
||||||
|
return send_from_directory(os.path.join(app.root_path, 'frontend_v2/static'), filename)
|
||||||
|
|
||||||
|
# Globale Variablen
|
||||||
|
app.config['PRINTERS'] = json.loads(app.config.get('PRINTERS', '{}'))
|
||||||
|
|
||||||
|
# Database functions registrieren
|
||||||
|
register_database_functions(app)
|
||||||
|
|
||||||
|
# Authentifizierung registrieren
|
||||||
|
register_auth_functions(app)
|
||||||
|
|
||||||
|
# API-Routen registrieren
|
||||||
|
register_api_routes(app)
|
||||||
|
|
||||||
|
# Web-UI-Routen registrieren
|
||||||
|
register_web_routes(app)
|
||||||
|
|
||||||
|
# Error-Handler registrieren
|
||||||
|
register_error_handlers(app)
|
||||||
|
|
||||||
|
# Hintergrund-Tasks registrieren
|
||||||
|
register_background_tasks(app)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
# Initialisierung - wird später durch create_app ersetzt
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app, supports_credentials=True)
|
|
||||||
|
|
||||||
# Initialisiere Netzwerkkonfiguration
|
# Initialisiere Netzwerkkonfiguration
|
||||||
network_config = NetworkConfig(app)
|
network_config = NetworkConfig(app)
|
||||||
|
|
||||||
# Konfiguration
|
# Temporäre Konfiguration für Legacy-Code
|
||||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev_secret_key')
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev_secret_key')
|
||||||
app.config['DATABASE'] = os.environ.get('DATABASE_PATH', 'instance/myp.db')
|
app.config['DATABASE'] = os.environ.get('DATABASE_PATH', 'instance/myp.db')
|
||||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||||
@ -39,11 +100,65 @@ app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
|||||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
|
||||||
app.config['JOB_CHECK_INTERVAL'] = int(os.environ.get('JOB_CHECK_INTERVAL', '60')) # Sekunden
|
app.config['JOB_CHECK_INTERVAL'] = int(os.environ.get('JOB_CHECK_INTERVAL', '60')) # Sekunden
|
||||||
|
|
||||||
|
# Registriere Frontend V2 Blueprint
|
||||||
|
app.register_blueprint(frontend_v2, url_prefix='/frontend_v2')
|
||||||
|
|
||||||
|
# Übergebe Funktionen an das Frontend v2
|
||||||
|
def setup_frontend_v2():
|
||||||
|
app_functions = {
|
||||||
|
'get_current_user': get_current_user,
|
||||||
|
'get_user_by_id': get_user_by_id,
|
||||||
|
'get_socket_by_id': get_socket_by_id,
|
||||||
|
'get_job_by_id': get_job_by_id,
|
||||||
|
'get_all_sockets': get_all_sockets,
|
||||||
|
'get_all_users': get_all_users,
|
||||||
|
'get_all_jobs': get_all_jobs,
|
||||||
|
'get_jobs_by_user': get_jobs_by_user,
|
||||||
|
'login_required': login_required,
|
||||||
|
'admin_required': admin_required,
|
||||||
|
'delete_session': delete_session,
|
||||||
|
'socket_to_dict': socket_to_dict,
|
||||||
|
'job_to_dict': job_to_dict,
|
||||||
|
'user_to_dict': user_to_dict
|
||||||
|
}
|
||||||
|
set_app_functions(app_functions)
|
||||||
|
|
||||||
|
# Konfiguriere statische Dateien für Frontend v2
|
||||||
|
@app.route('/frontend_v2/static/<path:filename>')
|
||||||
|
def frontend_v2_static(filename):
|
||||||
|
return send_from_directory(os.path.join(app.root_path, 'frontend_v2/static'), filename)
|
||||||
|
|
||||||
# Steckdosen-Konfiguration
|
# Steckdosen-Konfiguration
|
||||||
TAPO_USERNAME = os.environ.get('TAPO_USERNAME')
|
TAPO_USERNAME = os.environ.get('TAPO_USERNAME')
|
||||||
TAPO_PASSWORD = os.environ.get('TAPO_PASSWORD')
|
TAPO_PASSWORD = os.environ.get('TAPO_PASSWORD')
|
||||||
|
|
||||||
# Logging
|
def setup_logging(app):
|
||||||
|
"""
|
||||||
|
Konfiguriert das Logging basierend auf der Umgebung.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: Flask-Anwendung
|
||||||
|
"""
|
||||||
|
if not app.debug and not app.testing:
|
||||||
|
# Production logging
|
||||||
|
if not os.path.exists('logs'):
|
||||||
|
os.mkdir('logs')
|
||||||
|
|
||||||
|
file_handler = RotatingFileHandler('logs/myp.log', maxBytes=10240, backupCount=10)
|
||||||
|
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)
|
||||||
|
|
||||||
|
app.logger.setLevel(logging.INFO)
|
||||||
|
app.logger.info('MYP Backend starting in production mode')
|
||||||
|
else:
|
||||||
|
# Development logging
|
||||||
|
app.logger.setLevel(logging.DEBUG)
|
||||||
|
app.logger.info('MYP Backend starting in development mode')
|
||||||
|
|
||||||
|
# Logging - Legacy (wird durch setup_logging ersetzt)
|
||||||
if not os.path.exists('logs'):
|
if not os.path.exists('logs'):
|
||||||
os.mkdir('logs')
|
os.mkdir('logs')
|
||||||
file_handler = RotatingFileHandler('logs/myp.log', maxBytes=10240, backupCount=10)
|
file_handler = RotatingFileHandler('logs/myp.log', maxBytes=10240, backupCount=10)
|
||||||
@ -1686,9 +1801,33 @@ def stats_page():
|
|||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
return render_template('stats.html', current_user=current_user, active_page='stats')
|
return render_template('stats.html', current_user=current_user, active_page='stats')
|
||||||
|
|
||||||
# Initialisierung und Start des Hintergrund-Threads beim ersten Request
|
# Registrierungsfunktionen für modularen Aufbau
|
||||||
with app.app_context():
|
def register_database_functions(app):
|
||||||
# Diese Funktion wird nach dem App-Start aber vor dem ersten Request ausgeführt
|
"""Registriert Database-Funktionen und Teardown-Handler."""
|
||||||
|
app.teardown_appcontext(close_db)
|
||||||
|
|
||||||
|
def register_auth_functions(app):
|
||||||
|
"""Registriert Authentifizierungsfunktionen."""
|
||||||
|
# Authentifizierungsfunktionen sind bereits global definiert
|
||||||
|
pass
|
||||||
|
|
||||||
|
def register_api_routes(app):
|
||||||
|
"""Registriert alle API-Routen."""
|
||||||
|
# API-Routen sind bereits global definiert
|
||||||
|
pass
|
||||||
|
|
||||||
|
def register_web_routes(app):
|
||||||
|
"""Registriert alle Web-UI-Routen."""
|
||||||
|
# Web-Routen sind bereits global definiert
|
||||||
|
pass
|
||||||
|
|
||||||
|
def register_error_handlers(app):
|
||||||
|
"""Registriert Error-Handler."""
|
||||||
|
# Error-Handler sind bereits global definiert
|
||||||
|
pass
|
||||||
|
|
||||||
|
def register_background_tasks(app):
|
||||||
|
"""Registriert Hintergrund-Tasks."""
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def initialize_background_tasks():
|
def initialize_background_tasks():
|
||||||
"""Startet den Hintergrund-Thread für Job-Überprüfung beim ersten Request."""
|
"""Startet den Hintergrund-Thread für Job-Überprüfung beim ersten Request."""
|
||||||
@ -1711,6 +1850,7 @@ with app.app_context():
|
|||||||
|
|
||||||
# Server starten
|
# Server starten
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
# Legacy-Modus für direkte Ausführung
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
init_db()
|
init_db()
|
||||||
if PRINTERS:
|
if PRINTERS:
|
||||||
@ -1721,5 +1861,21 @@ if __name__ == '__main__':
|
|||||||
job_thread = threading.Thread(target=background_job_checker, daemon=True, name='job_checker_thread')
|
job_thread = threading.Thread(target=background_job_checker, daemon=True, name='job_checker_thread')
|
||||||
job_thread.start()
|
job_thread.start()
|
||||||
app.logger.info("Hintergrund-Thread für Job-Überprüfung gestartet")
|
app.logger.info("Hintergrund-Thread für Job-Überprüfung gestartet")
|
||||||
|
setup_frontend_v2()
|
||||||
|
|
||||||
app.run(debug=True, host='0.0.0.0')
|
# Produktionsmodus aktivieren
|
||||||
|
flask_env = os.environ.get('FLASK_ENV', 'development')
|
||||||
|
debug_mode = flask_env == 'development'
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=debug_mode)
|
||||||
|
else:
|
||||||
|
# Für WSGI-Server wie Gunicorn - verwende Application Factory
|
||||||
|
flask_env = os.environ.get('FLASK_ENV', 'production')
|
||||||
|
app = create_app(flask_env)
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
init_db()
|
||||||
|
printers_config = json.loads(app.config.get('PRINTERS', '{}'))
|
||||||
|
if printers_config:
|
||||||
|
init_printers()
|
||||||
|
setup_frontend_v2()
|
218
backend/cleanup.sh
Normal file
218
backend/cleanup.sh
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
#!/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
|
145
backend/config.py
Normal file
145
backend/config.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
"""
|
||||||
|
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', '{}')
|
||||||
|
|
||||||
|
@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'))
|
||||||
|
|
||||||
|
@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'))
|
||||||
|
|
||||||
|
@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')
|
||||||
|
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
|
'logs/myp.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-Logging
|
||||||
|
error_handler = RotatingFileHandler(
|
||||||
|
'logs/myp-errors.log',
|
||||||
|
maxBytes=Config.LOG_MAX_BYTES,
|
||||||
|
backupCount=Config.LOG_BACKUP_COUNT
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
app.logger.setLevel(logging.INFO)
|
||||||
|
app.logger.info('MYP Backend starting in production mode')
|
||||||
|
|
||||||
|
class TestingConfig(Config):
|
||||||
|
"""Konfiguration für die Testumgebung."""
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
TESTING = True
|
||||||
|
|
||||||
|
# In-Memory-Datenbank für Tests
|
||||||
|
DATABASE = ':memory:'
|
||||||
|
|
||||||
|
# Deaktiviere CSRF für Tests
|
||||||
|
WTF_CSRF_ENABLED = False
|
||||||
|
|
||||||
|
# Kürzere Session-Lebensdauer für Tests
|
||||||
|
PERMANENT_SESSION_LIFETIME = timedelta(minutes=5)
|
||||||
|
|
||||||
|
# Kürzere Job-Check-Intervalle für Tests
|
||||||
|
JOB_CHECK_INTERVAL = 5
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def init_app(app):
|
||||||
|
Config.init_app(app)
|
||||||
|
|
||||||
|
# Konfigurationsmapping
|
||||||
|
config = {
|
||||||
|
'development': DevelopmentConfig,
|
||||||
|
'production': ProductionConfig,
|
||||||
|
'testing': TestingConfig,
|
||||||
|
'default': DevelopmentConfig
|
||||||
|
}
|
617
backend/debug-server/static/css/debug-dashboard.css
Normal file
617
backend/debug-server/static/css/debug-dashboard.css
Normal file
@ -0,0 +1,617 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
470
backend/debug-server/static/js/debug-charts.js
Normal file
470
backend/debug-server/static/js/debug-charts.js
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
/*
|
||||||
|
* Debug-Charts.js
|
||||||
|
* JavaScript-Funktionen für das Rendering von Diagrammen und Visualisierungen
|
||||||
|
* im MYP Debug-Server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Globale Variablen für Charts
|
||||||
|
let cpuUsageChart = null;
|
||||||
|
let memoryUsageChart = null;
|
||||||
|
let networkTrafficChart = null;
|
||||||
|
let diskUsageChart = null;
|
||||||
|
let containerStatusChart = null;
|
||||||
|
|
||||||
|
// Hilfsfunktion zum Formatieren von Bytes
|
||||||
|
function formatBytes(bytes, decimals = 2) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere Systemdiagramme
|
||||||
|
function updateSystemCharts() {
|
||||||
|
fetch('/api/system/metrics')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// CPU-Nutzung aktualisieren
|
||||||
|
if (cpuUsageChart) {
|
||||||
|
cpuUsageChart.data.labels.push(new Date().toLocaleTimeString());
|
||||||
|
cpuUsageChart.data.datasets[0].data.push(data.cpu_percent);
|
||||||
|
|
||||||
|
// Behalte nur die letzten 30 Datenpunkte
|
||||||
|
if (cpuUsageChart.data.labels.length > 30) {
|
||||||
|
cpuUsageChart.data.labels.shift();
|
||||||
|
cpuUsageChart.data.datasets[0].data.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuUsageChart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichernutzung aktualisieren
|
||||||
|
if (memoryUsageChart) {
|
||||||
|
memoryUsageChart.data.datasets[0].data = [
|
||||||
|
data.memory.used,
|
||||||
|
data.memory.available
|
||||||
|
];
|
||||||
|
memoryUsageChart.update();
|
||||||
|
|
||||||
|
// Aktualisiere die Speicherinfo-Texte
|
||||||
|
document.getElementById('memory-used').textContent = formatBytes(data.memory.used);
|
||||||
|
document.getElementById('memory-available').textContent = formatBytes(data.memory.available);
|
||||||
|
document.getElementById('memory-total').textContent = formatBytes(data.memory.total);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Festplattennutzung aktualisieren
|
||||||
|
if (diskUsageChart && data.disk_usage) {
|
||||||
|
const diskLabels = [];
|
||||||
|
const diskUsed = [];
|
||||||
|
const diskFree = [];
|
||||||
|
|
||||||
|
for (const disk of data.disk_usage) {
|
||||||
|
diskLabels.push(disk.mountpoint);
|
||||||
|
diskUsed.push(disk.used);
|
||||||
|
diskFree.push(disk.free);
|
||||||
|
}
|
||||||
|
|
||||||
|
diskUsageChart.data.labels = diskLabels;
|
||||||
|
diskUsageChart.data.datasets[0].data = diskUsed;
|
||||||
|
diskUsageChart.data.datasets[1].data = diskFree;
|
||||||
|
diskUsageChart.update();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Fehler beim Abrufen der Systemmetriken:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisiere CPU-Nutzungsdiagramm
|
||||||
|
function initCpuUsageChart() {
|
||||||
|
const ctx = document.getElementById('cpu-usage-chart').getContext('2d');
|
||||||
|
|
||||||
|
cpuUsageChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: 'CPU-Auslastung (%)',
|
||||||
|
data: [],
|
||||||
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.2)'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Auslastung (%)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Zeit'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'CPU-Auslastung',
|
||||||
|
font: {
|
||||||
|
size: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisiere Speichernutzungsdiagramm
|
||||||
|
function initMemoryUsageChart() {
|
||||||
|
const ctx = document.getElementById('memory-usage-chart').getContext('2d');
|
||||||
|
|
||||||
|
memoryUsageChart = new Chart(ctx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['Verwendet', 'Verfügbar'],
|
||||||
|
datasets: [{
|
||||||
|
data: [0, 0],
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(255, 99, 132, 0.7)',
|
||||||
|
'rgba(75, 192, 192, 0.7)'
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Speichernutzung',
|
||||||
|
font: {
|
||||||
|
size: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisiere Festplattennutzungsdiagramm
|
||||||
|
function initDiskUsageChart() {
|
||||||
|
const ctx = document.getElementById('disk-usage-chart').getContext('2d');
|
||||||
|
|
||||||
|
diskUsageChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Belegt',
|
||||||
|
data: [],
|
||||||
|
backgroundColor: 'rgba(255, 99, 132, 0.7)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Frei',
|
||||||
|
data: [],
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.7)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Laufwerk'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Speicherplatz (Bytes)'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return formatBytes(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Festplattennutzung',
|
||||||
|
font: {
|
||||||
|
size: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere Docker-Container-Status
|
||||||
|
function updateContainerStatus() {
|
||||||
|
fetch('/api/docker/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const containerTable = document.getElementById('container-table');
|
||||||
|
if (!containerTable) return;
|
||||||
|
|
||||||
|
// Tabelle leeren
|
||||||
|
containerTable.innerHTML = '';
|
||||||
|
|
||||||
|
// Überschriftenzeile
|
||||||
|
const headerRow = document.createElement('tr');
|
||||||
|
['Name', 'Status', 'CPU', 'Speicher', 'Netzwerk', 'Aktionen'].forEach(header => {
|
||||||
|
const th = document.createElement('th');
|
||||||
|
th.textContent = header;
|
||||||
|
headerRow.appendChild(th);
|
||||||
|
});
|
||||||
|
containerTable.appendChild(headerRow);
|
||||||
|
|
||||||
|
// Containerdaten
|
||||||
|
data.containers.forEach(container => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
// Name
|
||||||
|
const nameCell = document.createElement('td');
|
||||||
|
nameCell.textContent = container.name;
|
||||||
|
row.appendChild(nameCell);
|
||||||
|
|
||||||
|
// Status
|
||||||
|
const statusCell = document.createElement('td');
|
||||||
|
const statusBadge = document.createElement('span');
|
||||||
|
statusBadge.textContent = container.status;
|
||||||
|
statusBadge.className = container.running ? 'status-badge running' : 'status-badge stopped';
|
||||||
|
statusCell.appendChild(statusBadge);
|
||||||
|
row.appendChild(statusCell);
|
||||||
|
|
||||||
|
// CPU
|
||||||
|
const cpuCell = document.createElement('td');
|
||||||
|
cpuCell.textContent = container.cpu_percent ? `${container.cpu_percent.toFixed(2)}%` : 'N/A';
|
||||||
|
row.appendChild(cpuCell);
|
||||||
|
|
||||||
|
// Speicher
|
||||||
|
const memoryCell = document.createElement('td');
|
||||||
|
memoryCell.textContent = container.memory_usage ? formatBytes(container.memory_usage) : 'N/A';
|
||||||
|
row.appendChild(memoryCell);
|
||||||
|
|
||||||
|
// Netzwerk
|
||||||
|
const networkCell = document.createElement('td');
|
||||||
|
if (container.network_io) {
|
||||||
|
networkCell.innerHTML = `↓ ${formatBytes(container.network_io.rx_bytes)}<br>↑ ${formatBytes(container.network_io.tx_bytes)}`;
|
||||||
|
} else {
|
||||||
|
networkCell.textContent = 'N/A';
|
||||||
|
}
|
||||||
|
row.appendChild(networkCell);
|
||||||
|
|
||||||
|
// Aktionen
|
||||||
|
const actionsCell = document.createElement('td');
|
||||||
|
const actionsDiv = document.createElement('div');
|
||||||
|
actionsDiv.className = 'container-actions';
|
||||||
|
|
||||||
|
// Restart-Button
|
||||||
|
const restartBtn = document.createElement('button');
|
||||||
|
restartBtn.className = 'btn btn-warning btn-sm';
|
||||||
|
restartBtn.innerHTML = '<i class="fas fa-sync"></i>';
|
||||||
|
restartBtn.title = 'Container neustarten';
|
||||||
|
restartBtn.onclick = () => restartContainer(container.id);
|
||||||
|
actionsDiv.appendChild(restartBtn);
|
||||||
|
|
||||||
|
// Logs-Button
|
||||||
|
const logsBtn = document.createElement('button');
|
||||||
|
logsBtn.className = 'btn btn-info btn-sm';
|
||||||
|
logsBtn.innerHTML = '<i class="fas fa-list-alt"></i>';
|
||||||
|
logsBtn.title = 'Container-Logs anzeigen';
|
||||||
|
logsBtn.onclick = () => showContainerLogs(container.id);
|
||||||
|
actionsDiv.appendChild(logsBtn);
|
||||||
|
|
||||||
|
actionsCell.appendChild(actionsDiv);
|
||||||
|
row.appendChild(actionsCell);
|
||||||
|
|
||||||
|
containerTable.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Container-Status-Diagramm aktualisieren
|
||||||
|
updateContainerStatusChart(data.containers);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Fehler beim Abrufen der Docker-Informationen:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisiere Container-Status-Diagramm
|
||||||
|
function initContainerStatusChart() {
|
||||||
|
const ctx = document.getElementById('container-status-chart').getContext('2d');
|
||||||
|
|
||||||
|
containerStatusChart = new Chart(ctx, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: ['Aktiv', 'Inaktiv'],
|
||||||
|
datasets: [{
|
||||||
|
data: [0, 0],
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(75, 192, 192, 0.7)',
|
||||||
|
'rgba(255, 99, 132, 0.7)'
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Container-Status',
|
||||||
|
font: {
|
||||||
|
size: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere Container-Status-Diagramm
|
||||||
|
function updateContainerStatusChart(containers) {
|
||||||
|
if (!containerStatusChart) return;
|
||||||
|
|
||||||
|
const running = containers.filter(c => c.running).length;
|
||||||
|
const stopped = containers.filter(c => !c.running).length;
|
||||||
|
|
||||||
|
containerStatusChart.data.datasets[0].data = [running, stopped];
|
||||||
|
containerStatusChart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container neustarten
|
||||||
|
function restartContainer(containerId) {
|
||||||
|
fetch(`/api/docker/restart/${containerId}`, {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showMessage('Container wird neugestartet...', false);
|
||||||
|
// Nach kurzer Verzögerung aktualisieren
|
||||||
|
setTimeout(updateContainerStatus, 2000);
|
||||||
|
} else {
|
||||||
|
showMessage('Fehler beim Neustarten des Containers: ' + data.message, true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showMessage('Fehler beim Neustarten des Containers', true);
|
||||||
|
console.error('Fehler beim Neustarten des Containers:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container-Logs anzeigen
|
||||||
|
function showContainerLogs(containerId) {
|
||||||
|
fetch(`/api/docker/logs/${containerId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.logs) {
|
||||||
|
// Modal erstellen und anzeigen
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'modal';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Container-Logs: ${data.container_name}</h3>
|
||||||
|
<span class="close">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<pre>${data.logs}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// Modal schließen, wenn auf X geklickt wird
|
||||||
|
modal.querySelector('.close').onclick = function() {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modal schließen, wenn außerhalb geklickt wird
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (event.target === modal) {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modal anzeigen
|
||||||
|
modal.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
showMessage('Keine Logs verfügbar', true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showMessage('Fehler beim Abrufen der Container-Logs', true);
|
||||||
|
console.error('Fehler beim Abrufen der Container-Logs:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeige Fehlermeldung oder Erfolgsmeldung
|
||||||
|
function showMessage(message, isError = false) {
|
||||||
|
const messageEl = document.getElementById('message');
|
||||||
|
if (messageEl) {
|
||||||
|
messageEl.textContent = message;
|
||||||
|
messageEl.className = isError ? 'message message-error' : 'message message-success';
|
||||||
|
messageEl.style.display = 'block';
|
||||||
|
|
||||||
|
// Verstecke Nachricht nach 5 Sekunden
|
||||||
|
setTimeout(() => {
|
||||||
|
messageEl.style.display = 'none';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisiere alle Diagramme
|
||||||
|
function initAllCharts() {
|
||||||
|
// Überprüfe, ob Chart.js geladen ist
|
||||||
|
if (typeof Chart !== 'undefined') {
|
||||||
|
if (document.getElementById('cpu-usage-chart')) {
|
||||||
|
initCpuUsageChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementById('memory-usage-chart')) {
|
||||||
|
initMemoryUsageChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementById('disk-usage-chart')) {
|
||||||
|
initDiskUsageChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementById('container-status-chart')) {
|
||||||
|
initContainerStatusChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialen Datenabruf starten
|
||||||
|
updateSystemCharts();
|
||||||
|
updateContainerStatus();
|
||||||
|
|
||||||
|
// Regelmäßige Aktualisierung der Diagramme
|
||||||
|
setInterval(updateSystemCharts, 5000); // Alle 5 Sekunden aktualisieren
|
||||||
|
setInterval(updateContainerStatus, 10000); // Alle 10 Sekunden aktualisieren
|
||||||
|
} else {
|
||||||
|
console.error('Chart.js konnte nicht geladen werden.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatischer Start beim Laden der Seite
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initAllCharts();
|
||||||
|
});
|
1234
backend/debug-server/static/js/debug-dashboard.js
Normal file
1234
backend/debug-server/static/js/debug-dashboard.js
Normal file
File diff suppressed because it is too large
Load Diff
444
backend/debug-server/templates/dashboard.html
Normal file
444
backend/debug-server/templates/dashboard.html
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
<!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>
|
261
backend/debug-server/templates/debug.html
Normal file
261
backend/debug-server/templates/debug.html
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
<!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>
|
346
backend/frontend_v2_routes.py
Normal file
346
backend/frontend_v2_routes.py
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
"""
|
||||||
|
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
|
510
backend/install.sh
Normal file
510
backend/install.sh
Normal file
@ -0,0 +1,510 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# MYP Backend Installations-Skript
|
||||||
|
# Dieses Skript installiert das Backend mit Docker und Host-Netzwerkanbindung
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Bereinigen vorhandener Installationen
|
||||||
|
cleanup_existing_installation() {
|
||||||
|
log "${YELLOW}Bereinige vorhandene Installation...${NC}"
|
||||||
|
|
||||||
|
# Stoppe und entferne existierende Container
|
||||||
|
if docker ps -a | grep -q "myp-backend"; then
|
||||||
|
log "Stoppe und entferne existierenden Backend-Container..."
|
||||||
|
docker stop myp-backend &>/dev/null || true
|
||||||
|
docker rm myp-backend &>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Entferne Docker Images
|
||||||
|
if docker images | grep -q "myp-backend"; then
|
||||||
|
log "Entferne existierendes Backend-Image..."
|
||||||
|
docker rmi myp-backend &>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "${GREEN}Bereinigung abgeschlossen.${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pfade definieren
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
BACKEND_DIR="$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Bereinige existierende Installation
|
||||||
|
cleanup_existing_installation
|
||||||
|
|
||||||
|
# Funktion zur Installation von Docker und Docker Compose für Raspberry Pi
|
||||||
|
install_docker() {
|
||||||
|
log "${YELLOW}Docker ist nicht installiert. Installation wird gestartet...${NC}"
|
||||||
|
|
||||||
|
# Erkenne Raspberry Pi
|
||||||
|
if [ -f /proc/device-tree/model ] && grep -q "Raspberry Pi" /proc/device-tree/model; then
|
||||||
|
log "${GREEN}Raspberry Pi erkannt. Installiere Docker für ARM-Architektur...${NC}"
|
||||||
|
IS_RASPBERRY_PI=true
|
||||||
|
else
|
||||||
|
IS_RASPBERRY_PI=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aktualisiere Paketindex
|
||||||
|
if ! sudo apt-get update; then
|
||||||
|
error_log "Konnte Paketindex nicht aktualisieren. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Installiere erforderliche Pakete
|
||||||
|
if ! sudo apt-get install -y apt-transport-https ca-certificates curl gnupg software-properties-common; then
|
||||||
|
error_log "Konnte erforderliche Pakete nicht installieren. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Raspberry Pi-spezifische Installation
|
||||||
|
if [ "$IS_RASPBERRY_PI" = true ]; then
|
||||||
|
# Setze Systemarchitektur für Raspberry Pi (armhf oder arm64)
|
||||||
|
ARCH=$(dpkg --print-architecture)
|
||||||
|
log "Erkannte Systemarchitektur: ${ARCH}"
|
||||||
|
|
||||||
|
# Installiere Docker mit convenience script (für Raspberry Pi empfohlen)
|
||||||
|
log "${YELLOW}Installiere Docker mit dem convenience script...${NC}"
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sudo sh get-docker.sh
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error_log "Docker-Installation fehlgeschlagen. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Standard-Installation für andere Systeme
|
||||||
|
# Füge Docker's offiziellen GPG-Schlüssel hinzu
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||||
|
|
||||||
|
# Füge Docker-Repository hinzu
|
||||||
|
if ! sudo add-apt-repository "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"; then
|
||||||
|
error_log "Konnte Docker-Repository nicht hinzufügen. Prüfen Sie, ob Ihr System unterstützt wird."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aktualisiere Paketindex erneut
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
# Installiere Docker
|
||||||
|
if ! sudo apt-get install -y docker-ce docker-ce-cli containerd.io; then
|
||||||
|
error_log "Konnte Docker nicht installieren. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Füge aktuellen Benutzer zur Docker-Gruppe hinzu
|
||||||
|
sudo usermod -aG docker "$USER"
|
||||||
|
|
||||||
|
log "${GREEN}Docker wurde installiert.${NC}"
|
||||||
|
log "${YELLOW}WICHTIG: Möglicherweise müssen Sie sich neu anmelden, damit die Gruppenänderung wirksam wird.${NC}"
|
||||||
|
|
||||||
|
# Prüfen, ob Docker Compose v2 Plugin verfügbar ist (bevorzugt, da moderner)
|
||||||
|
log "${YELLOW}Prüfe Docker Compose Version...${NC}"
|
||||||
|
|
||||||
|
if docker compose version &> /dev/null; then
|
||||||
|
log "${GREEN}Docker Compose v2 Plugin ist bereits installiert.${NC}"
|
||||||
|
DOCKER_COMPOSE_V2=true
|
||||||
|
else
|
||||||
|
log "${YELLOW}Docker Compose v2 Plugin nicht gefunden. Versuche Docker Compose v1 zu installieren...${NC}"
|
||||||
|
DOCKER_COMPOSE_V2=false
|
||||||
|
|
||||||
|
if [ "$IS_RASPBERRY_PI" = true ]; then
|
||||||
|
# Für Raspberry Pi ist es besser, die richtige Architektur zu verwenden
|
||||||
|
if [ "$ARCH" = "armhf" ]; then
|
||||||
|
log "Installiere Docker Compose für armhf (32-bit)..."
|
||||||
|
sudo 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)..."
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose
|
||||||
|
else
|
||||||
|
# Fallback auf v1.29.2 für unbekannte ARM-Architekturen
|
||||||
|
log "Verwende automatische Architekturerkennung für Docker Compose v1.29.2..."
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Für andere Systeme versuche zuerst v2, dann v1.29.2 als Fallback
|
||||||
|
log "Installiere Docker Compose v2 für $(uname -s)/$(uname -m)..."
|
||||||
|
if ! sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose; then
|
||||||
|
log "${YELLOW}Konnte Docker Compose v2 nicht herunterladen. Versuche v1.29.2...${NC}"
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error_log "Konnte Docker Compose nicht herunterladen. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
log "${GREEN}Docker Compose wurde installiert.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Starte Docker-Dienst
|
||||||
|
if command -v systemctl &> /dev/null; then
|
||||||
|
sudo systemctl enable docker
|
||||||
|
sudo systemctl start docker
|
||||||
|
elif command -v service &> /dev/null; then
|
||||||
|
sudo service docker enable
|
||||||
|
sudo service docker start
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prüfen ob Docker installiert ist
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
log "${YELLOW}Docker ist nicht installiert.${NC}"
|
||||||
|
read -p "Möchten Sie Docker installieren? (j/n): " install_docker_choice
|
||||||
|
if [[ "$install_docker_choice" == "j" ]]; then
|
||||||
|
install_docker
|
||||||
|
else
|
||||||
|
error_log "Docker wird für die Installation benötigt. Bitte installieren Sie Docker manuell."
|
||||||
|
log "Siehe: https://docs.docker.com/get-docker/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen ob Docker Daemon läuft
|
||||||
|
if ! docker info &> /dev/null; then
|
||||||
|
log "${YELLOW}Docker-Daemon läuft nicht. Versuche, den Dienst zu starten...${NC}"
|
||||||
|
|
||||||
|
# Versuche, Docker zu starten
|
||||||
|
if command -v systemctl &> /dev/null; then
|
||||||
|
sudo systemctl start docker
|
||||||
|
elif command -v service &> /dev/null; then
|
||||||
|
sudo service docker start
|
||||||
|
else
|
||||||
|
error_log "Konnte Docker-Daemon nicht starten. Bitte starten Sie den Docker-Dienst manuell."
|
||||||
|
log "Starten mit: sudo systemctl start docker oder sudo service docker start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe erneut, ob Docker läuft
|
||||||
|
if ! docker info &> /dev/null; then
|
||||||
|
error_log "Docker-Daemon konnte nicht gestartet werden. Bitte starten Sie den Docker-Dienst manuell."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "${GREEN}Docker-Daemon wurde erfolgreich gestartet.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen ob Docker Compose installiert ist
|
||||||
|
if docker compose version &> /dev/null; then
|
||||||
|
log "${GREEN}Docker Compose v2 Plugin ist bereits installiert.${NC}"
|
||||||
|
DOCKER_COMPOSE_V2=true
|
||||||
|
elif command -v docker-compose &> /dev/null; then
|
||||||
|
log "${GREEN}Docker Compose v1 ist bereits installiert.${NC}"
|
||||||
|
DOCKER_COMPOSE_V2=false
|
||||||
|
else
|
||||||
|
log "${YELLOW}Docker Compose ist nicht installiert.${NC}"
|
||||||
|
DOCKER_COMPOSE_V2=false
|
||||||
|
read -p "Möchten Sie Docker Compose installieren? (j/n): " install_compose_choice
|
||||||
|
if [[ "$install_compose_choice" == "j" ]]; then
|
||||||
|
log "${YELLOW}Installiere Docker Compose...${NC}"
|
||||||
|
|
||||||
|
# Prüfe ob das Betriebssystem ARM-basiert ist (z.B. Raspberry Pi)
|
||||||
|
if grep -q "arm" /proc/cpuinfo 2> /dev/null; then
|
||||||
|
ARCH=$(dpkg --print-architecture 2> /dev/null || echo "unknown")
|
||||||
|
IS_RASPBERRY_PI=true
|
||||||
|
else
|
||||||
|
IS_RASPBERRY_PI=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Versuche zuerst Docker Compose v2 zu installieren
|
||||||
|
if [ "$IS_RASPBERRY_PI" = true ]; then
|
||||||
|
if [ "$ARCH" = "armhf" ]; then
|
||||||
|
log "Installiere Docker Compose für armhf (32-bit)..."
|
||||||
|
sudo 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)..."
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose
|
||||||
|
else
|
||||||
|
log "Verwende automatische Architekturerkennung für Docker Compose v1.29.2..."
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Installiere Docker Compose v2 für $(uname -s)/$(uname -m)..."
|
||||||
|
if ! sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose; then
|
||||||
|
log "${YELLOW}Konnte Docker Compose v2 nicht herunterladen. Versuche v1.29.2...${NC}"
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error_log "Konnte Docker Compose nicht herunterladen. Bitte manuell installieren."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
log "${GREEN}Docker Compose wurde installiert.${NC}"
|
||||||
|
else
|
||||||
|
error_log "Docker Compose wird für die Installation benötigt. Bitte installieren Sie es manuell."
|
||||||
|
log "Siehe: https://docs.docker.com/compose/install/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen ob wget installiert ist (wird für healthcheck verwendet)
|
||||||
|
if ! command -v wget &> /dev/null; then
|
||||||
|
error_log "wget ist nicht installiert, wird aber für den Container-Healthcheck benötigt."
|
||||||
|
log "Installation mit: sudo apt-get install wget"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wechsle ins Backend-Verzeichnis
|
||||||
|
log "Wechsle ins Verzeichnis: $BACKEND_DIR"
|
||||||
|
cd "$BACKEND_DIR" || {
|
||||||
|
error_log "Konnte nicht ins Verzeichnis $BACKEND_DIR wechseln."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prüfe ob Dockerfile existiert
|
||||||
|
if [ ! -f "Dockerfile" ]; then
|
||||||
|
error_log "Dockerfile nicht gefunden in $BACKEND_DIR."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe ob docker-compose.yml existiert
|
||||||
|
if [ ! -f "docker-compose.yml" ]; then
|
||||||
|
error_log "docker-compose.yml nicht gefunden in $BACKEND_DIR."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle .env-Datei
|
||||||
|
log "${YELLOW}Erstelle .env Datei...${NC}"
|
||||||
|
cat > .env << EOL
|
||||||
|
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"}}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
error_log "Konnte .env-Datei nicht erstellen. Prüfen Sie die Berechtigungen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "${GREEN}.env Datei erfolgreich erstellt${NC}"
|
||||||
|
|
||||||
|
# Verzeichnisse erstellen
|
||||||
|
log "Erstelle benötigte Verzeichnisse"
|
||||||
|
if ! mkdir -p logs; then
|
||||||
|
error_log "Konnte Verzeichnis 'logs' nicht erstellen. Prüfen Sie die Berechtigungen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! mkdir -p instance; then
|
||||||
|
error_log "Konnte Verzeichnis 'instance' nicht erstellen. Prüfen Sie die Berechtigungen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docker-Image bauen und starten
|
||||||
|
log "${YELLOW}Baue und starte Backend-Container...${NC}"
|
||||||
|
log "${YELLOW}Dies kann auf einem Raspberry Pi einige Minuten dauern - bitte geduldig sein${NC}"
|
||||||
|
|
||||||
|
# Prüfe, ob Docker-Daemon läuft
|
||||||
|
if ! docker info &>/dev/null; then
|
||||||
|
log "${YELLOW}Docker-Daemon scheint nicht zu laufen. Versuche zu starten...${NC}"
|
||||||
|
|
||||||
|
# Versuche Docker zu starten
|
||||||
|
if command -v systemctl &>/dev/null; then
|
||||||
|
sudo systemctl start docker || true
|
||||||
|
sleep 5
|
||||||
|
elif command -v service &>/dev/null; then
|
||||||
|
sudo service docker start || true
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe erneut, ob Docker jetzt läuft
|
||||||
|
if ! docker info &>/dev/null; then
|
||||||
|
error_log "Docker-Daemon konnte nicht gestartet werden."
|
||||||
|
log "Führen Sie vor der Installation bitte folgende Befehle aus:"
|
||||||
|
log " sudo systemctl start docker"
|
||||||
|
log " sudo systemctl enable docker"
|
||||||
|
log "Starten Sie dann das Installationsskript erneut."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docker-Rechte prüfen
|
||||||
|
if ! docker ps &>/dev/null; then
|
||||||
|
error_log "Sie haben keine Berechtigung, Docker ohne sudo zu verwenden."
|
||||||
|
log "Bitte führen Sie folgenden Befehl aus und melden Sie sich danach neu an:"
|
||||||
|
log " sudo usermod -aG docker $USER"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob erforderliche Basis-Images lokal verfügbar sind
|
||||||
|
if ! docker image inspect python:3-slim &>/dev/null; then
|
||||||
|
log "${YELLOW}Prüfe und setze DNS-Server für Docker...${NC}"
|
||||||
|
|
||||||
|
# DNS-Einstellungen prüfen und anpassen
|
||||||
|
if [ -f /etc/docker/daemon.json ]; then
|
||||||
|
log "Bestehende Docker-Konfiguration gefunden."
|
||||||
|
else
|
||||||
|
log "Erstelle Docker-Konfiguration mit Google DNS..."
|
||||||
|
sudo mkdir -p /etc/docker
|
||||||
|
echo '{
|
||||||
|
"dns": ["8.8.8.8", "8.8.4.4"]
|
||||||
|
}' | sudo tee /etc/docker/daemon.json > /dev/null
|
||||||
|
|
||||||
|
# Docker neu starten, damit die Änderungen wirksam werden
|
||||||
|
if command -v systemctl &>/dev/null; then
|
||||||
|
sudo systemctl restart docker
|
||||||
|
sleep 5
|
||||||
|
elif command -v service &>/dev/null; then
|
||||||
|
sudo service docker restart
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Versuche Image explizit mit anderen Tags herunterzuladen
|
||||||
|
log "${YELLOW}Versuche lokal vorhandene Python-Version zu finden...${NC}"
|
||||||
|
|
||||||
|
# Suche nach allen verfügbaren Python-Images
|
||||||
|
PYTHON_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep "python:")
|
||||||
|
|
||||||
|
if [ -n "$PYTHON_IMAGES" ]; then
|
||||||
|
log "Gefundene Python-Images: $PYTHON_IMAGES"
|
||||||
|
# Verwende das erste gefundene Python-Image
|
||||||
|
FIRST_PYTHON=$(echo "$PYTHON_IMAGES" | head -n 1)
|
||||||
|
log "${GREEN}Verwende vorhandenes Python-Image: $FIRST_PYTHON${NC}"
|
||||||
|
|
||||||
|
# Aktualisiere den Dockerfile
|
||||||
|
sed -i "s|FROM python:3-slim|FROM $FIRST_PYTHON|g" Dockerfile
|
||||||
|
log "Dockerfile aktualisiert, um lokales Image zu verwenden."
|
||||||
|
else
|
||||||
|
# Versuche unterschiedliche Python-Versionen
|
||||||
|
for PYTHON_VERSION in "python:3.11-slim" "python:3.10-slim" "python:3.9-slim" "python:slim" "python:alpine"; do
|
||||||
|
log "Versuche $PYTHON_VERSION zu laden..."
|
||||||
|
if docker pull $PYTHON_VERSION; then
|
||||||
|
log "${GREEN}Erfolgreich $PYTHON_VERSION heruntergeladen${NC}"
|
||||||
|
# Aktualisiere den Dockerfile
|
||||||
|
sed -i "s|FROM python:3-slim|FROM $PYTHON_VERSION|g" Dockerfile
|
||||||
|
log "Dockerfile aktualisiert, um $PYTHON_VERSION zu verwenden."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erhöhe Docker-Timeout für langsame Verbindungen und Raspberry Pi
|
||||||
|
export DOCKER_CLIENT_TIMEOUT=300
|
||||||
|
export COMPOSE_HTTP_TIMEOUT=300
|
||||||
|
|
||||||
|
# Verwende die richtige Docker Compose Version
|
||||||
|
if [ "${DOCKER_COMPOSE_V2:-false}" = true ]; then
|
||||||
|
# Docker Compose V2 Plugin (docker compose)
|
||||||
|
log "Baue lokales Image..."
|
||||||
|
if ! docker compose build --no-cache; then
|
||||||
|
error_log "Docker Compose Build (v2) fehlgeschlagen. Versuche mit v1 Format..."
|
||||||
|
if ! docker-compose build --no-cache; then
|
||||||
|
error_log "Docker Compose Build fehlgeschlagen. Siehe Fehlermeldung oben."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starte Container aus lokalem Image..."
|
||||||
|
if ! docker compose up -d; then
|
||||||
|
error_log "Docker Compose Up (v2) fehlgeschlagen. Versuche mit v1 Format..."
|
||||||
|
if ! docker-compose up -d; then
|
||||||
|
error_log "Docker Compose Up fehlgeschlagen. Siehe Fehlermeldung oben."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Docker Compose V1 (docker-compose)
|
||||||
|
log "Baue lokales Image..."
|
||||||
|
if ! docker-compose build --no-cache; then
|
||||||
|
error_log "Docker Compose Build fehlgeschlagen. Siehe Fehlermeldung oben."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starte Container aus lokalem Image..."
|
||||||
|
if ! docker-compose up -d; then
|
||||||
|
error_log "Docker Compose Up fehlgeschlagen. Siehe Fehlermeldung oben."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe, ob der Container läuft
|
||||||
|
log "Warte 10 Sekunden, bis der Container gestartet ist..."
|
||||||
|
sleep 10
|
||||||
|
if docker ps | grep -q "myp-backend"; then
|
||||||
|
log "${GREEN}Backend-Container läuft${NC}"
|
||||||
|
else
|
||||||
|
error_log "Backend-Container läuft nicht. Container-Status:"
|
||||||
|
docker ps -a | grep myp-backend
|
||||||
|
log "Container-Logs:"
|
||||||
|
docker logs myp-backend
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test API-Endpunkt
|
||||||
|
log "${YELLOW}Teste Backend-API...${NC}"
|
||||||
|
log "${YELLOW}HINWEIS: Der API-Server ist bei der ersten Installation oft noch nicht erreichbar${NC}"
|
||||||
|
log "${YELLOW}Dies ist ein bekanntes Verhalten wegen der Netzwerkkonfiguration${NC}"
|
||||||
|
log "${YELLOW}Bitte nach der Installation das System neu starten, danach sollte der API-Server erreichbar sein${NC}"
|
||||||
|
|
||||||
|
# Wir versuchen es trotzdem einmal, um zu sehen, ob er vielleicht doch läuft
|
||||||
|
if curl -s http://localhost:5000/health 2>/dev/null | grep -q "healthy"; then
|
||||||
|
log "${GREEN}Backend-API ist erreichbar und funktioniert${NC}"
|
||||||
|
else
|
||||||
|
log "${YELLOW}Backend-API ist wie erwartet noch nicht erreichbar${NC}"
|
||||||
|
log "${GREEN}Das ist völlig normal bei der Erstinstallation${NC}"
|
||||||
|
log "${GREEN}Nach einem Neustart des Systems sollte der API-Server korrekt erreichbar sein${NC}"
|
||||||
|
log "Container-Status prüfen mit: docker logs myp-backend"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialisierung der Datenbank prüfen
|
||||||
|
log "${YELLOW}Prüfe Datenbank-Initialisierung...${NC}"
|
||||||
|
if [ ! -s "instance/myp.db" ]; then
|
||||||
|
log "${YELLOW}Datenbank scheint leer zu sein. Führe Initialisierungsskript aus...${NC}"
|
||||||
|
DB_INIT_OUTPUT=$(docker exec myp-backend python -c "from app import init_db; init_db()" 2>&1)
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "${GREEN}Datenbank erfolgreich initialisiert${NC}"
|
||||||
|
else
|
||||||
|
error_log "Fehler bei der Datenbank-Initialisierung:"
|
||||||
|
echo "$DB_INIT_OUTPUT"
|
||||||
|
log "Container-Logs:"
|
||||||
|
docker logs myp-backend
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "${GREEN}Datenbank existiert bereits${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Teste, ob ein API-Endpunkt Daten zurückgibt
|
||||||
|
log "${YELLOW}Teste Datenbank-Verbindung über API...${NC}"
|
||||||
|
if curl -s http://localhost:5000/api/printers | grep -q "\[\]"; then
|
||||||
|
log "${GREEN}Datenbank-Verbindung funktioniert${NC}"
|
||||||
|
else
|
||||||
|
log "${YELLOW}API gibt keine leere Drucker-Liste zurück. Möglicherweise ist die DB nicht korrekt initialisiert.${NC}"
|
||||||
|
log "API-Antwort:"
|
||||||
|
curl -s http://localhost:5000/api/printers
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "${GREEN}=== Installation abgeschlossen ===${NC}"
|
||||||
|
log "${YELLOW}WICHTIG: Nach der Erstinstallation ist ein Systemneustart erforderlich${NC}"
|
||||||
|
log "${YELLOW}Danach ist das Backend unter http://localhost:5000 erreichbar${NC}"
|
||||||
|
log "Anzeigen der Logs: docker logs -f myp-backend"
|
||||||
|
|
||||||
|
# Verwende die richtige Docker Compose Version für Hinweis
|
||||||
|
if [ "${DOCKER_COMPOSE_V2:-false}" = true ]; then
|
||||||
|
log "Backend stoppen: docker compose -f $BACKEND_DIR/docker-compose.yml down"
|
||||||
|
else
|
||||||
|
log "Backend stoppen: docker-compose -f $BACKEND_DIR/docker-compose.yml down"
|
||||||
|
fi
|
185
backend/network_config.py
Normal file
185
backend/network_config.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
import netifaces
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class NetworkConfig:
|
||||||
|
"""Verwaltet die Netzwerkkonfiguration für das MYP-System."""
|
||||||
|
|
||||||
|
CONFIG_FILE = 'instance/network_config.json'
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
'backend_hostname': '192.168.0.5',
|
||||||
|
'backend_port': 5000,
|
||||||
|
'frontend_hostname': '192.168.0.106',
|
||||||
|
'frontend_port': 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, app=None):
|
||||||
|
"""Initialisierung der Netzwerkkonfiguration."""
|
||||||
|
self.logger = logging.getLogger('myp.network')
|
||||||
|
self.config = self.DEFAULT_CONFIG.copy()
|
||||||
|
self.last_check = None
|
||||||
|
self.backend_status = "Nicht überprüft"
|
||||||
|
self.frontend_status = "Nicht überprüft"
|
||||||
|
|
||||||
|
# Stelle sicher, dass das Verzeichnis existiert
|
||||||
|
os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
# Lade gespeicherte Konfiguration, falls vorhanden
|
||||||
|
self.load_config()
|
||||||
|
|
||||||
|
if app:
|
||||||
|
self.init_app(app)
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
"""Initialisiert die Anwendung mit dieser Konfiguration."""
|
||||||
|
app.network_config = self
|
||||||
|
|
||||||
|
# Registriere Route für Netzwerkkonfiguration
|
||||||
|
@app.route('/admin/network-config', methods=['GET', 'POST'])
|
||||||
|
def network_config():
|
||||||
|
from flask import request, render_template, flash, redirect, url_for
|
||||||
|
|
||||||
|
# Prüfe aktuelle Status
|
||||||
|
self.check_connection_statuses()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Aktualisiere Konfiguration
|
||||||
|
self.config['backend_hostname'] = request.form.get('backend_hostname', self.DEFAULT_CONFIG['backend_hostname'])
|
||||||
|
self.config['backend_port'] = int(request.form.get('backend_port', self.DEFAULT_CONFIG['backend_port']))
|
||||||
|
self.config['frontend_hostname'] = request.form.get('frontend_hostname', self.DEFAULT_CONFIG['frontend_hostname'])
|
||||||
|
self.config['frontend_port'] = int(request.form.get('frontend_port', self.DEFAULT_CONFIG['frontend_port']))
|
||||||
|
|
||||||
|
# Speichere Konfiguration
|
||||||
|
self.save_config()
|
||||||
|
|
||||||
|
# Teste die neue Konfiguration
|
||||||
|
self.check_connection_statuses()
|
||||||
|
|
||||||
|
flash('Netzwerkkonfiguration erfolgreich gespeichert!', 'success')
|
||||||
|
return redirect(url_for('network_config'))
|
||||||
|
|
||||||
|
# Ermittle Netzwerkschnittstellen
|
||||||
|
network_interfaces = self.get_network_interfaces()
|
||||||
|
|
||||||
|
return render_template('network_config.html',
|
||||||
|
config=self.config,
|
||||||
|
backend_status=self.backend_status,
|
||||||
|
frontend_status=self.frontend_status,
|
||||||
|
last_check=self.last_check,
|
||||||
|
network_interfaces=network_interfaces,
|
||||||
|
message=request.args.get('message'),
|
||||||
|
message_type=request.args.get('message_type', 'info'))
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
"""Lädt die gespeicherte Konfiguration."""
|
||||||
|
try:
|
||||||
|
if os.path.exists(self.CONFIG_FILE):
|
||||||
|
with open(self.CONFIG_FILE, 'r') as f:
|
||||||
|
saved_config = json.load(f)
|
||||||
|
self.config.update(saved_config)
|
||||||
|
self.logger.info(f"Netzwerkkonfiguration geladen: {self.config}")
|
||||||
|
else:
|
||||||
|
self.logger.info(f"Keine gespeicherte Konfiguration gefunden, verwende Standardwerte: {self.config}")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Fehler beim Laden der Netzwerkkonfiguration: {e}")
|
||||||
|
|
||||||
|
def save_config(self):
|
||||||
|
"""Speichert die aktuelle Konfiguration."""
|
||||||
|
try:
|
||||||
|
with open(self.CONFIG_FILE, 'w') as f:
|
||||||
|
json.dump(self.config, f, indent=4)
|
||||||
|
self.logger.info(f"Netzwerkkonfiguration gespeichert: {self.config}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Fehler beim Speichern der Netzwerkkonfiguration: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_backend_url(self):
|
||||||
|
"""Gibt die Backend-URL zurück."""
|
||||||
|
return f"http://{self.config['backend_hostname']}:{self.config['backend_port']}"
|
||||||
|
|
||||||
|
def get_frontend_url(self):
|
||||||
|
"""Gibt die Frontend-URL zurück."""
|
||||||
|
return f"http://{self.config['frontend_hostname']}:{self.config['frontend_port']}"
|
||||||
|
|
||||||
|
def check_connection_statuses(self):
|
||||||
|
"""Überprüft den Verbindungsstatus zu Backend und Frontend."""
|
||||||
|
self.last_check = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
|
||||||
|
|
||||||
|
# Prüfe Backend-Verbindung
|
||||||
|
backend_url = self.get_backend_url()
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{backend_url}/api/test", timeout=3)
|
||||||
|
if response.status_code == 200:
|
||||||
|
self.backend_status = "Verbunden"
|
||||||
|
else:
|
||||||
|
self.backend_status = f"Fehler: HTTP {response.status_code}"
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.backend_status = f"Nicht erreichbar: {str(e)}"
|
||||||
|
|
||||||
|
# Prüfe Frontend-Verbindung
|
||||||
|
frontend_url = self.get_frontend_url()
|
||||||
|
try:
|
||||||
|
response = requests.get(frontend_url, timeout=3)
|
||||||
|
if response.status_code == 200:
|
||||||
|
self.frontend_status = "Verbunden"
|
||||||
|
else:
|
||||||
|
self.frontend_status = f"Fehler: HTTP {response.status_code}"
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.frontend_status = f"Nicht erreichbar: {str(e)}"
|
||||||
|
|
||||||
|
self.logger.info(f"Verbindungsstatus - Backend: {self.backend_status}, Frontend: {self.frontend_status}")
|
||||||
|
|
||||||
|
def get_network_interfaces(self):
|
||||||
|
"""Gibt Informationen zu allen Netzwerkschnittstellen zurück."""
|
||||||
|
interfaces = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for interface in netifaces.interfaces():
|
||||||
|
if interface.startswith(('lo', 'docker', 'br-')):
|
||||||
|
continue # Ignoriere Loopback und Docker-Interfaces
|
||||||
|
|
||||||
|
addresses = []
|
||||||
|
try:
|
||||||
|
addrs = netifaces.ifaddresses(interface)
|
||||||
|
if netifaces.AF_INET in addrs:
|
||||||
|
for addr in addrs[netifaces.AF_INET]:
|
||||||
|
if 'addr' in addr:
|
||||||
|
addresses.append(addr['addr'])
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Fehler beim Ermitteln der Adresse für Interface {interface}: {e}")
|
||||||
|
|
||||||
|
if addresses:
|
||||||
|
interfaces.append({
|
||||||
|
'name': interface,
|
||||||
|
'address': ', '.join(addresses)
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Fehler beim Ermitteln der Netzwerkschnittstellen: {e}")
|
||||||
|
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
# Helper-Funktion zum Testen von Netzwerkverbindungen
|
||||||
|
def test_connection(host, port, timeout=2):
|
||||||
|
"""Testet eine TCP-Verbindung zu einem Host und Port."""
|
||||||
|
try:
|
||||||
|
socket.setdefaulttimeout(timeout)
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.connect((host, port))
|
||||||
|
s.close()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Helper-Funktion zum Ping eines Hosts
|
||||||
|
def ping_host(host, count=1):
|
||||||
|
"""Pingt einen Host an."""
|
||||||
|
param = '-n' if platform.system().lower() == 'windows' else '-c'
|
||||||
|
command = ['ping', param, str(count), host]
|
||||||
|
return subprocess.call(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0
|
@ -1,9 +1,30 @@
|
|||||||
|
# Core Flask-Abhängigkeiten
|
||||||
flask==2.3.3
|
flask==2.3.3
|
||||||
flask-cors==4.0.0
|
flask-cors==4.0.0
|
||||||
pyjwt==2.8.0
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
werkzeug==2.3.7
|
werkzeug==2.3.7
|
||||||
|
|
||||||
|
# Authentifizierung und Sicherheit
|
||||||
|
pyjwt==2.8.0
|
||||||
|
flask-wtf==1.1.1
|
||||||
|
flask-talisman==1.1.0
|
||||||
|
|
||||||
|
# Umgebung und Konfiguration
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
|
||||||
|
# WSGI-Server für Produktion
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
|
waitress==2.1.2
|
||||||
|
|
||||||
|
# Netzwerk und Hardware-Integration
|
||||||
PyP100==0.0.19
|
PyP100==0.0.19
|
||||||
netifaces==0.11.0
|
netifaces==0.11.0
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
|
|
||||||
|
# Monitoring und Logging
|
||||||
|
flask-healthcheck==0.1.0
|
||||||
|
prometheus-flask-exporter==0.23.0
|
||||||
|
|
||||||
|
# Entwicklung und Testing (optional)
|
||||||
|
pytest==7.4.3
|
||||||
|
pytest-flask==1.3.0
|
||||||
|
coverage==7.3.2
|
1
backend/security.py
Normal file
1
backend/security.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
36
backend/start-debug-server.bat
Normal file
36
backend/start-debug-server.bat
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
@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
|
52
backend/start-debug-server.sh
Normal file
52
backend/start-debug-server.sh
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pfad zum Debug-Server
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
DEBUG_SERVER_DIR="$SCRIPT_DIR/debug-server"
|
||||||
|
|
||||||
|
# Prüfe, ob das Debug-Server-Verzeichnis existiert
|
||||||
|
if [ ! -d "$DEBUG_SERVER_DIR" ]; then
|
||||||
|
error_log "Debug-Server-Verzeichnis nicht gefunden: $DEBUG_SERVER_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe, ob Python installiert ist
|
||||||
|
if ! command -v python &> /dev/null; then
|
||||||
|
error_log "Python nicht gefunden. Bitte installieren Sie Python."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe, ob pip installiert ist
|
||||||
|
if ! command -v pip &> /dev/null; then
|
||||||
|
error_log "pip nicht gefunden. Bitte installieren Sie pip."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wechsle ins Debug-Server-Verzeichnis
|
||||||
|
cd "$DEBUG_SERVER_DIR" || exit 1
|
||||||
|
|
||||||
|
# Installiere Abhängigkeiten, falls nötig
|
||||||
|
if [ -f "requirements.txt" ]; then
|
||||||
|
log "Installiere Abhängigkeiten..."
|
||||||
|
pip install -r requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Starte den Debug-Server
|
||||||
|
log "Starte Backend-Debug-Server..."
|
||||||
|
python app.py
|
38
backend/start-production.bat
Normal file
38
backend/start-production.bat
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
@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
|
56
backend/start-production.sh
Normal file
56
backend/start-production.sh
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# MYP Backend - Produktions-Startskript
|
||||||
|
# Startet die Flask-Anwendung mit Gunicorn für den Produktionsbetrieb
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== MYP Backend - Produktionsstart ==="
|
||||||
|
|
||||||
|
# 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}
|
||||||
|
|
||||||
|
# Umgebungsvariablen
|
||||||
|
export FLASK_ENV=production
|
||||||
|
export PYTHONPATH=${PYTHONPATH}:$(pwd)
|
||||||
|
|
||||||
|
# Log-Verzeichnis erstellen
|
||||||
|
mkdir -p logs
|
||||||
|
|
||||||
|
# Prüfe, ob alle erforderlichen Umgebungsvariablen gesetzt sind
|
||||||
|
if [ -z "$SECRET_KEY" ]; then
|
||||||
|
echo "WARNUNG: SECRET_KEY ist nicht gesetzt. Verwende einen generierten Schlüssel."
|
||||||
|
export SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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 ""
|
||||||
|
|
||||||
|
# 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
|
119
backend/templates/network_config.html
Normal file
119
backend/templates/network_config.html
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<!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>
|
37
backend/wsgi.py
Normal file
37
backend/wsgi.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""
|
||||||
|
WSGI-Einstiegspunkt für die MYP Flask-Anwendung.
|
||||||
|
Verwendet für den Produktionsbetrieb mit WSGI-Servern wie Gunicorn.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Lade Umgebungsvariablen
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
from app import create_app
|
||||||
|
|
||||||
|
# Erstelle Anwendungsinstanz für Produktionsbetrieb
|
||||||
|
flask_env = os.environ.get('FLASK_ENV', 'production')
|
||||||
|
application = create_app(flask_env)
|
||||||
|
|
||||||
|
# Initialisierung für WSGI-Server
|
||||||
|
with application.app_context():
|
||||||
|
from app import init_db, init_printers, setup_frontend_v2
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Datenbank initialisieren
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
# Drucker initialisieren, falls konfiguriert
|
||||||
|
printers_config = json.loads(application.config.get('PRINTERS', '{}'))
|
||||||
|
if printers_config:
|
||||||
|
init_printers()
|
||||||
|
|
||||||
|
# Frontend v2 Setup
|
||||||
|
setup_frontend_v2()
|
||||||
|
|
||||||
|
application.logger.info(f'MYP Backend gestartet in {flask_env} Modus')
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
application.run()
|
50
cleanup.ps1
Normal file
50
cleanup.ps1
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
Write-Host "MYP-Umgebung wird bereinigt..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Stoppen der Debug-Server, falls sie laufen
|
||||||
|
if (Test-Path -Path "logs\backend-debug.jobid") {
|
||||||
|
Write-Host "Stoppe Backend Debug-Server..." -ForegroundColor Yellow
|
||||||
|
$jobId = Get-Content "logs\backend-debug.jobid"
|
||||||
|
Stop-Job -Id $jobId -ErrorAction SilentlyContinue
|
||||||
|
Remove-Job -Id $jobId -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item "logs\backend-debug.jobid" -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -Path "logs\frontend-debug.jobid") {
|
||||||
|
Write-Host "Stoppe Frontend Debug-Server..." -ForegroundColor Yellow
|
||||||
|
$jobId = Get-Content "logs\frontend-debug.jobid"
|
||||||
|
Stop-Job -Id $jobId -ErrorAction SilentlyContinue
|
||||||
|
Remove-Job -Id $jobId -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item "logs\frontend-debug.jobid" -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stoppen und Entfernen aller Docker-Container
|
||||||
|
Write-Host "Stoppe und entferne alle MYP-Container..." -ForegroundColor Yellow
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Entfernen aller MYP-Container, auch die bereits gestoppten
|
||||||
|
Write-Host "Entferne alle MYP-Container..." -ForegroundColor Yellow
|
||||||
|
$containers = docker ps -a --filter "name=myp-" -q
|
||||||
|
if ($containers) {
|
||||||
|
docker rm -f $containers
|
||||||
|
}
|
||||||
|
|
||||||
|
# Entfernen aller MYP-Images
|
||||||
|
Write-Host "Entferne alle MYP-Images..." -ForegroundColor Yellow
|
||||||
|
$images = docker images --filter "reference=*myp*" -q
|
||||||
|
if ($images) {
|
||||||
|
docker rmi -f $images
|
||||||
|
}
|
||||||
|
|
||||||
|
# Entfernen von nicht verwendeten Volumes (optional)
|
||||||
|
Write-Host "Entferne nicht verwendete Volumes..." -ForegroundColor Yellow
|
||||||
|
docker volume prune -f
|
||||||
|
|
||||||
|
# Entfernen von nicht verwendeten Netzwerken (optional)
|
||||||
|
Write-Host "Entferne nicht verwendete Netzwerke..." -ForegroundColor Yellow
|
||||||
|
docker network prune -f
|
||||||
|
|
||||||
|
# Entfernen von Build-Cache (optional)
|
||||||
|
Write-Host "Entferne Docker Build-Cache..." -ForegroundColor Yellow
|
||||||
|
docker builder prune -f
|
||||||
|
|
||||||
|
Write-Host "Bereinigung abgeschlossen. Sie können nun 'start.ps1' ausführen, um eine frische Installation zu starten." -ForegroundColor Green
|
42
cleanup.sh
Normal file
42
cleanup.sh
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "MYP-Umgebung wird bereinigt..."
|
||||||
|
|
||||||
|
# Stoppen der Debug-Server, falls sie laufen
|
||||||
|
if [ -f logs/backend-debug.pid ]; then
|
||||||
|
echo "Stoppe Backend Debug-Server..."
|
||||||
|
kill $(cat logs/backend-debug.pid) 2>/dev/null || true
|
||||||
|
rm logs/backend-debug.pid
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f logs/frontend-debug.pid ]; then
|
||||||
|
echo "Stoppe Frontend Debug-Server..."
|
||||||
|
kill $(cat logs/frontend-debug.pid) 2>/dev/null || true
|
||||||
|
rm logs/frontend-debug.pid
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stoppen und Entfernen aller Docker-Container
|
||||||
|
echo "Stoppe und entferne alle MYP-Container..."
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Entfernen aller MYP-Container, auch die bereits gestoppten
|
||||||
|
echo "Entferne alle MYP-Container..."
|
||||||
|
docker ps -a --filter "name=myp-" -q | xargs -r docker rm -f
|
||||||
|
|
||||||
|
# Entfernen aller MYP-Images
|
||||||
|
echo "Entferne alle MYP-Images..."
|
||||||
|
docker images | grep "myp-" | awk '{print $3}' | xargs -r docker rmi -f
|
||||||
|
|
||||||
|
# Entfernen von nicht verwendeten Volumes (optional)
|
||||||
|
echo "Entferne nicht verwendete Volumes..."
|
||||||
|
docker volume prune -f
|
||||||
|
|
||||||
|
# Entfernen von nicht verwendeten Netzwerken (optional)
|
||||||
|
echo "Entferne nicht verwendete Netzwerke..."
|
||||||
|
docker network prune -f
|
||||||
|
|
||||||
|
# Entfernen von Build-Cache (optional)
|
||||||
|
echo "Entferne Docker Build-Cache..."
|
||||||
|
docker builder prune -f
|
||||||
|
|
||||||
|
echo "Bereinigung abgeschlossen. Sie können nun 'start.sh' ausführen, um eine frische Installation zu starten."
|
80
config/README.md
Normal file
80
config/README.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# MYP-Projekt Service-Installation
|
||||||
|
|
||||||
|
Diese Anleitung beschreibt, wie der MYP-Projektservice als systemd-Dienst eingerichtet wird, damit das System beim Booten automatisch startet.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Docker und Docker Compose sind installiert
|
||||||
|
- sudo-Rechte auf dem System
|
||||||
|
|
||||||
|
## Installation des Services
|
||||||
|
|
||||||
|
1. Bearbeiten Sie die Datei `myp-service.service` und passen Sie die Pfade an:
|
||||||
|
|
||||||
|
- Ersetzen Sie `/path/to/Projektarbeit-MYP` mit dem tatsächlichen Pfad zum Projektverzeichnis
|
||||||
|
2. Kopieren Sie die Service-Datei in das systemd-Verzeichnis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp myp-service.service /etc/systemd/system/
|
||||||
|
```
|
||||||
|
3. Aktualisieren Sie die systemd-Konfiguration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
4. Aktivieren Sie den Service, damit er beim Booten startet:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable myp-service
|
||||||
|
```
|
||||||
|
5. Starten Sie den Service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl start myp-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Überprüfen des Service-Status
|
||||||
|
|
||||||
|
Um den Status des Services zu überprüfen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl status myp-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stoppen des Services
|
||||||
|
|
||||||
|
Um den Service zu stoppen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl stop myp-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deaktivieren des Autostart
|
||||||
|
|
||||||
|
Um den automatischen Start zu deaktivieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl disable myp-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fehlerbehebung
|
||||||
|
|
||||||
|
Überprüfen Sie die Logs bei Problemen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u myp-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hinweis für Windows-Systeme
|
||||||
|
|
||||||
|
Für Windows-Systeme empfehlen wir die Verwendung des Task-Schedulers:
|
||||||
|
|
||||||
|
1. Öffnen Sie den Task-Scheduler (taskschd.msc)
|
||||||
|
2. Erstellen Sie eine neue Aufgabe:
|
||||||
|
- Trigger: "Bei Start"
|
||||||
|
- Aktion: "Programm starten"
|
||||||
|
- Programm/Skript: `powershell.exe`
|
||||||
|
- Argumente: `-ExecutionPolicy Bypass -File "C:\Pfad\zu\Projektarbeit-MYP\start.ps1"`
|
||||||
|
- "Mit höchsten Privilegien ausführen" aktivieren
|
||||||
|
|
||||||
|
Dadurch wird das MYP-Projekt automatisch beim Systemstart gestartet.
|
58
config/install-linux-service.sh
Normal file
58
config/install-linux-service.sh
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# MYP-Projekt systemd-Service Installationsskript
|
||||||
|
|
||||||
|
# Überprüfen, ob das Skript mit Root-Rechten ausgeführt wird
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "Bitte führen Sie dieses Skript mit Root-Rechten aus (sudo)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ermitteln des Projektpfads
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
echo "MYP-Projekt Service-Installation"
|
||||||
|
echo "================================"
|
||||||
|
echo "Projektpfad: $PROJECT_DIR"
|
||||||
|
|
||||||
|
# Kopieren der Service-Datei mit angepasstem Pfad
|
||||||
|
echo "Erstelle systemd-Service-Datei..."
|
||||||
|
cp "$SCRIPT_DIR/myp-service.service" /tmp/myp-service.service
|
||||||
|
sed -i "s|/path/to/Projektarbeit-MYP|$PROJECT_DIR|g" /tmp/myp-service.service
|
||||||
|
|
||||||
|
# Kopieren der Service-Datei in das systemd-Verzeichnis
|
||||||
|
echo "Installiere systemd-Service..."
|
||||||
|
cp /tmp/myp-service.service /etc/systemd/system/
|
||||||
|
rm /tmp/myp-service.service
|
||||||
|
|
||||||
|
# Setze Ausführungsrechte für das Start-Skript
|
||||||
|
chmod +x "$PROJECT_DIR/start.sh"
|
||||||
|
|
||||||
|
# Systemd aktualisieren
|
||||||
|
echo "Aktualisiere systemd..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Service aktivieren
|
||||||
|
echo "Aktiviere Service für Autostart..."
|
||||||
|
systemctl enable myp-service
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Installation abgeschlossen."
|
||||||
|
echo "Möchten Sie den Service jetzt starten? (j/n)"
|
||||||
|
read -r ANTWORT
|
||||||
|
|
||||||
|
if [[ "$ANTWORT" =~ ^[Jj]$ ]]; then
|
||||||
|
echo "Starte MYP-Projekt Service..."
|
||||||
|
systemctl start myp-service
|
||||||
|
|
||||||
|
# Status anzeigen
|
||||||
|
echo
|
||||||
|
echo "Service-Status:"
|
||||||
|
systemctl status myp-service --no-pager
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Sie können den Service-Status jederzeit mit folgendem Befehl überprüfen:"
|
||||||
|
echo " sudo systemctl status myp-service"
|
||||||
|
echo
|
||||||
|
echo "Der MYP-Projekt Service wird nun bei jedem Systemstart automatisch gestartet."
|
66
config/install-windows-service.bat
Normal file
66
config/install-windows-service.bat
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
@echo off
|
||||||
|
echo MYP-Projekt Autostart-Einrichtung
|
||||||
|
echo =================================
|
||||||
|
|
||||||
|
REM Erfordert Admin-Rechte
|
||||||
|
NET SESSION >nul 2>&1
|
||||||
|
IF %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo Bitte führen Sie dieses Skript mit Administratorrechten aus.
|
||||||
|
echo Klicken Sie mit der rechten Maustaste und wählen Sie "Als Administrator ausführen".
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Pfad zum Projektverzeichnis ermitteln
|
||||||
|
set SCRIPT_DIR=%~dp0
|
||||||
|
set PROJECT_DIR=%SCRIPT_DIR%..
|
||||||
|
cd %PROJECT_DIR%
|
||||||
|
set PROJECT_PATH=%CD%
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Projektpfad: %PROJECT_PATH%
|
||||||
|
|
||||||
|
REM Erstellung der PowerShell-Skriptdatei für den Task
|
||||||
|
echo Erstelle PowerShell-Skriptdatei für den Windows Task...
|
||||||
|
set PS_SCRIPT=%PROJECT_PATH%\config\secure\myp-autostart.ps1
|
||||||
|
|
||||||
|
if not exist "%PROJECT_PATH%\config\secure" mkdir "%PROJECT_PATH%\config\secure"
|
||||||
|
|
||||||
|
echo $ErrorActionPreference = "Stop" > "%PS_SCRIPT%"
|
||||||
|
echo try { >> "%PS_SCRIPT%"
|
||||||
|
echo Write-Host "Starte MYP-Projekt..." >> "%PS_SCRIPT%"
|
||||||
|
echo Set-Location -Path "%PROJECT_PATH%" >> "%PS_SCRIPT%"
|
||||||
|
echo Start-Process -FilePath "powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -File '%PROJECT_PATH%\start.ps1'" >> "%PS_SCRIPT%"
|
||||||
|
echo Write-Host "MYP-Projekt erfolgreich gestartet" >> "%PS_SCRIPT%"
|
||||||
|
echo } catch { >> "%PS_SCRIPT%"
|
||||||
|
echo $ErrorMessage = $_.Exception.Message >> "%PS_SCRIPT%"
|
||||||
|
echo Write-Host "Fehler beim Starten des MYP-Projekts: $ErrorMessage" >> "%PS_SCRIPT%"
|
||||||
|
echo Add-Content -Path "%PROJECT_PATH%\logs\autostart_error.log" -Value "$(Get-Date) - Fehler: $ErrorMessage" >> "%PS_SCRIPT%"
|
||||||
|
echo exit 1 >> "%PS_SCRIPT%"
|
||||||
|
echo } >> "%PS_SCRIPT%"
|
||||||
|
|
||||||
|
REM Erstellung des geplanten Tasks
|
||||||
|
echo Erstelle geplanten Windows Task...
|
||||||
|
schtasks /create /tn "MYP-Projekt Autostart" /sc onstart /delay 0000:30 /ru "System" /rl highest /tr "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File \"%PS_SCRIPT%\"" /f
|
||||||
|
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo Fehler bei der Erstellung des geplanten Tasks.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Der MYP-Projekt Autostart wurde erfolgreich eingerichtet.
|
||||||
|
echo Das System wird nun bei jedem Systemstart automatisch das MYP-Projekt starten.
|
||||||
|
echo.
|
||||||
|
echo Möchten Sie das Projekt jetzt starten?
|
||||||
|
choice /c JN /m "Projekt jetzt starten (J/N)?"
|
||||||
|
|
||||||
|
if %ERRORLEVEL% EQU 1 (
|
||||||
|
echo Starte MYP-Projekt...
|
||||||
|
powershell.exe -ExecutionPolicy Bypass -File "%PROJECT_PATH%\start.ps1"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Installation abgeschlossen.
|
||||||
|
pause
|
14
config/myp-service.service
Normal file
14
config/myp-service.service
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MYP Projektarbeit Service
|
||||||
|
After=docker.service network.target
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
WorkingDirectory=/path/to/Projektarbeit-MYP
|
||||||
|
ExecStart=/path/to/Projektarbeit-MYP/start.sh
|
||||||
|
ExecStop=/usr/bin/docker-compose -f /path/to/Projektarbeit-MYP/docker-compose.yml down
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
110
docker-compose.dev.yml
Normal file
110
docker-compose.dev.yml
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# 🔧 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
|
95
docker-compose.yml
Normal file
95
docker-compose.yml
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Backend
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
container_name: myp-backend
|
||||||
|
restart: always
|
||||||
|
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
|
||||||
|
volumes:
|
||||||
|
- ./backend/logs:/app/logs
|
||||||
|
- ./backend/instance:/app/instance
|
||||||
|
networks:
|
||||||
|
myp-network:
|
||||||
|
ipv4_address: 192.168.0.5
|
||||||
|
expose:
|
||||||
|
- "5000"
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:5000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Next.js Frontend
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
container_name: myp-rp
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- NEXT_PUBLIC_API_URL=/api
|
||||||
|
networks:
|
||||||
|
- myp-network
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
# Caddy Proxy
|
||||||
|
caddy:
|
||||||
|
image: caddy:2.7-alpine
|
||||||
|
container_name: myp-caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./frontend/docker/caddy/Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
networks:
|
||||||
|
- myp-network
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
environment:
|
||||||
|
- CADDY_HOST=53.37.211.254
|
||||||
|
- CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
depends_on:
|
||||||
|
- frontend
|
||||||
|
- backend
|
||||||
|
|
||||||
|
networks:
|
||||||
|
myp-network:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
driver: default
|
||||||
|
config:
|
||||||
|
- subnet: 192.168.0.0/24
|
||||||
|
gateway: 192.168.0.1
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
|
caddy_config:
|
43
frontend/.gitignore
vendored
Normal file
43
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# db folder
|
||||||
|
db/
|
||||||
|
|
||||||
|
# Env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
34
frontend/Dockerfile
Normal file
34
frontend/Dockerfile
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
FROM node:20-bookworm-slim
|
||||||
|
|
||||||
|
# Create application directory
|
||||||
|
RUN mkdir -p /usr/src/app
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Copy package.json and pnpm-lock.yaml
|
||||||
|
COPY package.json /usr/src/app
|
||||||
|
COPY pnpm-lock.yaml /usr/src/app
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN corepack enable pnpm
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pnpm install
|
||||||
|
|
||||||
|
# Copy the rest of the application code
|
||||||
|
COPY . /usr/src/app
|
||||||
|
|
||||||
|
# Initialize Database, if it not already exists
|
||||||
|
RUN pnpm run db
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["/bin/sh", "-c", "if [ ! -f ./db/sqlite.db ]; then pnpm db; fi && pnpm start"]
|
32
frontend/README.md
Normal file
32
frontend/README.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# MYP - Manage Your Printer
|
||||||
|
|
||||||
|
MYP (Manage Your Printer) ist eine Webanwendung zur Reservierung von 3D-Druckern.
|
||||||
|
Sie wurde im Rahmen des Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
- Netzwerk auf Raspberry Pi ist eingerichtet
|
||||||
|
- Docker ist installiert
|
||||||
|
|
||||||
|
### Schritte
|
||||||
|
|
||||||
|
1. Docker-Container bauen (docker/build.sh)
|
||||||
|
2. Docker-Container speichern (docker/save.sh caddy:2.8 myp-rp:latest)
|
||||||
|
3. Docker-Container auf Raspberry Pi bereitstellen (docker/deploy.sh)
|
||||||
|
|
||||||
|
## Entwicklerinformationen
|
||||||
|
|
||||||
|
### Raspberry Pi Einstellungen
|
||||||
|
|
||||||
|
Auf dem Raspberry Pi wurde Raspbian Lite installiert.
|
||||||
|
Unter /srv/* sind die Projektdateien zu finden.
|
||||||
|
|
||||||
|
### Anmeldedaten
|
||||||
|
|
||||||
|
```
|
||||||
|
Benutzer: myp
|
||||||
|
Passwort: (persönlich bekannt)
|
||||||
|
```
|
||||||
|
|
19
frontend/biome.json
Normal file
19
frontend/biome.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"lineWidth": 120
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"correctness": {
|
||||||
|
"noUnusedImports": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
245
frontend/cleanup.sh
Normal file
245
frontend/cleanup.sh
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Raspberry Pi Bereinigungsskript für MYP-Projekt Frontend
|
||||||
|
# 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 Frontend Raspberry Pi Bereinigung und Setup ===${NC}"
|
||||||
|
log "Dieses 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 das Frontend
|
||||||
|
log "${YELLOW}Installiere zusätzliche Frontend-Abhängigkeiten...${NC}"
|
||||||
|
apt-get install -y \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
|| {
|
||||||
|
error_log "Konnte zusätzliche Frontend-Abhängigkeiten nicht installieren."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installiere Node.js LTS-Version über NodeSource
|
||||||
|
log "${YELLOW}Installiere aktuelle Node.js LTS-Version...${NC}"
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
|
||||||
|
# Überprüfe Node.js-Installation
|
||||||
|
NODE_VERSION=$(node -v)
|
||||||
|
log "${GREEN}Node.js $NODE_VERSION erfolgreich installiert!${NC}"
|
||||||
|
|
||||||
|
# Installiere pnpm
|
||||||
|
log "${YELLOW}Installiere pnpm Paketmanager...${NC}"
|
||||||
|
npm install -g pnpm
|
||||||
|
log "${GREEN}pnpm $(pnpm --version) erfolgreich installiert!${NC}"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Erstelle Datenbankverzeichnis für Frontend
|
||||||
|
log "Erstelle Datenbankverzeichnis für Frontend..."
|
||||||
|
mkdir -p /srv/MYP-DB
|
||||||
|
chmod 777 /srv/MYP-DB
|
||||||
|
|
||||||
|
# Erstelle Umgebungsvariablen-Verzeichnis
|
||||||
|
log "Erstelle Umgebungsvariablen-Verzeichnis..."
|
||||||
|
mkdir -p /srv/myp-env
|
||||||
|
cat > /srv/myp-env/github.env << EOL
|
||||||
|
# OAuth-Konfiguration für Frontend
|
||||||
|
OAUTH_CLIENT_ID=client_id
|
||||||
|
OAUTH_CLIENT_SECRET=client_secret
|
||||||
|
NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
|
||||||
|
EOL
|
||||||
|
chmod 600 /srv/myp-env/github.env
|
||||||
|
|
||||||
|
# Prüfe, ob Frontend-Installationsdateien vorhanden sind
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
FRONTEND_DIR="$SCRIPT_DIR"
|
||||||
|
|
||||||
|
if [ -f "$FRONTEND_DIR/package.json" ]; then
|
||||||
|
log "${GREEN}Frontend-Projektdateien gefunden in $FRONTEND_DIR${NC}"
|
||||||
|
else
|
||||||
|
log "${YELLOW}Warnung: Frontend-Projektdateien nicht gefunden in $FRONTEND_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 Frontend-Installationsskript ausführen:"
|
||||||
|
log " cd $FRONTEND_DIR && ./install.sh"
|
||||||
|
log "3. Bei Problemen mit Docker-Berechtigungen stellen Sie sicher, dass Sie sich neu angemeldet haben."
|
||||||
|
log "4. Sie müssen noch die OAuth-Konfiguration in /srv/myp-env/github.env anpassen."
|
||||||
|
|
||||||
|
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
|
17
frontend/components.json
Normal file
17
frontend/components.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/utils/styles"
|
||||||
|
}
|
||||||
|
}
|
18
frontend/debug-server/package.json
Normal file
18
frontend/debug-server/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "myp-frontend-debug-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Debug-Server für das MYP Frontend",
|
||||||
|
"main": "src/app.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/app.js",
|
||||||
|
"dev": "nodemon src/app.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"ejs": "^3.1.9",
|
||||||
|
"express": "^4.18.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.1"
|
||||||
|
}
|
||||||
|
}
|
417
frontend/debug-server/public/css/style.css
Normal file
417
frontend/debug-server/public/css/style.css
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
/* Variablen */
|
||||||
|
:root {
|
||||||
|
--primary-color: #3f51b5;
|
||||||
|
--secondary-color: #283593;
|
||||||
|
--accent-color: #ff4081;
|
||||||
|
--background-color: #f5f5f5;
|
||||||
|
--card-color: #ffffff;
|
||||||
|
--text-color: #333333;
|
||||||
|
--text-light: #757575;
|
||||||
|
--border-color: #dddddd;
|
||||||
|
--success-color: #4caf50;
|
||||||
|
--warning-color: #ff9800;
|
||||||
|
--danger-color: #f44336;
|
||||||
|
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
--card-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grundlegende Stile */
|
||||||
|
* {
|
||||||
|
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);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 99;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
background: none;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button.active {
|
||||||
|
color: white;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel.active {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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(--card-shadow);
|
||||||
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loader */
|
||||||
|
.loader {
|
||||||
|
text-align: center;
|
||||||
|
margin: 2rem 0;
|
||||||
|
color: var(--text-light);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fortschrittsbalken */
|
||||||
|
.progress-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 8px;
|
||||||
|
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;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: rgba(0, 0, 0, 0.05);
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status-Anzeigen */
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Echtzeit-Monitor */
|
||||||
|
.gauge-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge {
|
||||||
|
position: relative;
|
||||||
|
width: 150px;
|
||||||
|
height: 75px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-body {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-top-left-radius: 100px;
|
||||||
|
border-top-right-radius: 100px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-fill {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
transition: height 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-cover {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 90%;
|
||||||
|
height: 90%;
|
||||||
|
margin-left: 5%;
|
||||||
|
margin-bottom: 5%;
|
||||||
|
background-color: var(--card-color);
|
||||||
|
border-top-left-radius: 80px;
|
||||||
|
border-top-right-radius: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-value {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -25px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CPU-Cores */
|
||||||
|
.cpu-core {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu-core-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu-core-bar {
|
||||||
|
height: 6px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu-core-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Network Chart */
|
||||||
|
canvas {
|
||||||
|
margin-top: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge {
|
||||||
|
width: 120px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
505
frontend/debug-server/public/js/script.js
Normal file
505
frontend/debug-server/public/js/script.js
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
// DOM-Element-Referenzen
|
||||||
|
const navButtons = document.querySelectorAll('.nav-button');
|
||||||
|
const panels = document.querySelectorAll('.panel');
|
||||||
|
|
||||||
|
// Panel-Navigation
|
||||||
|
navButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const panelId = button.getAttribute('data-panel');
|
||||||
|
|
||||||
|
// 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(panelId).classList.add('active');
|
||||||
|
|
||||||
|
// Lade Panel-Daten, wenn sie noch nicht geladen wurden
|
||||||
|
if (panelId === 'system' && document.getElementById('system-container').style.display === 'none') {
|
||||||
|
loadSystemInfo();
|
||||||
|
} else if (panelId === 'network' && document.getElementById('network-container').style.display === 'none') {
|
||||||
|
loadNetworkInfo();
|
||||||
|
} else if (panelId === 'services' && document.getElementById('services-container').style.display === 'none') {
|
||||||
|
loadServicesInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// API-Anfragen
|
||||||
|
async function fetchData(endpoint) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP-Fehler: ${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 loader = document.getElementById('system-loader');
|
||||||
|
const container = document.getElementById('system-container');
|
||||||
|
|
||||||
|
loader.style.display = 'block';
|
||||||
|
container.style.display = 'none';
|
||||||
|
|
||||||
|
const data = await fetchData('/api/system');
|
||||||
|
if (!data) {
|
||||||
|
loader.textContent = 'Fehler beim Laden der Systemdaten';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Betriebssystem-Info
|
||||||
|
document.getElementById('os-info').innerHTML = `
|
||||||
|
<p><strong>Plattform:</strong> ${data.os.platform}</p>
|
||||||
|
<p><strong>Distribution:</strong> ${data.os.distro}</p>
|
||||||
|
<p><strong>Version:</strong> ${data.os.release}</p>
|
||||||
|
<p><strong>Architektur:</strong> ${data.os.arch}</p>
|
||||||
|
<p><strong>Betriebszeit:</strong> ${data.os.uptime}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// CPU-Info
|
||||||
|
document.getElementById('cpu-info').innerHTML = `
|
||||||
|
<p><strong>Hersteller:</strong> ${data.cpu.manufacturer}</p>
|
||||||
|
<p><strong>Modell:</strong> ${data.cpu.brand}</p>
|
||||||
|
<p><strong>Geschwindigkeit:</strong> ${data.cpu.speed} GHz</p>
|
||||||
|
<p><strong>Kerne:</strong> ${data.cpu.cores} (${data.cpu.physicalCores} physisch)</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Speicher-Info
|
||||||
|
document.getElementById('memory-info').innerHTML = `
|
||||||
|
<p><strong>Gesamt:</strong> ${data.memory.total}</p>
|
||||||
|
<p><strong>Verwendet:</strong> ${data.memory.used} (${data.memory.usedPercent}%)</p>
|
||||||
|
<p><strong>Frei:</strong> ${data.memory.free}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('memory-bar').style.width = `${data.memory.usedPercent}%`;
|
||||||
|
document.getElementById('memory-bar').style.backgroundColor = getColorByPercentage(data.memory.usedPercent);
|
||||||
|
|
||||||
|
// Festplatten-Info
|
||||||
|
let diskHTML = '<table><tr><th>Laufwerk</th><th>Typ</th><th>Größe</th><th>Verwendet</th><th>Verfügbar</th><th>Nutzung</th></tr>';
|
||||||
|
|
||||||
|
data.filesystem.forEach(fs => {
|
||||||
|
diskHTML += `
|
||||||
|
<tr>
|
||||||
|
<td>${fs.mount}</td>
|
||||||
|
<td>${fs.type}</td>
|
||||||
|
<td>${fs.size}</td>
|
||||||
|
<td>${fs.used}</td>
|
||||||
|
<td>${fs.available}</td>
|
||||||
|
<td>
|
||||||
|
<div class="progress-container" style="margin: 0; width: 100%">
|
||||||
|
<div class="progress-bar" style="width: ${fs.usePercent}%; background-color: ${getColorByPercentage(fs.usePercent)}"></div>
|
||||||
|
</div>
|
||||||
|
${fs.usePercent}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
diskHTML += '</table>';
|
||||||
|
document.getElementById('disk-info').innerHTML = diskHTML;
|
||||||
|
|
||||||
|
// UI anzeigen
|
||||||
|
loader.style.display = 'none';
|
||||||
|
container.style.display = 'grid';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Netzwerk-Informationen laden
|
||||||
|
async function loadNetworkInfo() {
|
||||||
|
const loader = document.getElementById('network-loader');
|
||||||
|
const container = document.getElementById('network-container');
|
||||||
|
|
||||||
|
loader.style.display = 'block';
|
||||||
|
container.style.display = 'none';
|
||||||
|
|
||||||
|
const data = await fetchData('/api/network');
|
||||||
|
if (!data) {
|
||||||
|
loader.textContent = 'Fehler beim Laden der Netzwerkdaten';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Netzwerkschnittstellen
|
||||||
|
let interfacesHTML = '<table><tr><th>Name</th><th>Status</th><th>IPv4</th><th>IPv6</th><th>MAC</th><th>Typ</th><th>DHCP</th></tr>';
|
||||||
|
|
||||||
|
data.interfaces.forEach(iface => {
|
||||||
|
interfacesHTML += `
|
||||||
|
<tr>
|
||||||
|
<td>${iface.iface}</td>
|
||||||
|
<td><span class="status ${iface.operstate === 'up' ? 'status-online' : 'status-offline'}">${iface.operstate}</span></td>
|
||||||
|
<td>${iface.ip4 || '-'}</td>
|
||||||
|
<td>${iface.ip6 || '-'}</td>
|
||||||
|
<td>${iface.mac || '-'}</td>
|
||||||
|
<td>${iface.type || '-'}</td>
|
||||||
|
<td>${iface.dhcp ? 'Ja' : 'Nein'}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
interfacesHTML += '</table>';
|
||||||
|
document.getElementById('network-interfaces').innerHTML = interfacesHTML;
|
||||||
|
|
||||||
|
// DNS-Server
|
||||||
|
let dnsHTML = '<ul>';
|
||||||
|
data.dns.forEach(server => {
|
||||||
|
dnsHTML += `<li>${server}</li>`;
|
||||||
|
});
|
||||||
|
dnsHTML += '</ul>';
|
||||||
|
document.getElementById('dns-servers').innerHTML = dnsHTML;
|
||||||
|
|
||||||
|
// Gateway
|
||||||
|
document.getElementById('default-gateway').innerHTML = `<p>${data.gateway || 'Nicht verfügbar'}</p>`;
|
||||||
|
|
||||||
|
// Netzwerkstatistiken
|
||||||
|
let statsHTML = '<table><tr><th>Schnittstelle</th><th>Empfangen</th><th>Gesendet</th><th>Empfangen/s</th><th>Gesendet/s</th></tr>';
|
||||||
|
|
||||||
|
data.stats.forEach(stat => {
|
||||||
|
statsHTML += `
|
||||||
|
<tr>
|
||||||
|
<td>${stat.iface}</td>
|
||||||
|
<td>${stat.rx_bytes}</td>
|
||||||
|
<td>${stat.tx_bytes}</td>
|
||||||
|
<td>${stat.rx_sec}</td>
|
||||||
|
<td>${stat.tx_sec}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
statsHTML += '</table>';
|
||||||
|
document.getElementById('network-stats').innerHTML = statsHTML;
|
||||||
|
|
||||||
|
// UI anzeigen
|
||||||
|
loader.style.display = 'none';
|
||||||
|
container.style.display = 'grid';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dienst-Informationen laden
|
||||||
|
async function loadServicesInfo() {
|
||||||
|
const loader = document.getElementById('services-loader');
|
||||||
|
const container = document.getElementById('services-container');
|
||||||
|
|
||||||
|
loader.style.display = 'block';
|
||||||
|
container.style.display = 'none';
|
||||||
|
|
||||||
|
const data = await fetchData('/api/services');
|
||||||
|
if (!data) {
|
||||||
|
loader.textContent = 'Fehler beim Laden der Dienstdaten';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frontend-Status
|
||||||
|
document.getElementById('frontend-status').innerHTML = `
|
||||||
|
<p>Status: <span class="status ${data.frontend.status === 'online' ? 'status-online' : 'status-offline'}">${data.frontend.status}</span></p>
|
||||||
|
<p><strong>Name:</strong> ${data.frontend.name}</p>
|
||||||
|
<p><strong>Host:</strong> ${data.frontend.host}</p>
|
||||||
|
<p><strong>Port:</strong> ${data.frontend.port}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Backend-Status
|
||||||
|
document.getElementById('backend-status').innerHTML = `
|
||||||
|
<p>Status: <span class="status ${data.backend.status === 'online' ? 'status-online' : 'status-offline'}">${data.backend.status}</span></p>
|
||||||
|
<p><strong>Name:</strong> ${data.backend.name}</p>
|
||||||
|
<p><strong>Host:</strong> ${data.backend.host}</p>
|
||||||
|
<p><strong>Port:</strong> ${data.backend.port}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Docker-Container
|
||||||
|
if (data.docker.containers && data.docker.containers.length > 0) {
|
||||||
|
let containersHTML = '<table><tr><th>ID</th><th>Name</th><th>Image</th><th>Status</th><th>Ports</th></tr>';
|
||||||
|
|
||||||
|
data.docker.containers.forEach(container => {
|
||||||
|
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-container-list').innerHTML = containersHTML;
|
||||||
|
} else {
|
||||||
|
document.getElementById('docker-container-list').innerHTML = '<p>Keine Docker-Container gefunden</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI anzeigen
|
||||||
|
loader.style.display = 'none';
|
||||||
|
container.style.display = 'grid';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Netzwerk-Tools
|
||||||
|
document.getElementById('ping-button').addEventListener('click', async () => {
|
||||||
|
const hostInput = document.getElementById('ping-host');
|
||||||
|
const resultBox = document.getElementById('ping-result');
|
||||||
|
const host = hostInput.value.trim();
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
resultBox.textContent = 'Bitte geben Sie einen Hostnamen oder eine IP-Adresse ein';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBox.textContent = 'Ping wird ausgeführt...';
|
||||||
|
|
||||||
|
const data = await fetchData(`/api/ping/${encodeURIComponent(host)}`);
|
||||||
|
if (!data) {
|
||||||
|
resultBox.textContent = 'Fehler beim Ausführen des Ping-Befehls';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBox.textContent = data.output || data.error || 'Keine Ausgabe';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('traceroute-button').addEventListener('click', async () => {
|
||||||
|
const hostInput = document.getElementById('traceroute-host');
|
||||||
|
const resultBox = document.getElementById('traceroute-result');
|
||||||
|
const host = hostInput.value.trim();
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
resultBox.textContent = 'Bitte geben Sie einen Hostnamen oder eine IP-Adresse ein';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBox.textContent = 'Traceroute wird ausgeführt...';
|
||||||
|
|
||||||
|
const data = await fetchData(`/api/traceroute/${encodeURIComponent(host)}`);
|
||||||
|
if (!data) {
|
||||||
|
resultBox.textContent = 'Fehler beim Ausführen des Traceroute-Befehls';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBox.textContent = data.output || data.error || 'Keine Ausgabe';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('nslookup-button').addEventListener('click', async () => {
|
||||||
|
const hostInput = document.getElementById('nslookup-host');
|
||||||
|
const resultBox = document.getElementById('nslookup-result');
|
||||||
|
const host = hostInput.value.trim();
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
resultBox.textContent = 'Bitte geben Sie einen Hostnamen oder eine IP-Adresse ein';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBox.textContent = 'DNS-Abfrage wird ausgeführt...';
|
||||||
|
|
||||||
|
const data = await fetchData(`/api/nslookup/${encodeURIComponent(host)}`);
|
||||||
|
if (!data) {
|
||||||
|
resultBox.textContent = 'Fehler beim Ausführen des NSLookup-Befehls';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBox.textContent = data.output || data.error || 'Keine Ausgabe';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Echtzeit-Monitoring mit WebSockets
|
||||||
|
const socket = io();
|
||||||
|
|
||||||
|
// CPU- und RAM-Statistiken
|
||||||
|
socket.on('system-stats', data => {
|
||||||
|
// CPU-Anzeige aktualisieren
|
||||||
|
const cpuPercentage = data.cpu.load;
|
||||||
|
document.getElementById('cpu-percentage').textContent = `${cpuPercentage}%`;
|
||||||
|
document.getElementById('cpu-gauge').style.height = `${cpuPercentage}%`;
|
||||||
|
document.getElementById('cpu-gauge').style.backgroundColor = getColorByPercentage(cpuPercentage);
|
||||||
|
|
||||||
|
// CPU-Kerne anzeigen
|
||||||
|
const cpuCoresContainer = document.getElementById('cpu-cores-container');
|
||||||
|
|
||||||
|
if (cpuCoresContainer.childElementCount === 0) {
|
||||||
|
// Erstelle Kern-Anzeigen, falls sie noch nicht existieren
|
||||||
|
data.cpu.cores.forEach((load, index) => {
|
||||||
|
const coreElement = document.createElement('div');
|
||||||
|
coreElement.className = 'cpu-core';
|
||||||
|
coreElement.innerHTML = `
|
||||||
|
<div class="cpu-core-label">
|
||||||
|
<span>Kern ${index}</span>
|
||||||
|
<span id="cpu-core-${index}-value">${load}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="cpu-core-bar">
|
||||||
|
<div id="cpu-core-${index}-fill" class="cpu-core-fill" style="width: ${load}%; background-color: ${getColorByPercentage(load)}"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
cpuCoresContainer.appendChild(coreElement);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Aktualisiere bestehende Kern-Anzeigen
|
||||||
|
data.cpu.cores.forEach((load, index) => {
|
||||||
|
const valueElement = document.getElementById(`cpu-core-${index}-value`);
|
||||||
|
const fillElement = document.getElementById(`cpu-core-${index}-fill`);
|
||||||
|
|
||||||
|
if (valueElement && fillElement) {
|
||||||
|
valueElement.textContent = `${load}%`;
|
||||||
|
fillElement.style.width = `${load}%`;
|
||||||
|
fillElement.style.backgroundColor = getColorByPercentage(load);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAM-Anzeige aktualisieren
|
||||||
|
const memPercentage = data.memory.usedPercent;
|
||||||
|
document.getElementById('memory-percentage').textContent = `${memPercentage}%`;
|
||||||
|
document.getElementById('memory-gauge').style.height = `${memPercentage}%`;
|
||||||
|
document.getElementById('memory-gauge').style.backgroundColor = getColorByPercentage(memPercentage);
|
||||||
|
|
||||||
|
document.getElementById('memory-details').innerHTML = `
|
||||||
|
<p><strong>Gesamt:</strong> ${formatBytes(data.memory.total)}</p>
|
||||||
|
<p><strong>Verwendet:</strong> ${formatBytes(data.memory.used)}</p>
|
||||||
|
<p><strong>Frei:</strong> ${formatBytes(data.memory.free)}</p>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Netzwerkstatistiken
|
||||||
|
let networkChart;
|
||||||
|
socket.on('network-stats', data => {
|
||||||
|
const networkThroughput = document.getElementById('network-throughput');
|
||||||
|
let throughputHTML = '';
|
||||||
|
|
||||||
|
data.stats.forEach(stat => {
|
||||||
|
throughputHTML += `
|
||||||
|
<div class="network-stat">
|
||||||
|
<strong>${stat.iface}:</strong> ↓ ${formatBytes(stat.rx_sec)}/s | ↑ ${formatBytes(stat.tx_sec)}/s
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
networkThroughput.innerHTML = throughputHTML;
|
||||||
|
|
||||||
|
// Aktualisiere oder erstelle Netzwerk-Chart
|
||||||
|
updateNetworkChart(data.stats);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Netzwerk-Chart initialisieren oder aktualisieren
|
||||||
|
function updateNetworkChart(stats) {
|
||||||
|
const ctx = document.getElementById('network-chart').getContext('2d');
|
||||||
|
|
||||||
|
if (!networkChart) {
|
||||||
|
// Chart initialisieren, wenn er noch nicht existiert
|
||||||
|
const labels = [];
|
||||||
|
const rxData = [];
|
||||||
|
const txData = [];
|
||||||
|
|
||||||
|
// Fülle anfängliche Daten
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
labels.push('');
|
||||||
|
rxData.push(0);
|
||||||
|
txData.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
networkChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Empfangen (Bytes/s)',
|
||||||
|
data: rxData,
|
||||||
|
borderColor: '#3f51b5',
|
||||||
|
backgroundColor: 'rgba(63, 81, 181, 0.1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
tension: 0.2,
|
||||||
|
fill: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gesendet (Bytes/s)',
|
||||||
|
data: txData,
|
||||||
|
borderColor: '#f44336',
|
||||||
|
backgroundColor: 'rgba(244, 67, 54, 0.1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
tension: 0.2,
|
||||||
|
fill: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return formatBytes(value, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
duration: 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Chart aktualisieren
|
||||||
|
let rxTotal = 0;
|
||||||
|
let txTotal = 0;
|
||||||
|
|
||||||
|
stats.forEach(stat => {
|
||||||
|
rxTotal += stat.rx_sec || 0;
|
||||||
|
txTotal += stat.tx_sec || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Füge neue Daten hinzu und entferne alte
|
||||||
|
networkChart.data.labels.push('');
|
||||||
|
networkChart.data.labels.shift();
|
||||||
|
|
||||||
|
networkChart.data.datasets[0].data.push(rxTotal);
|
||||||
|
networkChart.data.datasets[0].data.shift();
|
||||||
|
|
||||||
|
networkChart.data.datasets[1].data.push(txTotal);
|
||||||
|
networkChart.data.datasets[1].data.shift();
|
||||||
|
|
||||||
|
networkChart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes, decimals = 2) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisierung
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Zeit aktualisieren
|
||||||
|
setInterval(() => {
|
||||||
|
document.getElementById('current-time').innerHTML = `<strong>Zeitstempel:</strong> ${new Date().toLocaleString('de-DE')}`;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Lade Systemdaten, wenn das System-Panel aktiv ist
|
||||||
|
if (document.getElementById('system').classList.contains('active')) {
|
||||||
|
loadSystemInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lade Script für Netzwerk-Chart, falls noch nicht geladen
|
||||||
|
if (typeof Chart === 'undefined') {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
|
||||||
|
script.onload = () => {
|
||||||
|
console.log('Chart.js geladen');
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
});
|
227
frontend/debug-server/public/views/index.ejs
Normal file
227
frontend/debug-server/public/views/index.ejs
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %></title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
h1, h2, h3 {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
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-primary {
|
||||||
|
background-color: #2ecc71;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.interface-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.interface-item {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1><%= title %></h1>
|
||||||
|
<p>Letzte Aktualisierung: <%= lastCheck %></p>
|
||||||
|
|
||||||
|
<div id="message" class="message"></div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Backend-Konfiguration</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="backendUrl">Backend-URL:</label>
|
||||||
|
<input type="text" id="backendUrl" value="<%= backendUrl %>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn" onclick="testBackend()">Verbindung testen</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="updateBackend()">URL aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Backend-Status</h2>
|
||||||
|
<div class="status <%= backendStatus === 'Verbunden' ? 'status-good' : 'status-error' %>">
|
||||||
|
<strong>Status:</strong> <%= backendStatus %>
|
||||||
|
</div>
|
||||||
|
<div class="status <%= pingStatus ? 'status-good' : 'status-error' %>">
|
||||||
|
<strong>Ping:</strong> <%= pingStatus ? 'Erfolgreich' : 'Fehlgeschlagen' %>
|
||||||
|
</div>
|
||||||
|
<div class="status">
|
||||||
|
<strong>Host:</strong> <%= backendHost %>
|
||||||
|
</div>
|
||||||
|
<div class="status">
|
||||||
|
<strong>Port:</strong> <%= backendPort %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Netzwerkschnittstellen</h2>
|
||||||
|
<ul class="interface-list">
|
||||||
|
<% if (interfaces && interfaces.length > 0) { %>
|
||||||
|
<% interfaces.forEach(function(iface) { %>
|
||||||
|
<li class="interface-item">
|
||||||
|
<strong><%= iface.name %>:</strong> <%= iface.address %>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
<% } else { %>
|
||||||
|
<li class="interface-item">Keine Netzwerkschnittstellen gefunden</li>
|
||||||
|
<% } %>
|
||||||
|
</ul>
|
||||||
|
</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 testBackend() {
|
||||||
|
const backendUrl = document.getElementById('backendUrl').value;
|
||||||
|
|
||||||
|
if (!backendUrl) {
|
||||||
|
showMessage('Bitte geben Sie eine Backend-URL ein', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/test-backend', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ backendUrl })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const pingStatus = data.ping ? 'Erfolgreich' : 'Fehlgeschlagen';
|
||||||
|
const connectionStatus = data.connection ? 'Verbunden' : 'Keine Verbindung';
|
||||||
|
|
||||||
|
showMessage(`Ping: ${pingStatus}, API-Verbindung: ${connectionStatus}`, !(data.ping && data.connection));
|
||||||
|
} else {
|
||||||
|
showMessage(data.message, true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showMessage(`Fehler: ${error}`, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBackend() {
|
||||||
|
const backendUrl = document.getElementById('backendUrl').value;
|
||||||
|
|
||||||
|
if (!backendUrl) {
|
||||||
|
showMessage('Bitte geben Sie eine Backend-URL ein', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/update-backend', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ backendUrl })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
showMessage(data.message, !data.success);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Lade die Seite neu nach erfolgreicher Aktualisierung
|
||||||
|
setTimeout(() => {
|
||||||
|
location.reload();
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showMessage(`Fehler: ${error}`, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
235
frontend/debug-server/src/app.js
Normal file
235
frontend/debug-server/src/app.js
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const axios = require('axios');
|
||||||
|
const os = require('os');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 8081;
|
||||||
|
|
||||||
|
// Konfigurationsdatei
|
||||||
|
const CONFIG_FILE = path.join(__dirname, '../../../.env.local');
|
||||||
|
const DEFAULT_BACKEND_URL = 'http://192.168.0.105:5000';
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(express.static(path.join(__dirname, '../public')));
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, '../public/views'));
|
||||||
|
|
||||||
|
// Hilfsfunktionen
|
||||||
|
function getBackendUrl() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||||
|
const match = content.match(/NEXT_PUBLIC_API_URL=(.+)/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Lesen der Backend-URL:', error);
|
||||||
|
}
|
||||||
|
return DEFAULT_BACKEND_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBackendUrl(url) {
|
||||||
|
try {
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||||
|
// Ersetze die URL, wenn sie bereits existiert
|
||||||
|
if (content.match(/NEXT_PUBLIC_API_URL=.+/)) {
|
||||||
|
content = content.replace(/NEXT_PUBLIC_API_URL=.+/, `NEXT_PUBLIC_API_URL=${url}`);
|
||||||
|
} else {
|
||||||
|
// Füge die URL hinzu, wenn sie nicht existiert
|
||||||
|
content += `\nNEXT_PUBLIC_API_URL=${url}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Erstelle eine neue Datei mit der URL
|
||||||
|
content = `NEXT_PUBLIC_API_URL=${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(CONFIG_FILE, content, 'utf8');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Speichern der Backend-URL:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNetworkInterfaces() {
|
||||||
|
const interfaces = [];
|
||||||
|
const networkInterfaces = os.networkInterfaces();
|
||||||
|
|
||||||
|
for (const [name, netInterface] of Object.entries(networkInterfaces)) {
|
||||||
|
if (name.startsWith('lo') || name.startsWith('docker') || name.startsWith('br-')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const iface of netInterface) {
|
||||||
|
if (iface.family === 'IPv4' || iface.family === 4) {
|
||||||
|
interfaces.push({
|
||||||
|
name: name,
|
||||||
|
address: iface.address
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pingHost(host) {
|
||||||
|
try {
|
||||||
|
const platform = process.platform;
|
||||||
|
const cmd = platform === 'win32' ?
|
||||||
|
`ping -n 1 -w 1000 ${host}` :
|
||||||
|
`ping -c 1 -W 1 ${host}`;
|
||||||
|
|
||||||
|
execSync(cmd, { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testBackendConnection(url) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${url}/api/test`, { timeout: 3000 });
|
||||||
|
return response.status === 200;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routen
|
||||||
|
app.get('/', async (req, res) => {
|
||||||
|
const backendUrl = getBackendUrl();
|
||||||
|
const interfaces = getNetworkInterfaces();
|
||||||
|
|
||||||
|
// Analysiere die URL, um Host und Port zu extrahieren
|
||||||
|
let backendHost = '';
|
||||||
|
let backendPort = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(backendUrl);
|
||||||
|
backendHost = url.hostname;
|
||||||
|
backendPort = url.port || (url.protocol === 'https:' ? '443' : '80');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ungültige Backend-URL:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe Backend-Verbindung
|
||||||
|
let backendStatus = 'Unbekannt';
|
||||||
|
let pingStatus = false;
|
||||||
|
|
||||||
|
if (backendHost) {
|
||||||
|
pingStatus = pingHost(backendHost);
|
||||||
|
|
||||||
|
if (pingStatus) {
|
||||||
|
try {
|
||||||
|
const connectionStatus = await testBackendConnection(backendUrl);
|
||||||
|
backendStatus = connectionStatus ? 'Verbunden' : 'Keine API-Verbindung';
|
||||||
|
} catch (error) {
|
||||||
|
backendStatus = 'Verbindungsfehler';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
backendStatus = 'Nicht erreichbar';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('index', {
|
||||||
|
title: 'MYP Frontend Debug',
|
||||||
|
backendUrl,
|
||||||
|
backendHost,
|
||||||
|
backendPort,
|
||||||
|
backendStatus,
|
||||||
|
pingStatus,
|
||||||
|
interfaces,
|
||||||
|
lastCheck: new Date().toLocaleString('de-DE')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/update-backend', async (req, res) => {
|
||||||
|
const { backendUrl } = req.body;
|
||||||
|
|
||||||
|
if (!backendUrl) {
|
||||||
|
return res.json({ success: false, message: 'Keine Backend-URL angegeben' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validiere URL
|
||||||
|
try {
|
||||||
|
new URL(backendUrl);
|
||||||
|
} catch (error) {
|
||||||
|
return res.json({ success: false, message: 'Ungültige URL' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichere die URL
|
||||||
|
const saved = setBackendUrl(backendUrl);
|
||||||
|
|
||||||
|
if (!saved) {
|
||||||
|
return res.json({ success: false, message: 'Fehler beim Speichern der URL' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teste die Verbindung zum Backend
|
||||||
|
let connectionStatus = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
connectionStatus = await testBackendConnection(backendUrl);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignoriere Fehler
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Backend-URL erfolgreich aktualisiert',
|
||||||
|
connection: connectionStatus
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/test-backend', async (req, res) => {
|
||||||
|
const { backendUrl } = req.body;
|
||||||
|
|
||||||
|
if (!backendUrl) {
|
||||||
|
return res.json({ success: false, message: 'Keine Backend-URL angegeben' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validiere URL
|
||||||
|
let hostname = '';
|
||||||
|
try {
|
||||||
|
const url = new URL(backendUrl);
|
||||||
|
hostname = url.hostname;
|
||||||
|
} catch (error) {
|
||||||
|
return res.json({ success: false, message: 'Ungültige URL' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teste Ping
|
||||||
|
const pingStatus = pingHost(hostname);
|
||||||
|
|
||||||
|
// Teste API-Verbindung
|
||||||
|
let connectionStatus = false;
|
||||||
|
|
||||||
|
if (pingStatus) {
|
||||||
|
try {
|
||||||
|
connectionStatus = await testBackendConnection(backendUrl);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignoriere Fehler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
ping: pingStatus,
|
||||||
|
connection: connectionStatus
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Server starten
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`MYP Frontend Debug Server läuft auf Port ${PORT}`);
|
||||||
|
});
|
471
frontend/debug-server/src/index.js
Normal file
471
frontend/debug-server/src/index.js
Normal file
@ -0,0 +1,471 @@
|
|||||||
|
// Frontend Debug-Server für MYP
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const http = require('http');
|
||||||
|
const si = require('systeminformation');
|
||||||
|
const os = require('os');
|
||||||
|
const osUtils = require('os-utils');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
const socketIo = require('socket.io');
|
||||||
|
|
||||||
|
// Konfiguration
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const io = socketIo(server);
|
||||||
|
const PORT = 6666;
|
||||||
|
const FRONTEND_PORT = 3000;
|
||||||
|
const FRONTEND_HOST = 'localhost';
|
||||||
|
const BACKEND_HOST = 'localhost';
|
||||||
|
const BACKEND_PORT = 5000;
|
||||||
|
|
||||||
|
// View Engine einrichten
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, '../public/views'));
|
||||||
|
app.use(express.static(path.join(__dirname, '../public')));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Hauptseite rendern
|
||||||
|
app.get('/', async (req, res) => {
|
||||||
|
const hostname = os.hostname();
|
||||||
|
const networkInterfaces = os.networkInterfaces();
|
||||||
|
const ipAddresses = {};
|
||||||
|
|
||||||
|
// IP-Adressen sammeln
|
||||||
|
Object.keys(networkInterfaces).forEach(interfaceName => {
|
||||||
|
const interfaceInfo = networkInterfaces[interfaceName];
|
||||||
|
const ipv4Addresses = interfaceInfo.filter(info => info.family === 'IPv4');
|
||||||
|
if (ipv4Addresses.length > 0) {
|
||||||
|
ipAddresses[interfaceName] = ipv4Addresses[0].address;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rendere die Hauptseite mit Basisdaten
|
||||||
|
res.render('index', {
|
||||||
|
hostname: hostname,
|
||||||
|
ipAddresses: ipAddresses,
|
||||||
|
timestamp: new Date().toLocaleString('de-DE'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// API-Endpunkte
|
||||||
|
|
||||||
|
// Systeminformationen
|
||||||
|
app.get('/api/system', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [cpu, mem, osInfo, diskLayout, fsSize] = await Promise.all([
|
||||||
|
si.cpu(),
|
||||||
|
si.mem(),
|
||||||
|
si.osInfo(),
|
||||||
|
si.diskLayout(),
|
||||||
|
si.fsSize()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
cpu: {
|
||||||
|
manufacturer: cpu.manufacturer,
|
||||||
|
brand: cpu.brand,
|
||||||
|
speed: cpu.speed,
|
||||||
|
cores: cpu.cores,
|
||||||
|
physicalCores: cpu.physicalCores
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
total: formatBytes(mem.total),
|
||||||
|
free: formatBytes(mem.free),
|
||||||
|
used: formatBytes(mem.used),
|
||||||
|
usedPercent: Math.round(mem.used / mem.total * 100)
|
||||||
|
},
|
||||||
|
os: {
|
||||||
|
platform: osInfo.platform,
|
||||||
|
distro: osInfo.distro,
|
||||||
|
release: osInfo.release,
|
||||||
|
arch: osInfo.arch,
|
||||||
|
uptime: formatUptime(os.uptime())
|
||||||
|
},
|
||||||
|
filesystem: fsSize.map(fs => ({
|
||||||
|
fs: fs.fs,
|
||||||
|
type: fs.type,
|
||||||
|
size: formatBytes(fs.size),
|
||||||
|
used: formatBytes(fs.used),
|
||||||
|
available: formatBytes(fs.available),
|
||||||
|
mount: fs.mount,
|
||||||
|
usePercent: Math.round(fs.use)
|
||||||
|
})),
|
||||||
|
disks: diskLayout.map(disk => ({
|
||||||
|
device: disk.device,
|
||||||
|
type: disk.type,
|
||||||
|
name: disk.name,
|
||||||
|
size: formatBytes(disk.size)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der Systemdaten:', error);
|
||||||
|
res.status(500).json({ error: 'Fehler beim Abrufen der Systemdaten' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Netzwerkinformationen
|
||||||
|
app.get('/api/network', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [netInterfaces, netStats] = await Promise.all([
|
||||||
|
si.networkInterfaces(),
|
||||||
|
si.networkStats()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dns = await getDnsServers();
|
||||||
|
const gateway = await getDefaultGateway();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
interfaces: netInterfaces.map(iface => ({
|
||||||
|
iface: iface.iface,
|
||||||
|
ip4: iface.ip4,
|
||||||
|
ip6: iface.ip6,
|
||||||
|
mac: iface.mac,
|
||||||
|
internal: iface.internal,
|
||||||
|
operstate: iface.operstate,
|
||||||
|
type: iface.type,
|
||||||
|
speed: iface.speed,
|
||||||
|
dhcp: iface.dhcp
|
||||||
|
})),
|
||||||
|
stats: netStats.map(stat => ({
|
||||||
|
iface: stat.iface,
|
||||||
|
rx_bytes: formatBytes(stat.rx_bytes),
|
||||||
|
tx_bytes: formatBytes(stat.tx_bytes),
|
||||||
|
rx_sec: formatBytes(stat.rx_sec),
|
||||||
|
tx_sec: formatBytes(stat.tx_sec)
|
||||||
|
})),
|
||||||
|
dns: dns,
|
||||||
|
gateway: gateway
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der Netzwerkdaten:', error);
|
||||||
|
res.status(500).json({ error: 'Fehler beim Abrufen der Netzwerkdaten' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dienststatus
|
||||||
|
app.get('/api/services', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Prüfen ob Frontend (Next.js) läuft
|
||||||
|
const frontendStatus = await checkServiceStatus(FRONTEND_HOST, FRONTEND_PORT);
|
||||||
|
|
||||||
|
// Prüfen ob Backend (Flask) läuft
|
||||||
|
const backendStatus = await checkServiceStatus(BACKEND_HOST, BACKEND_PORT);
|
||||||
|
|
||||||
|
// Docker-Container Status abrufen
|
||||||
|
const containers = await getDockerContainers();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
frontend: {
|
||||||
|
name: 'Next.js Frontend',
|
||||||
|
status: frontendStatus ? 'online' : 'offline',
|
||||||
|
port: FRONTEND_PORT,
|
||||||
|
host: FRONTEND_HOST
|
||||||
|
},
|
||||||
|
backend: {
|
||||||
|
name: 'Flask Backend',
|
||||||
|
status: backendStatus ? 'online' : 'offline',
|
||||||
|
port: BACKEND_PORT,
|
||||||
|
host: BACKEND_HOST
|
||||||
|
},
|
||||||
|
docker: {
|
||||||
|
containers: containers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der Dienststatus:', error);
|
||||||
|
res.status(500).json({ error: 'Fehler beim Abrufen der Dienststatus' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ping-Endpunkt für Netzwerkdiagnose
|
||||||
|
app.get('/api/ping/:host', (req, res) => {
|
||||||
|
const host = req.params.host;
|
||||||
|
|
||||||
|
// Sicherheitscheck für den Hostnamen
|
||||||
|
if (!isValidHostname(host)) {
|
||||||
|
return res.status(400).json({ error: 'Ungültiger Hostname oder IP-Adresse' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping-Befehl ausführen
|
||||||
|
exec(`ping -n 4 ${host}`, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
output: stderr || stdout,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
output: stdout
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Traceroute-Endpunkt für Netzwerkdiagnose
|
||||||
|
app.get('/api/traceroute/:host', (req, res) => {
|
||||||
|
const host = req.params.host;
|
||||||
|
|
||||||
|
// Sicherheitscheck für den Hostnamen
|
||||||
|
if (!isValidHostname(host)) {
|
||||||
|
return res.status(400).json({ error: 'Ungültiger Hostname oder IP-Adresse' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traceroute-Befehl ausführen (Windows: tracert, Unix: traceroute)
|
||||||
|
const command = process.platform === 'win32' ? 'tracert' : 'traceroute';
|
||||||
|
exec(`${command} ${host}`, (error, stdout, stderr) => {
|
||||||
|
// Traceroute kann einen Nicht-Null-Exit-Code zurückgeben, selbst wenn es teilweise erfolgreich ist
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
output: stdout,
|
||||||
|
error: stderr
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// DNS-Lookup-Endpunkt für Netzwerkdiagnose
|
||||||
|
app.get('/api/nslookup/:host', (req, res) => {
|
||||||
|
const host = req.params.host;
|
||||||
|
|
||||||
|
// Sicherheitscheck für den Hostnamen
|
||||||
|
if (!isValidHostname(host)) {
|
||||||
|
return res.status(400).json({ error: 'Ungültiger Hostname oder IP-Adresse' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// NSLookup-Befehl ausführen
|
||||||
|
exec(`nslookup ${host}`, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
output: stderr || stdout,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
output: stdout
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Echtzeit-Updates über WebSockets
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
console.log('Neue WebSocket-Verbindung');
|
||||||
|
|
||||||
|
// CPU- und Arbeitsspeichernutzung im Intervall senden
|
||||||
|
const systemMonitorInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const [cpu, mem] = await Promise.all([
|
||||||
|
si.currentLoad(),
|
||||||
|
si.mem()
|
||||||
|
]);
|
||||||
|
|
||||||
|
socket.emit('system-stats', {
|
||||||
|
cpu: {
|
||||||
|
load: Math.round(cpu.currentLoad),
|
||||||
|
cores: cpu.cpus.map(core => Math.round(core.load))
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
total: mem.total,
|
||||||
|
used: mem.used,
|
||||||
|
free: mem.free,
|
||||||
|
usedPercent: Math.round(mem.used / mem.total * 100)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Senden der Systemstatistiken:', error);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// Netzwerkstatistiken im Intervall senden
|
||||||
|
const networkMonitorInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const netStats = await si.networkStats();
|
||||||
|
|
||||||
|
socket.emit('network-stats', {
|
||||||
|
stats: netStats.map(stat => ({
|
||||||
|
iface: stat.iface,
|
||||||
|
rx_bytes: stat.rx_bytes,
|
||||||
|
tx_bytes: stat.tx_bytes,
|
||||||
|
rx_sec: stat.rx_sec,
|
||||||
|
tx_sec: stat.tx_sec
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Senden der Netzwerkstatistiken:', error);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// Aufräumen, wenn die Verbindung getrennt wird
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('WebSocket-Verbindung getrennt');
|
||||||
|
clearInterval(systemMonitorInterval);
|
||||||
|
clearInterval(networkMonitorInterval);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hilfsfunktionen
|
||||||
|
|
||||||
|
// Bytes in lesbare Größen formatieren
|
||||||
|
function formatBytes(bytes, decimals = 2) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uptime in lesbare Zeit formatieren
|
||||||
|
function formatUptime(seconds) {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
|
||||||
|
return `${days} Tage, ${hours} Stunden, ${minutes} Minuten, ${secs} Sekunden`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service-Status überprüfen
|
||||||
|
async function checkServiceStatus(host, port) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const socket = new (require('net').Socket)();
|
||||||
|
|
||||||
|
socket.setTimeout(1000);
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('timeout', () => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', () => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.connect(port, host);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker-Container abfragen
|
||||||
|
async function getDockerContainers() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
exec('docker ps --format "{{.ID}},{{.Image}},{{.Status}},{{.Ports}},{{.Names}}"', (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
resolve([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containers = [];
|
||||||
|
const lines = stdout.trim().split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line) {
|
||||||
|
const [id, image, status, ports, name] = line.split(',');
|
||||||
|
containers.push({ id, image, status, ports, name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(containers);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS-Server abfragen
|
||||||
|
async function getDnsServers() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Windows: DNS-Server über PowerShell abfragen
|
||||||
|
exec('powershell.exe -Command "Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object -ExpandProperty ServerAddresses"', (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
resolve(['DNS-Server konnten nicht ermittelt werden']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers = stdout.trim().split('\r\n').filter(Boolean);
|
||||||
|
resolve(servers);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Unix: DNS-Server aus /etc/resolv.conf lesen
|
||||||
|
exec('cat /etc/resolv.conf | grep nameserver | cut -d " " -f 2', (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
resolve(['DNS-Server konnten nicht ermittelt werden']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers = stdout.trim().split('\n').filter(Boolean);
|
||||||
|
resolve(servers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard-Gateway abfragen
|
||||||
|
async function getDefaultGateway() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Windows: Gateway über PowerShell abfragen
|
||||||
|
exec('powershell.exe -Command "Get-NetRoute -DestinationPrefix 0.0.0.0/0 | Select-Object -ExpandProperty NextHop"', (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
resolve('Gateway konnte nicht ermittelt werden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(stdout.trim());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Unix: Gateway aus den Routentabellen lesen
|
||||||
|
exec("ip route | grep default | awk '{print $3}'", (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
resolve('Gateway konnte nicht ermittelt werden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(stdout.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validierung des Hostnamens für Sicherheit
|
||||||
|
function isValidHostname(hostname) {
|
||||||
|
// Längenprüfung
|
||||||
|
if (!hostname || hostname.length > 255) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erlaubte Hostnamen
|
||||||
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv4-Prüfung
|
||||||
|
const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
|
if (ipv4Regex.test(hostname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname-Prüfung
|
||||||
|
const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||||
|
return hostnameRegex.test(hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server starten
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`MYP Frontend Debug-Server läuft auf http://localhost:${PORT}`);
|
||||||
|
});
|
51
frontend/docker-compose.yml
Normal file
51
frontend/docker-compose.yml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Next.js Frontend
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: myp-rp
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- NEXT_PUBLIC_API_URL=https://m040tbaraspi001.de040.corpintra.net/api
|
||||||
|
networks:
|
||||||
|
- myp-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Caddy Proxy
|
||||||
|
caddy:
|
||||||
|
image: caddy:2.7-alpine
|
||||||
|
container_name: myp-caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
networks:
|
||||||
|
- myp-network
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
environment:
|
||||||
|
- CADDY_HOST=53.37.211.254
|
||||||
|
- CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
|
||||||
|
networks:
|
||||||
|
myp-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
|
caddy_config:
|
31
frontend/docker/build.sh
Normal file
31
frontend/docker/build.sh
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Define image name
|
||||||
|
MYP_RP_IMAGE_NAME="myp-rp"
|
||||||
|
|
||||||
|
# Function to build Docker image
|
||||||
|
build_image() {
|
||||||
|
local image_name=$1
|
||||||
|
local dockerfile=$2
|
||||||
|
local platform=$3
|
||||||
|
|
||||||
|
echo "Building $image_name Docker image for $platform..."
|
||||||
|
|
||||||
|
docker buildx build --platform $platform -t ${image_name}:latest -f $dockerfile --load .
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "$image_name Docker image built successfully"
|
||||||
|
else
|
||||||
|
echo "Error occurred while building $image_name Docker image"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create and use a builder instance (if not already created)
|
||||||
|
BUILDER_NAME="myp-rp-arm64-builder"
|
||||||
|
docker buildx create --name $BUILDER_NAME --use || docker buildx use $BUILDER_NAME
|
||||||
|
|
||||||
|
# Build myp-rp image
|
||||||
|
build_image "$MYP_RP_IMAGE_NAME" "$PWD/Dockerfile" "linux/arm64"
|
||||||
|
|
||||||
|
# Remove the builder instance
|
||||||
|
docker buildx rm $BUILDER_NAME
|
52
frontend/docker/caddy/Caddyfile
Normal file
52
frontend/docker/caddy/Caddyfile
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hauptdomain und IP-Adresse für die Anwendung
|
||||||
|
53.37.211.254, m040tbaraspi001.de040.corpintra.net, m040tbaraspi001, de040.corpintra.net, localhost {
|
||||||
|
# API Anfragen zum Backend weiterleiten
|
||||||
|
@api {
|
||||||
|
path /api/* /health
|
||||||
|
}
|
||||||
|
handle @api {
|
||||||
|
uri strip_prefix /api
|
||||||
|
reverse_proxy 192.168.0.5:5000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alle anderen Anfragen zum Frontend weiterleiten
|
||||||
|
handle {
|
||||||
|
reverse_proxy myp-rp:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
tls internal {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
|
||||||
|
# Erlaube HTTP -> HTTPS Redirects für OAuth
|
||||||
|
@oauth path /auth/login/callback*
|
||||||
|
handle @oauth {
|
||||||
|
header Cache-Control "no-cache"
|
||||||
|
reverse_proxy myp-rp:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Allgemeine Header für Sicherheit und Caching
|
||||||
|
header {
|
||||||
|
# Sicherheitsheader
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
# Cache-Control für statische Assets
|
||||||
|
@static {
|
||||||
|
path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
|
||||||
|
}
|
||||||
|
header @static Cache-Control "public, max-age=86400"
|
||||||
|
|
||||||
|
# Keine Caches für dynamische Inhalte
|
||||||
|
@dynamic {
|
||||||
|
not path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
|
||||||
|
}
|
||||||
|
header @dynamic Cache-Control "no-store, no-cache, must-revalidate"
|
||||||
|
}
|
||||||
|
}
|
30
frontend/docker/compose.yml
Normal file
30
frontend/docker/compose.yml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
image: caddy:2.8
|
||||||
|
container_name: caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
volumes:
|
||||||
|
- ./caddy/data:/data
|
||||||
|
- ./caddy/config:/config
|
||||||
|
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
myp-rp:
|
||||||
|
image: myp-rp:latest
|
||||||
|
container_name: myp-rp
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
|
||||||
|
- OAUTH_CLIENT_ID=client_id
|
||||||
|
- OAUTH_CLIENT_SECRET=client_secret
|
||||||
|
env_file: "/srv/myp-env/github.env"
|
||||||
|
volumes:
|
||||||
|
- /srv/MYP-DB:/usr/src/app/db
|
||||||
|
restart: unless-stopped
|
||||||
|
# Füge Healthcheck hinzu für besseres Monitoring
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:3000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
36
frontend/docker/deploy.sh
Normal file
36
frontend/docker/deploy.sh
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Directory containing the Docker images
|
||||||
|
IMAGE_DIR="docker/images"
|
||||||
|
|
||||||
|
# Load all Docker images from the tar.xz files in the IMAGE_DIR
|
||||||
|
echo "Loading Docker images from $IMAGE_DIR..."
|
||||||
|
|
||||||
|
for image_file in "$IMAGE_DIR"/*.tar.xz; do
|
||||||
|
if [ -f "$image_file" ]; then
|
||||||
|
echo "Loading Docker image from $image_file..."
|
||||||
|
docker load -i "$image_file"
|
||||||
|
|
||||||
|
# Check if the image loading was successful
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error occurred while loading Docker image from $image_file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No Docker image tar.xz files found in $IMAGE_DIR."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Execute docker compose
|
||||||
|
echo "Running docker compose..."
|
||||||
|
docker compose -f "docker/compose.yml" up -d
|
||||||
|
|
||||||
|
# Check if the operation was successful
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Docker compose executed successfully"
|
||||||
|
else
|
||||||
|
echo "Error occurred while executing docker compose"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deployment completed successfully"
|
2
frontend/docker/images/.gitattributes
vendored
Normal file
2
frontend/docker/images/.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
caddy_2.8.tar.xz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
myp-rp_latest.tar.xz filter=lfs diff=lfs merge=lfs -text
|
68
frontend/docker/save.sh
Normal file
68
frontend/docker/save.sh
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get image name as argument
|
||||||
|
IMAGE_NAME=$1
|
||||||
|
PLATFORM="linux/arm64"
|
||||||
|
|
||||||
|
# Define paths
|
||||||
|
IMAGE_DIR="docker/images"
|
||||||
|
IMAGE_FILE="${IMAGE_DIR}/${IMAGE_NAME//[:\/]/_}.tar"
|
||||||
|
COMPRESSED_FILE="${IMAGE_FILE}.xz"
|
||||||
|
|
||||||
|
# Function to pull the image
|
||||||
|
pull_image() {
|
||||||
|
local image=$1
|
||||||
|
if [[ $image == arm64v8/* ]]; then
|
||||||
|
echo "Pulling image $image without platform specification..."
|
||||||
|
docker pull $image
|
||||||
|
else
|
||||||
|
echo "Pulling image $image for platform $PLATFORM..."
|
||||||
|
docker pull --platform $PLATFORM $image
|
||||||
|
fi
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pull the image if it is not available locally
|
||||||
|
if ! docker image inspect ${IMAGE_NAME} &>/dev/null; then
|
||||||
|
if pull_image ${IMAGE_NAME}; then
|
||||||
|
echo "Image $IMAGE_NAME pulled successfully."
|
||||||
|
else
|
||||||
|
echo "Error occurred while pulling $IMAGE_NAME for platform $PLATFORM"
|
||||||
|
echo "Trying to pull $IMAGE_NAME without platform specification..."
|
||||||
|
|
||||||
|
# Attempt to pull again without platform
|
||||||
|
if pull_image ${IMAGE_NAME}; then
|
||||||
|
echo "Image $IMAGE_NAME pulled successfully without platform."
|
||||||
|
else
|
||||||
|
echo "Error occurred while pulling $IMAGE_NAME without platform."
|
||||||
|
echo "Trying to pull arm64v8/${IMAGE_NAME} instead..."
|
||||||
|
|
||||||
|
# Construct new image name
|
||||||
|
NEW_IMAGE_NAME="arm64v8/${IMAGE_NAME}"
|
||||||
|
if pull_image ${NEW_IMAGE_NAME}; then
|
||||||
|
echo "Image $NEW_IMAGE_NAME pulled successfully."
|
||||||
|
IMAGE_NAME=${NEW_IMAGE_NAME} # Update IMAGE_NAME to use the new one
|
||||||
|
else
|
||||||
|
echo "Error occurred while pulling $NEW_IMAGE_NAME"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Image $IMAGE_NAME found locally. Skipping pull."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save the Docker image
|
||||||
|
echo "Saving $IMAGE_NAME Docker image..."
|
||||||
|
docker save ${IMAGE_NAME} > $IMAGE_FILE
|
||||||
|
|
||||||
|
# Compress the Docker image (overwriting if file exists)
|
||||||
|
echo "Compressing $IMAGE_FILE..."
|
||||||
|
xz -z --force $IMAGE_FILE
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "$IMAGE_NAME Docker image saved and compressed successfully as $COMPRESSED_FILE"
|
||||||
|
else
|
||||||
|
echo "Error occurred while compressing $IMAGE_NAME Docker image"
|
||||||
|
exit 1
|
||||||
|
fi
|
116
frontend/docs/Admin-Dashboard.md
Normal file
116
frontend/docs/Admin-Dashboard.md
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# **Detaillierte Dokumentation des Admin-Dashboards**
|
||||||
|
|
||||||
|
In diesem Abschnitt werde ich die Funktionen und Nutzung des Admin-Dashboards genauer beschreiben, einschließlich der verschiedenen Module, Diagramme und deren Zweck.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **1. Überblick über das Admin-Dashboard**
|
||||||
|
|
||||||
|
Das Admin-Dashboard ist der zentrale Verwaltungsbereich für Administratoren. Es bietet Funktionen wie die Verwaltung von Druckern, Benutzern und Druckaufträgen sowie detaillierte Statistiken und Analysen.
|
||||||
|
|
||||||
|
### **1.1. Navigation**
|
||||||
|
Das Dashboard enthält ein Sidebar-Menü mit den folgenden Hauptbereichen:
|
||||||
|
1. **Dashboard:** Übersicht der wichtigsten Statistiken.
|
||||||
|
2. **Benutzer:** Verwaltung von Benutzerkonten.
|
||||||
|
3. **Drucker:** Hinzufügen, Bearbeiten und Verwalten von Druckern.
|
||||||
|
4. **Druckaufträge:** Einsicht in alle Druckaufträge und deren Status.
|
||||||
|
5. **Einstellungen:** Konfiguration der Anwendung.
|
||||||
|
6. **Über MYP:** Informationen über das Projekt und den Entwickler.
|
||||||
|
|
||||||
|
Die Sidebar wird in der Datei `src/app/admin/admin-sidebar.tsx` definiert und dynamisch basierend auf der aktuellen Seite hervorgehoben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **2. Funktionen des Admin-Dashboards**
|
||||||
|
|
||||||
|
### **2.1. Benutzerverwaltung**
|
||||||
|
- **Datei:** `src/app/admin/users/page.tsx`
|
||||||
|
- **Beschreibung:** Ermöglicht das Anzeigen, Bearbeiten und Löschen von Benutzerkonten.
|
||||||
|
- **Funktionen:**
|
||||||
|
- Anzeige einer Liste aller registrierten Benutzer.
|
||||||
|
- Bearbeiten von Benutzerrollen (z. B. „admin“ oder „user“).
|
||||||
|
- Deaktivieren oder Löschen von Benutzerkonten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.2. Druckerverwaltung**
|
||||||
|
- **Datei:** `src/app/admin/printers/page.tsx`
|
||||||
|
- **Beschreibung:** Verwaltung der Drucker, einschließlich Hinzufügen, Bearbeiten und Deaktivieren.
|
||||||
|
- **Funktionen:**
|
||||||
|
- Statusanzeige der Drucker (aktiv/inaktiv).
|
||||||
|
- Hinzufügen neuer Drucker mit Namen und Beschreibung.
|
||||||
|
- Löschen oder Bearbeiten bestehender Drucker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.3. Druckaufträge**
|
||||||
|
- **Datei:** `src/app/admin/jobs/page.tsx`
|
||||||
|
- **Beschreibung:** Übersicht aller Druckaufträge, einschließlich Details wie Startzeit, Dauer und Status.
|
||||||
|
- **Funktionen:**
|
||||||
|
- Filtern nach Benutzern, Druckern oder Status (abgeschlossen, abgebrochen).
|
||||||
|
- Anzeigen von Abbruchgründen und Fehlermeldungen.
|
||||||
|
- Sortieren nach Zeit oder Benutzer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.4. Einstellungen**
|
||||||
|
- **Datei:** `src/app/admin/settings/page.tsx`
|
||||||
|
- **Beschreibung:** Konfigurationsseite für die Anwendung.
|
||||||
|
- **Funktionen:**
|
||||||
|
- Ändern von globalen Einstellungen wie Standardzeiten oder Fehlerrichtlinien.
|
||||||
|
- Download von Daten (z. B. Export der Druckhistorie).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **3. Statistiken und Diagramme**
|
||||||
|
|
||||||
|
Das Admin-Dashboard enthält interaktive Diagramme, die wichtige Statistiken visualisieren. Hier einige der zentralen Diagramme:
|
||||||
|
|
||||||
|
### **3.1. Abbruchgründe**
|
||||||
|
- **Datei:** `src/app/admin/charts/printer-error-chart.tsx`
|
||||||
|
- **Beschreibung:** Zeigt die Häufigkeit der Abbruchgründe für Druckaufträge in einem Balkendiagramm.
|
||||||
|
- **Nutzen:** Identifiziert häufige Probleme wie Materialmangel oder Düsenverstopfungen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3.2. Fehlerraten**
|
||||||
|
- **Datei:** `src/app/admin/charts/printer-error-rate.tsx`
|
||||||
|
- **Beschreibung:** Zeigt die prozentuale Fehlerrate für jeden Drucker in einem Balkendiagramm.
|
||||||
|
- **Nutzen:** Ermöglicht die Überwachung und Identifizierung von problematischen Druckern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3.3. Druckvolumen**
|
||||||
|
- **Datei:** `src/app/admin/charts/printer-volume.tsx`
|
||||||
|
- **Beschreibung:** Zeigt das Druckvolumen für heute, diese Woche und diesen Monat.
|
||||||
|
- **Nutzen:** Vergleich des Druckeroutputs über verschiedene Zeiträume.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3.4. Prognostizierte Nutzung**
|
||||||
|
- **Datei:** `src/app/admin/charts/printer-forecast.tsx`
|
||||||
|
- **Beschreibung:** Ein Bereichsdiagramm zeigt die erwartete Druckernutzung pro Wochentag.
|
||||||
|
- **Nutzen:** Hilft bei der Planung von Wartungsarbeiten oder Ressourcenzuweisungen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **3.5. Druckerauslastung**
|
||||||
|
- **Datei:** `src/app/admin/charts/printer-utilization.tsx`
|
||||||
|
- **Beschreibung:** Zeigt die aktuelle Nutzung eines Druckers in Prozent in einem Kreisdiagramm.
|
||||||
|
- **Nutzen:** Überwacht die Auslastung und identifiziert ungenutzte Ressourcen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **4. Rollenbasierte Zugriffssteuerung**
|
||||||
|
|
||||||
|
Das Admin-Dashboard ist nur für Benutzer mit der Rolle „admin“ zugänglich. Nicht berechtigte Benutzer werden auf die Startseite umgeleitet. Die Zugriffssteuerung erfolgt durch folgende Logik:
|
||||||
|
- **Datei:** `src/app/admin/layout.tsx`
|
||||||
|
- **Funktion:** `validateRequest` prüft die Rolle des aktuellen Benutzers.
|
||||||
|
- **Umleitung:** Falls die Rolle unzureichend ist, wird der Benutzer automatisch umgeleitet:
|
||||||
|
```typescript
|
||||||
|
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nächster Schritt: [=> API-Endpunkte und deren Nutzung](./API.md)
|
79
frontend/docs/Architektur.md
Normal file
79
frontend/docs/Architektur.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# **Technische Architektur und Codeaufbau**
|
||||||
|
|
||||||
|
In diesem Abschnitt erläutere ich die Architektur und Struktur des MYP-Projekts sowie die Funktionalitäten der zentralen Komponenten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **1. Technische Architektur**
|
||||||
|
|
||||||
|
### **1.1. Architekturübersicht**
|
||||||
|
MYP basiert auf einer modernen Webanwendungsarchitektur:
|
||||||
|
- **Frontend:** Entwickelt mit React und Next.js. Stellt die Benutzeroberfläche bereit.
|
||||||
|
- **Backend:** Nutzt Node.js und Drizzle ORM für die Datenbankinteraktion und Geschäftslogik.
|
||||||
|
- **Datenbank:** SQLite zur Speicherung von Nutzerdaten, Druckaufträgen und Druckerkonfigurationen.
|
||||||
|
- **Containerisierung:** Docker wird verwendet, um die Anwendung in isolierten Containern bereitzustellen.
|
||||||
|
- **Webserver:** Caddy dient als Reverse Proxy mit HTTPS-Unterstützung.
|
||||||
|
|
||||||
|
### **1.2. Modulübersicht**
|
||||||
|
- **Datenfluss:** Die Anwendung ist stark datengetrieben. API-Routen werden genutzt, um Daten zwischen Frontend und Backend auszutauschen.
|
||||||
|
- **Rollenbasierter Zugriff:** Über ein Berechtigungssystem können Administratoren und Benutzer unterschiedliche Funktionen nutzen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **2. Codeaufbau**
|
||||||
|
|
||||||
|
### **2.1. Ordnerstruktur**
|
||||||
|
Die Datei `repomix-output.txt` zeigt eine strukturierte Übersicht des Projekts. Nachfolgend einige wichtige Verzeichnisse:
|
||||||
|
|
||||||
|
| **Verzeichnis** | **Inhalt** |
|
||||||
|
|--------------------------|---------------------------------------------------------------------------|
|
||||||
|
| `src/app` | Next.js-Seiten und Komponenten für Benutzer und Admins. |
|
||||||
|
| `src/components` | Wiederverwendbare UI-Komponenten wie Karten, Diagramme, Buttons etc. |
|
||||||
|
| `src/server` | Backend-Logik, Authentifizierung und Datenbankinteraktionen. |
|
||||||
|
| `src/utils` | Hilfsfunktionen für Analysen, Validierungen und Datenbankzugriffe. |
|
||||||
|
| `drizzle` | Datenbank-Migrationsdateien und Metadaten. |
|
||||||
|
| `docker` | Docker-Konfigurations- und Bereitstellungsskripte. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.2. Hauptdateien**
|
||||||
|
#### **Frontend**
|
||||||
|
- **`src/app/page.tsx`:** Startseite der Anwendung.
|
||||||
|
- **`src/app/admin/`:** Admin-spezifische Seiten, z. B. Druckerverwaltung oder Fehlerstatistiken.
|
||||||
|
- **`src/components/ui/`:** UI-Komponenten wie Dialoge, Formulare und Tabellen.
|
||||||
|
|
||||||
|
#### **Backend**
|
||||||
|
- **`src/server/auth/`:** Authentifizierung und Benutzerrollenmanagement.
|
||||||
|
- **`src/server/actions/`:** Funktionen zur Interaktion mit Druckaufträgen und Druckern.
|
||||||
|
- **`src/utils/`:** Analyse und Verarbeitung von Druckdaten (z. B. Fehlerquoten und Auslastung).
|
||||||
|
|
||||||
|
#### **Datenbank**
|
||||||
|
- **`drizzle/0000_overjoyed_strong_guy.sql`:** SQLite-Datenbankschema mit Tabellen für Drucker, Benutzer und Druckaufträge.
|
||||||
|
- **`drizzle.meta/`:** Metadaten zur Datenbankmigration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.3. Datenbankschema**
|
||||||
|
Das Schema enthält vier Haupttabellen:
|
||||||
|
1. **`user`:** Speichert Benutzerinformationen, einschließlich Rollen und E-Mail-Adressen.
|
||||||
|
2. **`printer`:** Beschreibt die Drucker, ihren Status und ihre Eigenschaften.
|
||||||
|
3. **`printJob`:** Zeichnet Druckaufträge auf, einschließlich Startzeit, Dauer und Abbruchgrund.
|
||||||
|
4. **`session`:** Verwaltert Benutzer-Sitzungen und Ablaufzeiten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **3. Wichtige Funktionen**
|
||||||
|
|
||||||
|
### **3.1. Authentifizierung**
|
||||||
|
Das System nutzt OAuth zur Anmeldung. Benutzerrollen werden in der Tabelle `user` gespeichert und im Backend überprüft.
|
||||||
|
|
||||||
|
### **3.2. Statistiken**
|
||||||
|
- **Fehlerrate:** Berechnet die Häufigkeit von Abbrüchen für jeden Drucker.
|
||||||
|
- **Auslastung:** Prozentuale Nutzung der Drucker, basierend auf geplanten und abgeschlossenen Druckaufträgen.
|
||||||
|
- **Prognosen:** Verwenden historische Daten, um zukünftige Drucknutzungen vorherzusagen.
|
||||||
|
|
||||||
|
### **3.3. API-Endpunkte**
|
||||||
|
- **`src/app/api/printers/`:** Zugriff auf Druckerkonfigurationsdaten.
|
||||||
|
- **`src/app/api/job/[jobId]/`:** Verwaltung einzelner Druckaufträge.
|
||||||
|
|
||||||
|
Nächster Schritt: [=> Datenbank und Analytik-Funktionen](./Datenbank.md)
|
150
frontend/docs/Bereitstellungsdetails .md
Normal file
150
frontend/docs/Bereitstellungsdetails .md
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# **Bereitstellungsdetails und Best Practices**
|
||||||
|
|
||||||
|
In diesem Abschnitt erläutere ich, wie das MYP-Projekt auf einem Server bereitgestellt wird, sowie empfohlene Praktiken zur Verwaltung und Optimierung des Systems.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **1. Bereitstellungsschritte**
|
||||||
|
|
||||||
|
### **1.1. Voraussetzungen**
|
||||||
|
- **Server:** Raspberry Pi mit installiertem Raspbian Lite.
|
||||||
|
- **Docker:** Docker und Docker Compose müssen vorab installiert sein.
|
||||||
|
- **Netzwerk:** Der Server muss über eine statische IP-Adresse oder einen DNS-Namen erreichbar sein.
|
||||||
|
|
||||||
|
### **1.2. Vorbereitung**
|
||||||
|
#### **1.2.1. Docker-Images erstellen und speichern**
|
||||||
|
Führen Sie die folgenden Schritte auf dem Entwicklungssystem aus:
|
||||||
|
1. **Images erstellen:**
|
||||||
|
```bash
|
||||||
|
bash docker/build.sh
|
||||||
|
```
|
||||||
|
2. **Images exportieren und komprimieren:**
|
||||||
|
```bash
|
||||||
|
bash docker/save.sh <image-name>
|
||||||
|
```
|
||||||
|
Dies speichert die Docker-Images im Verzeichnis `docker/images/`.
|
||||||
|
|
||||||
|
#### **1.2.2. Übertragung auf den Server**
|
||||||
|
Kopieren Sie die erzeugten `.tar.xz`-Dateien auf den Raspberry Pi:
|
||||||
|
```bash
|
||||||
|
scp docker/images/*.tar.xz <username>@<server-ip>:/path/to/destination/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **1.3. Images auf dem Server laden**
|
||||||
|
Loggen Sie sich auf dem Server ein und laden Sie die Docker-Images:
|
||||||
|
```bash
|
||||||
|
docker load -i /path/to/destination/<image-name>.tar.xz
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **1.4. Starten der Anwendung**
|
||||||
|
Führen Sie das Bereitstellungsskript aus:
|
||||||
|
```bash
|
||||||
|
bash docker/deploy.sh
|
||||||
|
```
|
||||||
|
Dieses Skript:
|
||||||
|
- Startet die Docker-Container mithilfe von `docker compose`.
|
||||||
|
- Verbindet den Reverse Proxy (Caddy) mit der Anwendung.
|
||||||
|
|
||||||
|
Die Anwendung sollte unter `http://<server-ip>` oder der konfigurierten Domain erreichbar sein.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **2. Best Practices**
|
||||||
|
|
||||||
|
### **2.1. Sicherheit**
|
||||||
|
1. **Umgebungsvariablen schützen:**
|
||||||
|
- Stellen Sie sicher, dass die Datei `.env` nicht versehentlich in ein öffentliches Repository hochgeladen wird.
|
||||||
|
- Verwenden Sie geeignete Zugriffsrechte:
|
||||||
|
```bash
|
||||||
|
chmod 600 .env
|
||||||
|
```
|
||||||
|
2. **HTTPS aktivieren:**
|
||||||
|
- Der Caddy-Webserver unterstützt automatisch HTTPS. Stellen Sie sicher, dass eine gültige Domain konfiguriert ist.
|
||||||
|
|
||||||
|
3. **Zugriffsrechte beschränken:**
|
||||||
|
- Verwenden Sie Benutzerrollen („admin“, „guest“), um den Zugriff auf kritische Funktionen zu steuern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.2. Performance**
|
||||||
|
1. **Docker-Container optimieren:**
|
||||||
|
- Reduzieren Sie die Größe der Docker-Images, indem Sie unnötige Dateien in `.dockerignore` ausschließen.
|
||||||
|
|
||||||
|
2. **Datenbankwartung:**
|
||||||
|
- Führen Sie regelmäßige Backups der SQLite-Datenbank durch:
|
||||||
|
```bash
|
||||||
|
cp db/sqlite.db /path/to/backup/location/
|
||||||
|
```
|
||||||
|
- Optimieren Sie die Datenbank regelmäßig:
|
||||||
|
```sql
|
||||||
|
VACUUM;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Skalierung:**
|
||||||
|
- Bei hoher Last kann die Anwendung mit Kubernetes oder einer Cloud-Lösung (z. B. AWS oder Azure) skaliert werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.3. Fehlerbehebung**
|
||||||
|
1. **Logs überprüfen:**
|
||||||
|
- Docker-Logs können wichtige Debug-Informationen liefern:
|
||||||
|
```bash
|
||||||
|
docker logs <container-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Health Checks:**
|
||||||
|
- Integrieren Sie Health Checks in die Docker Compose-Datei, um sicherzustellen, dass die Dienste korrekt laufen.
|
||||||
|
|
||||||
|
3. **Fehlerhafte Drucker deaktivieren:**
|
||||||
|
- Deaktivieren Sie Drucker mit einer hohen Fehlerrate über das Admin-Dashboard, um die Benutzererfahrung zu verbessern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.4. Updates**
|
||||||
|
1. **Neue Funktionen hinzufügen:**
|
||||||
|
- Aktualisieren Sie die Anwendung und erstellen Sie neue Docker-Images:
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
bash docker/build.sh
|
||||||
|
```
|
||||||
|
- Stellen Sie die aktualisierten Images bereit:
|
||||||
|
```bash
|
||||||
|
bash docker/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Datenbankmigrationen:**
|
||||||
|
- Führen Sie neue Migrationsskripte mit folgendem Befehl aus:
|
||||||
|
```bash
|
||||||
|
pnpm run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **3. Backup und Wiederherstellung**
|
||||||
|
|
||||||
|
### **3.1. Backups erstellen**
|
||||||
|
Sichern Sie wichtige Dateien und Datenbanken regelmäßig:
|
||||||
|
- **SQLite-Datenbank:**
|
||||||
|
```bash
|
||||||
|
cp db/sqlite.db /backup/location/sqlite-$(date +%F).db
|
||||||
|
```
|
||||||
|
- **Docker-Images:**
|
||||||
|
```bash
|
||||||
|
docker save myp-rp:latest | gzip > /backup/location/myp-rp-$(date +%F).tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3.2. Wiederherstellung**
|
||||||
|
- **Datenbank wiederherstellen:**
|
||||||
|
```bash
|
||||||
|
cp /backup/location/sqlite-<date>.db db/sqlite.db
|
||||||
|
```
|
||||||
|
- **Docker-Images importieren:**
|
||||||
|
```bash
|
||||||
|
docker load < /backup/location/myp-rp-<date>.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
Nächster Schritt: [=> Admin-Dashboard](./Admin-Dashboard.md)
|
153
frontend/docs/Datenbank.md
Normal file
153
frontend/docs/Datenbank.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# **Datenbank und Analytik-Funktionen**
|
||||||
|
|
||||||
|
Dieser Abschnitt konzentriert sich auf die Struktur der Datenbank sowie die Analyse- und Prognosefunktionen, die im Projekt verwendet werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **1. Datenbankstruktur**
|
||||||
|
|
||||||
|
Das Datenbankschema wurde mit **Drizzle ORM** definiert und basiert auf SQLite. Die wichtigsten Tabellen und ihre Zwecke sind:
|
||||||
|
|
||||||
|
### **1.1. Tabellenübersicht**
|
||||||
|
|
||||||
|
#### **`user`**
|
||||||
|
- Speichert Benutzerinformationen.
|
||||||
|
- Enthält Rollen wie „admin“ oder „guest“ zur Verwaltung von Berechtigungen.
|
||||||
|
|
||||||
|
| **Feld** | **Typ** | **Beschreibung** |
|
||||||
|
|-------------------|------------|-------------------------------------------|
|
||||||
|
| `id` | `text` | Eindeutige ID des Benutzers. |
|
||||||
|
| `github_id` | `integer` | ID des Benutzers aus dem OAuth-Dienst. |
|
||||||
|
| `name` | `text` | Benutzername. |
|
||||||
|
| `displayName` | `text` | Angezeigter Name. |
|
||||||
|
| `email` | `text` | E-Mail-Adresse. |
|
||||||
|
| `role` | `text` | Benutzerrolle, Standardwert: „guest“. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **`printer`**
|
||||||
|
- Beschreibt verfügbare Drucker und deren Status.
|
||||||
|
|
||||||
|
| **Feld** | **Typ** | **Beschreibung** |
|
||||||
|
|-------------------|------------|-------------------------------------------|
|
||||||
|
| `id` | `text` | Eindeutige Drucker-ID. |
|
||||||
|
| `name` | `text` | Name des Druckers. |
|
||||||
|
| `description` | `text` | Beschreibung oder Spezifikationen. |
|
||||||
|
| `status` | `integer` | Betriebsstatus (0 = inaktiv, 1 = aktiv). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **`printJob`**
|
||||||
|
- Speichert Informationen zu Druckaufträgen.
|
||||||
|
|
||||||
|
| **Feld** | **Typ** | **Beschreibung** |
|
||||||
|
|-----------------------|---------------|-------------------------------------------------------|
|
||||||
|
| `id` | `text` | Eindeutige Auftrags-ID. |
|
||||||
|
| `printerId` | `text` | Verweis auf die ID des Druckers. |
|
||||||
|
| `userId` | `text` | Verweis auf die ID des Benutzers. |
|
||||||
|
| `startAt` | `integer` | Startzeit des Druckauftrags (Unix-Timestamp). |
|
||||||
|
| `durationInMinutes` | `integer` | Dauer des Druckauftrags in Minuten. |
|
||||||
|
| `comments` | `text` | Zusätzliche Kommentare. |
|
||||||
|
| `aborted` | `integer` | 1 = Abgebrochen, 0 = Erfolgreich abgeschlossen. |
|
||||||
|
| `abortReason` | `text` | Grund für den Abbruch (falls zutreffend). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **`session`**
|
||||||
|
- Verwaltert Benutzer-Sitzungen und Ablaufzeiten.
|
||||||
|
|
||||||
|
| **Feld** | **Typ** | **Beschreibung** |
|
||||||
|
|-------------------|------------|-------------------------------------------|
|
||||||
|
| `id` | `text` | Eindeutige Sitzungs-ID. |
|
||||||
|
| `user_id` | `text` | Verweis auf die ID des Benutzers. |
|
||||||
|
| `expires_at` | `integer` | Zeitpunkt, wann die Sitzung abläuft. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **1.2. Relationen**
|
||||||
|
- `printer` → `printJob`: Druckaufträge sind an spezifische Drucker gebunden.
|
||||||
|
- `user` → `printJob`: Druckaufträge werden Benutzern zugewiesen.
|
||||||
|
- `user` → `session`: Sitzungen verknüpfen Benutzer mit Login-Details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **2. Analytik-Funktionen**
|
||||||
|
|
||||||
|
Das Projekt bietet verschiedene Analytik- und Prognosetools, um die Druckernutzung und Fehler zu überwachen.
|
||||||
|
|
||||||
|
### **2.1. Fehlerratenanalyse**
|
||||||
|
- Funktion: `calculatePrinterErrorRate` (in `src/utils/analytics/error-rate.ts`).
|
||||||
|
- Berechnet die prozentuale Fehlerrate für jeden Drucker basierend auf abgebrochenen Aufträgen.
|
||||||
|
|
||||||
|
Beispielausgabe:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "name": "Drucker 1", "errorRate": 5.2 },
|
||||||
|
{ "name": "Drucker 2", "errorRate": 3.7 }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.2. Abbruchgründe**
|
||||||
|
- Funktion: `calculateAbortReasonsCount` (in `src/utils/analytics/errors.ts`).
|
||||||
|
- Zählt die Häufigkeit der Abbruchgründe aus der Tabelle `printJob`.
|
||||||
|
|
||||||
|
Beispielausgabe:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "abortReason": "Materialmangel", "count": 10 },
|
||||||
|
{ "abortReason": "Düsenverstopfung", "count": 7 }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.3. Nutzung und Prognosen**
|
||||||
|
#### Nutzung:
|
||||||
|
- Funktion: `calculatePrinterUtilization` (in `src/utils/analytics/utilization.ts`).
|
||||||
|
- Berechnet die Nutzung der Drucker in Prozent.
|
||||||
|
|
||||||
|
Beispielausgabe:
|
||||||
|
```json
|
||||||
|
{ "printerId": "1", "utilizationPercentage": 85 }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Prognosen:
|
||||||
|
- Funktion: `forecastPrinterUsage` (in `src/utils/analytics/forecast.ts`).
|
||||||
|
- Nutzt historische Daten, um die erwartete Druckernutzung für kommende Tage/Wochen zu schätzen.
|
||||||
|
|
||||||
|
Beispielausgabe:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "day": 1, "usageMinutes": 300 },
|
||||||
|
{ "day": 2, "usageMinutes": 200 }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **2.4. Druckvolumen**
|
||||||
|
- Funktion: `calculatePrintVolumes` (in `src/utils/analytics/volume.ts`).
|
||||||
|
- Vergleicht die Anzahl der abgeschlossenen Druckaufträge für heute, diese Woche und diesen Monat.
|
||||||
|
|
||||||
|
Beispielausgabe:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"today": 15,
|
||||||
|
"thisWeek": 90,
|
||||||
|
"thisMonth": 300
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **3. Datenbankinitialisierung**
|
||||||
|
Die Datenbank wird über Skripte in der `package.json` initialisiert:
|
||||||
|
```bash
|
||||||
|
pnpm run db:clean # Datenbank und Migrationsordner löschen
|
||||||
|
pnpm run db:generate # Neues Schema generieren
|
||||||
|
pnpm run db:migrate # Migrationsskripte ausführen
|
||||||
|
```
|
||||||
|
|
||||||
|
Nächster Schritt: [=> Bereitstellungsdetails und Best Practices](./Bereitstellungsdetails.md)
|
93
frontend/docs/Installation.md
Normal file
93
frontend/docs/Installation.md
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# **Installation und Einrichtung**
|
||||||
|
|
||||||
|
In diesem Abschnitt wird beschrieben, wie die MYP-Anwendung installiert und eingerichtet wird. Diese Schritte umfassen die Vorbereitung der Umgebung, das Konfigurieren der notwendigen Dienste und die Bereitstellung des Projekts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Voraussetzungen**
|
||||||
|
### **Hardware und Software**
|
||||||
|
- **Raspberry Pi:** Die Anwendung ist für den Einsatz auf einem Raspberry Pi optimiert, auf dem Raspbian Lite installiert sein sollte.
|
||||||
|
- **Docker:** Docker und Docker Compose müssen installiert sein.
|
||||||
|
- **Netzwerkzugriff:** Der Raspberry Pi muss im Netzwerk erreichbar sein.
|
||||||
|
|
||||||
|
### **Abhängigkeiten**
|
||||||
|
- Node.js (mindestens Version 20)
|
||||||
|
- PNPM (Paketmanager)
|
||||||
|
- SQLite (für lokale Datenbankverwaltung)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Schritte zur Einrichtung**
|
||||||
|
|
||||||
|
### **1. Repository klonen**
|
||||||
|
Klonen Sie das Repository auf Ihr System:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd <repository-ordner>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Konfiguration der Umgebungsvariablen**
|
||||||
|
Passen Sie die Datei `.env.example` an und benennen Sie sie in `.env` um:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
Erforderliche Variablen:
|
||||||
|
- `OAUTH_CLIENT_ID`: Client-ID für die OAuth-Authentifizierung
|
||||||
|
- `OAUTH_CLIENT_SECRET`: Geheimnis für die OAuth-Authentifizierung
|
||||||
|
|
||||||
|
### **3. Docker-Container erstellen**
|
||||||
|
Führen Sie das Skript `build.sh` aus, um Docker-Images zu erstellen:
|
||||||
|
```bash
|
||||||
|
bash docker/build.sh
|
||||||
|
```
|
||||||
|
Dies erstellt die notwendigen Docker-Images, einschließlich der Anwendung und eines Caddy-Webservers.
|
||||||
|
|
||||||
|
### **4. Docker-Images speichern**
|
||||||
|
Speichern Sie die Images in komprimierter Form, um sie auf anderen Geräten bereitzustellen:
|
||||||
|
```bash
|
||||||
|
bash docker/save.sh <image-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### **5. Bereitstellung**
|
||||||
|
Kopieren Sie die Docker-Images auf den Zielserver (z. B. Raspberry Pi) und führen Sie `deploy.sh` aus:
|
||||||
|
```bash
|
||||||
|
scp docker/images/*.tar.xz <ziel-server>:/path/to/deployment/
|
||||||
|
bash docker/deploy.sh
|
||||||
|
```
|
||||||
|
Das Skript führt die Docker Compose-Konfiguration aus und startet die Anwendung.
|
||||||
|
|
||||||
|
### **(Optional: 6. Admin-User anlegen)**
|
||||||
|
|
||||||
|
Um einen Admin-User anzulegen, muss zuerst das Container-Image gestartet werden. Anschließend meldet man sich mittels
|
||||||
|
der GitHub-Authentifizierung bei der Anwendung an.
|
||||||
|
|
||||||
|
Der nun in der Datenbank angelegte User hat die Rolle `guest`. Über das CLI muss man nun in die SQLite-Datenbank (die Datenbank sollte außerhalb des Container-Images liegen) wechseln und
|
||||||
|
den User updaten.
|
||||||
|
|
||||||
|
|
||||||
|
#### SQL-Befehl, um den User zu updaten:
|
||||||
|
```bash
|
||||||
|
sqlite3 db.sqlite3
|
||||||
|
UPDATE users SET role = 'admin' WHERE id = <user-id>;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Start der Anwendung**
|
||||||
|
Sobald die Docker-Container laufen, ist die Anwendung unter der angegebenen Domain oder IP-Adresse erreichbar. Standardmäßig verwendet der Caddy-Webserver Port 80 (HTTP) und 443 (HTTPS).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Optional: Entwicklungsmodus**
|
||||||
|
Für lokale Tests können Sie die Anwendung ohne Docker starten:
|
||||||
|
1. Installieren Sie Abhängigkeiten:
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
2. Starten Sie den Entwicklungsserver:
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
Die Anwendung ist dann unter `http://localhost:3000` verfügbar.
|
||||||
|
|
||||||
|
Nächster Schritt: [=> Nutzung](./Nutzung.md)
|
75
frontend/docs/Nutzung.md
Normal file
75
frontend/docs/Nutzung.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# **Features und Nutzung der Anwendung**
|
||||||
|
|
||||||
|
In diesem Abschnitt beschreibe ich die Hauptfunktionen von MYP (Manage Your Printer) und gebe Anweisungen zur Nutzung der verschiedenen Module.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **1. Hauptfunktionen**
|
||||||
|
|
||||||
|
### **1.1. Druckerreservierung**
|
||||||
|
- Nutzer können Drucker für einen definierten Zeitraum reservieren.
|
||||||
|
- Konflikte bei Reservierungen werden durch ein Echtzeit-Überprüfungssystem verhindert.
|
||||||
|
|
||||||
|
### **1.2. Fehler- und Auslastungsanalyse**
|
||||||
|
- Darstellung von Druckfehlern nach Kategorien und Häufigkeiten.
|
||||||
|
- Übersicht der aktuellen und historischen Druckernutzung.
|
||||||
|
- Diagramme zur Fehlerrate, Nutzung und Druckvolumen.
|
||||||
|
|
||||||
|
### **1.3. Admin-Dashboard**
|
||||||
|
- Verwaltung von Druckern, Nutzern und Druckaufträgen.
|
||||||
|
- Überblick über alle Abbruchgründe und Druckfehler.
|
||||||
|
- Zugriff auf erweiterte Statistiken und Prognosen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **2. Nutzung der Anwendung**
|
||||||
|
|
||||||
|
### **2.1. Login und Authentifizierung**
|
||||||
|
- Die Anwendung unterstützt OAuth-basierte Authentifizierung.
|
||||||
|
- Nutzer müssen sich mit einem gültigen Konto anmelden, um Zugriff auf die Funktionen zu erhalten.
|
||||||
|
|
||||||
|
### **2.2. Dashboard**
|
||||||
|
- Nach dem Login gelangen die Nutzer auf das Dashboard, das einen Überblick über die aktuelle Druckernutzung bietet.
|
||||||
|
- Administratoren haben Zugriff auf zusätzliche Menüpunkte, wie z. B. Benutzerverwaltung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **3. Admin-Funktionen**
|
||||||
|
|
||||||
|
### **3.1. Druckerverwaltung**
|
||||||
|
- Administratoren können Drucker hinzufügen, bearbeiten oder löschen.
|
||||||
|
- Status eines Druckers (z. B. „in Betrieb“, „außer Betrieb“) kann angepasst werden.
|
||||||
|
|
||||||
|
### **3.2. Nutzerverwaltung**
|
||||||
|
- Verwalten von Benutzerkonten, einschließlich Rollen (z. B. „Admin“ oder „User“).
|
||||||
|
- Benutzer können aktiviert oder deaktiviert werden.
|
||||||
|
|
||||||
|
### **3.3. Statistiken und Berichte**
|
||||||
|
- Diagramme wie:
|
||||||
|
- **Abbruchgründe:** Zeigt häufige Fehlerursachen.
|
||||||
|
- **Fehlerrate:** Prozentuale Fehlerquote der Drucker.
|
||||||
|
- **Nutzung:** Prognosen für die Druckernutzung pro Wochentag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **4. Diagramme und Visualisierungen**
|
||||||
|
|
||||||
|
### **4.1. Abbruchgründe**
|
||||||
|
- Ein Säulendiagramm zeigt die Häufigkeiten der Fehlerursachen.
|
||||||
|
- Nutzt Echtzeit-Daten aus der Druckhistorie.
|
||||||
|
|
||||||
|
### **4.2. Prognostizierte Nutzung**
|
||||||
|
- Ein Liniendiagramm zeigt die erwartete Druckernutzung pro Tag.
|
||||||
|
- Hilft bei der Planung von Wartungszeiten.
|
||||||
|
|
||||||
|
### **4.3. Druckvolumen**
|
||||||
|
- Balkendiagramme vergleichen Druckaufträge heute, diese Woche und diesen Monat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **5. Interaktive Komponenten**
|
||||||
|
- **Benachrichtigungen:** Informieren über Druckaufträge, Fehler oder Systemereignisse.
|
||||||
|
- **Filter und Suchfunktionen:** Erleichtern das Auffinden von Druckern oder Druckaufträgen.
|
||||||
|
- **Rollenbasierter Zugriff:** Funktionen sind je nach Benutzerrolle eingeschränkt.
|
||||||
|
|
||||||
|
Nächster Schritt: [=> Technische Architektur und Codeaufbau](./Architektur.md)
|
37
frontend/docs/README.md
Normal file
37
frontend/docs/README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# **Einleitung**
|
||||||
|
|
||||||
|
> Information: Die Dokumenation wurde mit generativer AI erstellt und kann fehlerhaft sein. Im Zweifel bitte die Quellcode-Dateien anschauen oder die Entwickler kontaktieren.
|
||||||
|
|
||||||
|
## **Projektbeschreibung**
|
||||||
|
MYP (Manage Your Printer) ist eine Webanwendung zur Verwaltung und Reservierung von 3D-Druckern. Das Projekt wurde als Abschlussarbeit im Rahmen der Fachinformatiker-Ausbildung mit Schwerpunkt Daten- und Prozessanalyse entwickelt und dient als Plattform zur einfachen Koordination und Überwachung von Druckressourcen. Es wurde speziell für die Technische Berufsausbildung des Mercedes-Benz Werkes in Berlin-Marienfelde erstellt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Hauptmerkmale**
|
||||||
|
- **Druckerreservierungen:** Nutzer können 3D-Drucker in definierten Zeitfenstern reservieren.
|
||||||
|
- **Fehleranalyse:** Statistiken über Druckfehler und Abbruchgründe werden visuell dargestellt.
|
||||||
|
- **Druckauslastung:** Echtzeit-Daten über die Nutzung der Drucker.
|
||||||
|
- **Admin-Dashboard:** Übersichtliche Verwaltung und Konfiguration von Druckern, Benutzern und Druckaufträgen.
|
||||||
|
- **Datenbankintegration:** Alle Daten werden in einer SQLite-Datenbank gespeichert und verwaltet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Technologien**
|
||||||
|
- **Frontend:** React, Next.js, TailwindCSS
|
||||||
|
- **Backend:** Node.js, Drizzle ORM
|
||||||
|
- **Datenbank:** SQLite
|
||||||
|
- **Deployment:** Docker und Raspberry Pi
|
||||||
|
- **Zusätzliche Bibliotheken:** recharts für Diagramme, Faker.js für Testdaten, sowie diverse Radix-UI-Komponenten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Dateistruktur**
|
||||||
|
Die Repository-Dateien sind in logische Abschnitte unterteilt:
|
||||||
|
1. **Docker-Konfigurationen** (`docker/`) - Skripte und Konfigurationsdateien für die Bereitstellung.
|
||||||
|
2. **Frontend-Komponenten** (`src/app/`) - Weboberfläche und deren Funktionalitäten.
|
||||||
|
3. **Backend-Funktionen** (`src/server/`) - Datenbankinteraktionen und Authentifizierungslogik.
|
||||||
|
4. **Utils und Helferfunktionen** (`src/utils/`) - Wiederverwendbare Dienste und Hilfsmethoden.
|
||||||
|
5. **Datenbank-Skripte** (`drizzle/`) - Datenbankschemas und Migrationsdateien.
|
||||||
|
|
||||||
|
|
||||||
|
Nächster Schritt: [=> Installation](./Installation.md)
|
12
frontend/drizzle.config.ts
Normal file
12
frontend/drizzle.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
//@ts-ignore - better-sqlite driver throws an error even though its an valid value
|
||||||
|
export default defineConfig({
|
||||||
|
dialect: "sqlite",
|
||||||
|
schema: "./src/server/db/schema.ts",
|
||||||
|
out: "./drizzle",
|
||||||
|
driver: "libsql",
|
||||||
|
dbCredentials: {
|
||||||
|
url: "file:./db/sqlite.db",
|
||||||
|
},
|
||||||
|
});
|
35
frontend/drizzle/0000_overjoyed_strong_guy.sql
Normal file
35
frontend/drizzle/0000_overjoyed_strong_guy.sql
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
CREATE TABLE `printJob` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`printerId` text NOT NULL,
|
||||||
|
`userId` text NOT NULL,
|
||||||
|
`startAt` integer NOT NULL,
|
||||||
|
`durationInMinutes` integer NOT NULL,
|
||||||
|
`comments` text,
|
||||||
|
`aborted` integer DEFAULT false NOT NULL,
|
||||||
|
`abortReason` text,
|
||||||
|
FOREIGN KEY (`printerId`) REFERENCES `printer`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `printer` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`description` text NOT NULL,
|
||||||
|
`status` integer DEFAULT 0 NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `session` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `user` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`github_id` integer NOT NULL,
|
||||||
|
`name` text,
|
||||||
|
`displayName` text,
|
||||||
|
`email` text NOT NULL,
|
||||||
|
`role` text DEFAULT 'guest'
|
||||||
|
);
|
241
frontend/drizzle/meta/0000_snapshot.json
Normal file
241
frontend/drizzle/meta/0000_snapshot.json
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "791dc197-5254-4432-bd9f-1368d1a5aa6a",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"printJob": {
|
||||||
|
"name": "printJob",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"printerId": {
|
||||||
|
"name": "printerId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"startAt": {
|
||||||
|
"name": "startAt",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"durationInMinutes": {
|
||||||
|
"name": "durationInMinutes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"name": "comments",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"aborted": {
|
||||||
|
"name": "aborted",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"abortReason": {
|
||||||
|
"name": "abortReason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"printJob_printerId_printer_id_fk": {
|
||||||
|
"name": "printJob_printerId_printer_id_fk",
|
||||||
|
"tableFrom": "printJob",
|
||||||
|
"tableTo": "printer",
|
||||||
|
"columnsFrom": [
|
||||||
|
"printerId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"printJob_userId_user_id_fk": {
|
||||||
|
"name": "printJob_userId_user_id_fk",
|
||||||
|
"tableFrom": "printJob",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"printer": {
|
||||||
|
"name": "printer",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"name": "session",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"github_id": {
|
||||||
|
"name": "github_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"displayName": {
|
||||||
|
"name": "displayName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'guest'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
}
|
||||||
|
}
|
13
frontend/drizzle/meta/_journal.json
Normal file
13
frontend/drizzle/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1715416514336,
|
||||||
|
"tag": "0000_overjoyed_strong_guy",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
376
frontend/https-setup.sh
Normal file
376
frontend/https-setup.sh
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# HTTPS-Setup-Skript für das MYP-Projekt
|
||||||
|
# Konfiguriert einen Raspberry Pi mit einer dual-network-Konfiguration
|
||||||
|
# - LAN (eth0): Firmennetzwerk mit Zugang zu Internet und unter https://m040tbaraspi001.de040.corpintra.net/ erreichbar
|
||||||
|
# - WLAN (wlan0): Verbindung zum Offline-Netzwerk, wo der Backend-Host erreichbar ist
|
||||||
|
|
||||||
|
# Farbcodes für Ausgabe
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
success_log() {
|
||||||
|
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ERFOLG:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
header() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}===== $1 =====${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prüfen, ob das Skript als Root ausgeführt wird
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
error_log "Dieses Skript muss als Root ausgeführt werden."
|
||||||
|
error_log "Bitte führen Sie es mit 'sudo' aus."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Konfigurationswerte
|
||||||
|
FIRMENNETZWERK_HOSTNAME="m040tbaraspi001.de040.corpintra.net"
|
||||||
|
BACKEND_HOST="192.168.0.105" # Backend-IP im Offline-Netzwerk
|
||||||
|
BACKEND_PORT="5000" # Backend-Port
|
||||||
|
OFFLINE_NETWORK_SSID="MYP-Net"
|
||||||
|
OFFLINE_NETWORK_PASSWORD="myp-password"
|
||||||
|
CADDY_VERSION="2.7.6"
|
||||||
|
|
||||||
|
header "MYP-Netzwerk und HTTPS-Setup"
|
||||||
|
log "Dieses Skript konfiguriert Ihren Raspberry Pi für:"
|
||||||
|
log "1. Firmennetzwerk über LAN (eth0) mit Internet-Zugang"
|
||||||
|
log "2. Offline-Netzwerk über WLAN (wlan0) für Backend-Kommunikation"
|
||||||
|
log "3. HTTPS mit selbstsigniertem Zertifikat für ${FIRMENNETZWERK_HOSTNAME}"
|
||||||
|
|
||||||
|
# Netzwerkkonfiguration
|
||||||
|
setup_network() {
|
||||||
|
header "Netzwerkkonfiguration"
|
||||||
|
|
||||||
|
# Sichern der aktuellen Netzwerkkonfiguration
|
||||||
|
log "Sichere aktuelle Netzwerkkonfiguration..."
|
||||||
|
if [ -f /etc/dhcpcd.conf ]; then
|
||||||
|
cp /etc/dhcpcd.conf /etc/dhcpcd.conf.bak
|
||||||
|
success_log "Aktuelle Netzwerkkonfiguration gesichert in /etc/dhcpcd.conf.bak"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Konfigurieren von dhcpcd.conf für statische Routing
|
||||||
|
log "Konfiguriere Routing für duale Netzwerke..."
|
||||||
|
cat > /etc/dhcpcd.conf << EOL
|
||||||
|
# MYP Dual-Network Configuration
|
||||||
|
# eth0: Firmennetzwerk mit Internet
|
||||||
|
# wlan0: Offline-Netzwerk für Backend
|
||||||
|
|
||||||
|
# Allow dhcpcd to manage all interfaces
|
||||||
|
allowinterfaces eth0 wlan0
|
||||||
|
|
||||||
|
# eth0 configuration (Firmennetzwerk)
|
||||||
|
interface eth0
|
||||||
|
# DHCP for eth0, all default routes go through eth0
|
||||||
|
metric 100
|
||||||
|
|
||||||
|
# wlan0 configuration (Offline Network)
|
||||||
|
interface wlan0
|
||||||
|
# Static IP for wlan0
|
||||||
|
metric 200
|
||||||
|
# Add specific route to backend via wlan0
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Konfigurieren von wpa_supplicant für WLAN-Verbindung
|
||||||
|
log "Konfiguriere WLAN-Verbindung für Offline-Netzwerk..."
|
||||||
|
cat > /etc/wpa_supplicant/wpa_supplicant.conf << EOL
|
||||||
|
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||||
|
update_config=1
|
||||||
|
country=DE
|
||||||
|
|
||||||
|
network={
|
||||||
|
ssid="${OFFLINE_NETWORK_SSID}"
|
||||||
|
psk="${OFFLINE_NETWORK_PASSWORD}"
|
||||||
|
priority=1
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf
|
||||||
|
|
||||||
|
# Routing-Tabelle konfigurieren
|
||||||
|
log "Konfiguriere spezifisches Routing zum Backend..."
|
||||||
|
if ! grep -q "${BACKEND_HOST}" /etc/iproute2/rt_tables; then
|
||||||
|
echo "200 backend" >> /etc/iproute2/rt_tables
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Routing-Regeln in /etc/network/if-up.d/ hinzufügen
|
||||||
|
cat > /etc/network/if-up.d/route-backend << EOL
|
||||||
|
#!/bin/bash
|
||||||
|
# Routing-Regeln für Backend-Host über WLAN
|
||||||
|
|
||||||
|
# Wenn wlan0 hochgefahren wird
|
||||||
|
if [ "\$IFACE" = "wlan0" ]; then
|
||||||
|
# Spezifische Route zum Backend über wlan0
|
||||||
|
/sbin/ip route add ${BACKEND_HOST}/32 dev wlan0
|
||||||
|
fi
|
||||||
|
EOL
|
||||||
|
|
||||||
|
chmod +x /etc/network/if-up.d/route-backend
|
||||||
|
|
||||||
|
success_log "Netzwerkkonfiguration abgeschlossen"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installation von Caddy als Reverse-Proxy
|
||||||
|
install_caddy() {
|
||||||
|
header "Installation von Caddy als Reverse-Proxy"
|
||||||
|
|
||||||
|
log "Überprüfe, ob Caddy bereits installiert ist..."
|
||||||
|
if command -v caddy &> /dev/null; then
|
||||||
|
success_log "Caddy ist bereits installiert"
|
||||||
|
else
|
||||||
|
log "Installiere Caddy ${CADDY_VERSION}..."
|
||||||
|
|
||||||
|
# Download und Installation von Caddy
|
||||||
|
wget -O /tmp/caddy.tar.gz "https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_armv7.tar.gz"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error_log "Fehler beim Herunterladen von Caddy"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar -xzf /tmp/caddy.tar.gz -C /tmp
|
||||||
|
mv /tmp/caddy /usr/local/bin/
|
||||||
|
chmod +x /usr/local/bin/caddy
|
||||||
|
|
||||||
|
# Benutzer und Gruppe für Caddy erstellen
|
||||||
|
if ! id -u caddy &>/dev/null; then
|
||||||
|
useradd --system --home /var/lib/caddy --shell /usr/sbin/nologin caddy
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verzeichnisse für Caddy erstellen
|
||||||
|
mkdir -p /etc/caddy /var/lib/caddy /var/log/caddy
|
||||||
|
chown -R caddy:caddy /var/lib/caddy /var/log/caddy
|
||||||
|
|
||||||
|
# Systemd-Service für Caddy einrichten
|
||||||
|
cat > /etc/systemd/system/caddy.service << EOL
|
||||||
|
[Unit]
|
||||||
|
Description=Caddy Web Server
|
||||||
|
Documentation=https://caddyserver.com/docs/
|
||||||
|
After=network.target network-online.target
|
||||||
|
Requires=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=caddy
|
||||||
|
Group=caddy
|
||||||
|
ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
|
||||||
|
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
TimeoutStopSec=5s
|
||||||
|
LimitNOFILE=1048576
|
||||||
|
LimitNPROC=512
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=full
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
success_log "Caddy wurde installiert"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Caddyfile für HTTPS mit selbstsigniertem Zertifikat konfigurieren
|
||||||
|
log "Konfiguriere Caddy für HTTPS mit selbstsigniertem Zertifikat..."
|
||||||
|
cat > /etc/caddy/Caddyfile << EOL
|
||||||
|
{
|
||||||
|
# Globale Optionen
|
||||||
|
admin off
|
||||||
|
auto_https disable_redirects
|
||||||
|
|
||||||
|
# Selbstsigniertes Zertifikat verwenden
|
||||||
|
local_certs
|
||||||
|
default_sni ${FIRMENNETZWERK_HOSTNAME}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS-Konfiguration für den Firmennetzwerk-Hostnamen
|
||||||
|
${FIRMENNETZWERK_HOSTNAME} {
|
||||||
|
# TLS mit selbstsigniertem Zertifikat
|
||||||
|
tls internal {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse-Proxy zum Next.js-Frontend
|
||||||
|
reverse_proxy localhost:3000 {
|
||||||
|
# Zeitüberschreitungen für langsame Raspberry Pi-Verbindungen erhöhen
|
||||||
|
timeouts 5m
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/access.log
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP-Konfiguration für lokalen Zugriff
|
||||||
|
:80 {
|
||||||
|
# Weiterleitung zu HTTPS
|
||||||
|
redir https://${FIRMENNETZWERK_HOSTNAME}{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Zusätzlicher Server für Backend-Proxy (API-Anfragen weiterleiten)
|
||||||
|
localhost:8000 {
|
||||||
|
reverse_proxy ${BACKEND_HOST}:${BACKEND_PORT} {
|
||||||
|
# Headers für CORS anpassen
|
||||||
|
header_up Host ${BACKEND_HOST}:${BACKEND_PORT}
|
||||||
|
header_up X-Forwarded-Host ${FIRMENNETZWERK_HOSTNAME}
|
||||||
|
header_up X-Forwarded-Proto https
|
||||||
|
|
||||||
|
# Zeitüberschreitungen für API-Anfragen erhöhen
|
||||||
|
timeouts 1m
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/backend-access.log
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Caddy-Service neu laden und starten
|
||||||
|
log "Starte Caddy-Service..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable caddy
|
||||||
|
systemctl restart caddy
|
||||||
|
|
||||||
|
# Überprüfen, ob Caddy läuft
|
||||||
|
if systemctl is-active --quiet caddy; then
|
||||||
|
success_log "Caddy-Service wurde gestartet und ist aktiv"
|
||||||
|
else
|
||||||
|
error_log "Fehler beim Starten des Caddy-Services"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next.js Frontend-Konfiguration für HTTPS und Backend-Proxy
|
||||||
|
configure_frontend() {
|
||||||
|
header "Frontend-Konfiguration für HTTPS"
|
||||||
|
|
||||||
|
# Verzeichnis für das Frontend
|
||||||
|
FRONTEND_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
|
# Prüfen, ob das Frontend-Verzeichnis existiert
|
||||||
|
if [ ! -d "$FRONTEND_DIR" ]; then
|
||||||
|
error_log "Frontend-Verzeichnis nicht gefunden: $FRONTEND_DIR"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Konfiguriere Frontend für HTTPS und Backend-Proxy..."
|
||||||
|
|
||||||
|
# .env.local-Datei für das Frontend erstellen
|
||||||
|
cat > "$FRONTEND_DIR/.env.local" << EOL
|
||||||
|
# Backend API Konfiguration (über lokalen Proxy zu Backend)
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# Frontend-URL für OAuth Callback (HTTPS)
|
||||||
|
NEXT_PUBLIC_FRONTEND_URL=https://${FIRMENNETZWERK_HOSTNAME}
|
||||||
|
|
||||||
|
# Explizite OAuth Callback URL für GitHub
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=https://${FIRMENNETZWERK_HOSTNAME}/auth/login/callback
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Berechtigungen setzen
|
||||||
|
chown -R $(stat -c '%U:%G' "$FRONTEND_DIR") "$FRONTEND_DIR/.env.local"
|
||||||
|
chmod 600 "$FRONTEND_DIR/.env.local"
|
||||||
|
|
||||||
|
success_log "Frontend wurde für HTTPS und Backend-Proxy konfiguriert"
|
||||||
|
|
||||||
|
# Hinweis zur Installation und zum Start des Frontends
|
||||||
|
log "${YELLOW}Hinweis: Führen Sie nun das Frontend-Installationsskript aus:${NC}"
|
||||||
|
log "cd $FRONTEND_DIR && ./install.sh"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hostname setzen
|
||||||
|
set_hostname() {
|
||||||
|
header "Hostname konfigurieren"
|
||||||
|
|
||||||
|
log "Aktueller Hostname: $(hostname)"
|
||||||
|
log "Setze Hostname auf ${FIRMENNETZWERK_HOSTNAME}..."
|
||||||
|
|
||||||
|
# Hostname in /etc/hostname setzen
|
||||||
|
echo "${FIRMENNETZWERK_HOSTNAME}" > /etc/hostname
|
||||||
|
|
||||||
|
# Hostname in /etc/hosts aktualisieren
|
||||||
|
if grep -q "$(hostname)" /etc/hosts; then
|
||||||
|
sed -i "s/$(hostname)/${FIRMENNETZWERK_HOSTNAME}/g" /etc/hosts
|
||||||
|
else
|
||||||
|
echo "127.0.1.1 ${FIRMENNETZWERK_HOSTNAME}" >> /etc/hosts
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aktualisieren des Hostnamens ohne Neustart
|
||||||
|
hostname "${FIRMENNETZWERK_HOSTNAME}"
|
||||||
|
|
||||||
|
success_log "Hostname wurde auf ${FIRMENNETZWERK_HOSTNAME} gesetzt"
|
||||||
|
log "${YELLOW}Hinweis: Ein Neustart wird empfohlen, um sicherzustellen, dass der neue Hostname vollständig übernommen wurde.${NC}"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hauptfunktion
|
||||||
|
main() {
|
||||||
|
# Begrüßung und Bestätigung
|
||||||
|
header "MYP HTTPS und Dual-Network Setup"
|
||||||
|
log "Dieses Skript richtet Ihren Raspberry Pi für das MYP-Projekt ein:"
|
||||||
|
log "- Setzt den Hostname auf: ${FIRMENNETZWERK_HOSTNAME}"
|
||||||
|
log "- Konfiguriert das duale Netzwerk (LAN für Internet, WLAN für Backend)"
|
||||||
|
log "- Installiert Caddy als Reverse-Proxy mit selbstsigniertem HTTPS"
|
||||||
|
log "- Konfiguriert das Frontend für die Kommunikation mit dem Backend"
|
||||||
|
echo ""
|
||||||
|
log "${YELLOW}WICHTIG: Diese Konfiguration kann bestehende Netzwerk- und HTTPS-Einstellungen überschreiben.${NC}"
|
||||||
|
read -p "Möchten Sie fortfahren? (j/n): " confirm
|
||||||
|
|
||||||
|
if [[ "$confirm" != "j" ]]; then
|
||||||
|
log "Setup abgebrochen."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Schritte ausführen
|
||||||
|
set_hostname || { error_log "Fehler beim Setzen des Hostnamens"; exit 1; }
|
||||||
|
setup_network || { error_log "Fehler bei der Netzwerkkonfiguration"; exit 1; }
|
||||||
|
install_caddy || { error_log "Fehler bei der Caddy-Installation"; exit 1; }
|
||||||
|
configure_frontend || { error_log "Fehler bei der Frontend-Konfiguration"; exit 1; }
|
||||||
|
|
||||||
|
# Abschlussmeldung
|
||||||
|
header "Setup abgeschlossen"
|
||||||
|
success_log "MYP HTTPS und Dual-Network Setup erfolgreich!"
|
||||||
|
log "Ihr Raspberry Pi ist nun wie folgt konfiguriert:"
|
||||||
|
log "- Hostname: ${FIRMENNETZWERK_HOSTNAME}"
|
||||||
|
log "- HTTPS mit selbstsigniertem Zertifikat über Caddy"
|
||||||
|
log "- Duale Netzwerkkonfiguration:"
|
||||||
|
log " * eth0: Firmennetzwerk mit Internet-Zugang"
|
||||||
|
log " * wlan0: Verbindung zum Offline-Netzwerk (${OFFLINE_NETWORK_SSID})"
|
||||||
|
log "- Frontend-URL: https://${FIRMENNETZWERK_HOSTNAME}"
|
||||||
|
log "- Backend-Kommunikation über lokalen Proxy: http://localhost:8000 -> ${BACKEND_HOST}:${BACKEND_PORT}"
|
||||||
|
echo ""
|
||||||
|
log "${YELLOW}Wichtige nächste Schritte:${NC}"
|
||||||
|
log "1. Starten Sie das Frontend mit dem Installationsskript:"
|
||||||
|
log " cd $FRONTEND_DIR && ./install.sh"
|
||||||
|
log "2. Neustart des Raspberry Pi empfohlen:"
|
||||||
|
log " sudo reboot"
|
||||||
|
echo ""
|
||||||
|
log "${YELLOW}Hinweis zum selbstsignierten Zertifikat:${NC}"
|
||||||
|
log "Bei Zugriff auf https://${FIRMENNETZWERK_HOSTNAME} erhalten Sie eine Zertifikatswarnung im Browser."
|
||||||
|
log "Dies ist normal für selbstsignierte Zertifikate. Sie können die Warnung in Ihrem Browser bestätigen."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Skript starten
|
||||||
|
main
|
570
frontend/install.sh
Normal file
570
frontend/install.sh
Normal file
@ -0,0 +1,570 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# MYP Frontend Installations-Skript
|
||||||
|
# Dieses Skript installiert das Frontend mit Docker und Host-Netzwerkanbindung
|
||||||
|
|
||||||
|
# Farbcodes für Ausgabe
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
success_log() {
|
||||||
|
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ERFOLG:${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
header() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}===== $1 =====${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Variablen definieren
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
FRONTEND_DIR="$SCRIPT_DIR"
|
||||||
|
DOCKER_DIR="$FRONTEND_DIR/docker"
|
||||||
|
DEFAULT_BACKEND_URL="http://192.168.0.105:5000"
|
||||||
|
IMAGE_NAME="myp-rp:latest"
|
||||||
|
CONTAINER_NAME="myp-rp"
|
||||||
|
DB_VOLUME_DIR="/srv/MYP-DB"
|
||||||
|
ENV_FILE_PATH="/srv/myp-env/github.env"
|
||||||
|
|
||||||
|
# Prüfen, ob wir im Frontend-Verzeichnis sind
|
||||||
|
if [ ! -f "$FRONTEND_DIR/package.json" ]; then
|
||||||
|
error_log "Dieses Skript muss im Frontend-Verzeichnis ausgeführt werden."
|
||||||
|
error_log "Bitte wechseln Sie in das Frontend-Verzeichnis."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob Docker installiert ist
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
error_log "Docker ist nicht installiert. Bitte installieren Sie Docker zuerst."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob Docker läuft
|
||||||
|
if ! docker info &> /dev/null; then
|
||||||
|
error_log "Docker-Daemon läuft nicht. Bitte starten Sie Docker mit 'sudo systemctl start docker'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob der Benutzer in der Docker-Gruppe ist
|
||||||
|
if ! groups | grep -q '\bdocker\b'; then
|
||||||
|
error_log "Aktueller Benutzer hat keine Docker-Berechtigungen."
|
||||||
|
error_log "Bitte führen Sie das Skript mit 'sudo' aus oder fügen Sie den Benutzer zur Docker-Gruppe hinzu:"
|
||||||
|
error_log "sudo usermod -aG docker $USER && newgrp docker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle Datenbank-Verzeichnis, falls nicht vorhanden
|
||||||
|
if [ ! -d "$DB_VOLUME_DIR" ]; then
|
||||||
|
log "Erstelle Datenbankverzeichnis: $DB_VOLUME_DIR"
|
||||||
|
if ! mkdir -p "$DB_VOLUME_DIR"; then
|
||||||
|
if ! sudo mkdir -p "$DB_VOLUME_DIR"; then
|
||||||
|
error_log "Konnte Datenbankverzeichnis nicht erstellen. Bitte erstellen Sie es manuell:"
|
||||||
|
error_log "sudo mkdir -p $DB_VOLUME_DIR && sudo chown $USER:$USER $DB_VOLUME_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sudo chown $USER:$USER "$DB_VOLUME_DIR"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Datenbankverzeichnis existiert bereits: $DB_VOLUME_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Funktion zum Laden der Umgebungsvariablen aus /srv/myp-env/github.env
|
||||||
|
load_env_from_srv() {
|
||||||
|
if [ -f "$ENV_FILE_PATH" ]; then
|
||||||
|
log "Lade Umgebungsvariablen aus $ENV_FILE_PATH"
|
||||||
|
|
||||||
|
# Versuche, die Variablen aus der Datei zu laden
|
||||||
|
OAUTH_CLIENT_ID=$(grep -oP 'OAUTH_CLIENT_ID=\K.*' "$ENV_FILE_PATH" 2>/dev/null || echo "client_id")
|
||||||
|
OAUTH_CLIENT_SECRET=$(grep -oP 'OAUTH_CLIENT_SECRET=\K.*' "$ENV_FILE_PATH" 2>/dev/null || echo "client_secret")
|
||||||
|
|
||||||
|
# Prüfe, ob die Backend-URL in der Datei definiert ist
|
||||||
|
BACKEND_URL_FROM_FILE=$(grep -oP 'NEXT_PUBLIC_API_URL=\K.*' "$ENV_FILE_PATH" 2>/dev/null)
|
||||||
|
if [ -n "$BACKEND_URL_FROM_FILE" ]; then
|
||||||
|
log "Backend-URL aus $ENV_FILE_PATH geladen: $BACKEND_URL_FROM_FILE"
|
||||||
|
DEFAULT_BACKEND_URL="$BACKEND_URL_FROM_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "OAuth-Konfiguration aus $ENV_FILE_PATH geladen."
|
||||||
|
else
|
||||||
|
log "${YELLOW}Warnung: $ENV_FILE_PATH nicht gefunden. Verwende Standard-Konfiguration.${NC}"
|
||||||
|
OAUTH_CLIENT_ID="client_id"
|
||||||
|
OAUTH_CLIENT_SECRET="client_secret"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Konfigurieren der Backend-URL
|
||||||
|
configure_backend_url() {
|
||||||
|
local backend_url="${1:-$DEFAULT_BACKEND_URL}"
|
||||||
|
|
||||||
|
header "Backend-URL konfigurieren"
|
||||||
|
log "Konfiguriere Backend-URL für Frontend: $backend_url"
|
||||||
|
|
||||||
|
# Lade OAuth-Konfiguration aus /srv
|
||||||
|
load_env_from_srv
|
||||||
|
|
||||||
|
# Prüfen, ob setup-backend-url.sh existiert
|
||||||
|
if [ -f "$FRONTEND_DIR/setup-backend-url.sh" ]; then
|
||||||
|
chmod +x "$FRONTEND_DIR/setup-backend-url.sh"
|
||||||
|
if ! "$FRONTEND_DIR/setup-backend-url.sh" "$backend_url"; then
|
||||||
|
error_log "Fehler beim Konfigurieren der Backend-URL."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Bestimme den Hostnamen für OAuth
|
||||||
|
HOSTNAME=$(hostname)
|
||||||
|
if [[ "$HOSTNAME" == *"m040tbaraspi001"* ]] || [[ "$HOSTNAME" == *"corpintra"* ]]; then
|
||||||
|
FRONTEND_HOSTNAME="m040tbaraspi001.de040.corpintra.net"
|
||||||
|
OAUTH_URL="http://m040tbaraspi001.de040.corpintra.net/auth/login/callback"
|
||||||
|
log "Erkannt: Unternehmens-Hostname: $FRONTEND_HOSTNAME"
|
||||||
|
else
|
||||||
|
FRONTEND_HOSTNAME="$HOSTNAME"
|
||||||
|
OAUTH_URL="http://$HOSTNAME:3000/auth/login/callback"
|
||||||
|
log "Lokaler Hostname: $FRONTEND_HOSTNAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle .env.local-Datei manuell
|
||||||
|
log "Erstelle .env.local-Datei manuell..."
|
||||||
|
cat > "$FRONTEND_DIR/.env.local" << EOL
|
||||||
|
# Backend API Konfiguration
|
||||||
|
NEXT_PUBLIC_API_URL=${backend_url}
|
||||||
|
|
||||||
|
# Frontend-URL für OAuth Callback
|
||||||
|
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}
|
||||||
|
|
||||||
|
# Explizite OAuth Callback URL für GitHub
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}
|
||||||
|
|
||||||
|
# OAuth Konfiguration aus /srv/myp-env/github.env
|
||||||
|
OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
|
||||||
|
OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
if [ ! -f "$FRONTEND_DIR/.env.local" ]; then
|
||||||
|
error_log "Konnte .env.local-Datei nicht erstellen."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod 600 "$FRONTEND_DIR/.env.local"
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Backend-URL erfolgreich konfiguriert: $backend_url"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Bauen des Images
|
||||||
|
build_image() {
|
||||||
|
header "Docker-Image bauen"
|
||||||
|
log "Baue Docker-Image: $IMAGE_NAME"
|
||||||
|
|
||||||
|
if [ ! -f "$FRONTEND_DIR/Dockerfile" ]; then
|
||||||
|
error_log "Dockerfile nicht gefunden in $FRONTEND_DIR"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$FRONTEND_DIR" || return 1
|
||||||
|
|
||||||
|
# Vorhandenes Image entfernen, falls gewünscht
|
||||||
|
if docker image inspect "$IMAGE_NAME" &>/dev/null; then
|
||||||
|
log "Image $IMAGE_NAME existiert bereits."
|
||||||
|
read -p "Möchten Sie das existierende Image überschreiben? (j/n): " rebuild_choice
|
||||||
|
if [[ "$rebuild_choice" == "j" ]]; then
|
||||||
|
log "Entferne existierendes Image..."
|
||||||
|
docker rmi "$IMAGE_NAME" &>/dev/null || true
|
||||||
|
else
|
||||||
|
log "Behalte existierendes Image."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Baue das Image
|
||||||
|
log "${YELLOW}Baue Docker-Image... (Dies kann auf einem Raspberry Pi mehrere Minuten dauern)${NC}"
|
||||||
|
if ! docker build -t "$IMAGE_NAME" .; then
|
||||||
|
error_log "Fehler beim Bauen des Docker-Images."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Docker-Image erfolgreich gebaut: $IMAGE_NAME"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Speichern des Images
|
||||||
|
save_image() {
|
||||||
|
header "Docker-Image speichern"
|
||||||
|
local save_dir="${1:-$DOCKER_DIR/images}"
|
||||||
|
local save_file="$save_dir/myp-frontend.tar"
|
||||||
|
|
||||||
|
# Prüfen, ob das Image existiert
|
||||||
|
if ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
|
||||||
|
error_log "Image $IMAGE_NAME existiert nicht. Bitte bauen Sie es zuerst."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verzeichnis erstellen, falls es nicht existiert
|
||||||
|
mkdir -p "$save_dir"
|
||||||
|
|
||||||
|
log "Speichere Docker-Image in: $save_file"
|
||||||
|
log "${YELLOW}Dies kann einige Minuten dauern...${NC}"
|
||||||
|
|
||||||
|
if ! docker save -o "$save_file" "$IMAGE_NAME"; then
|
||||||
|
error_log "Fehler beim Speichern des Docker-Images."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe, ob die Datei erstellt wurde
|
||||||
|
if [ ! -f "$save_file" ]; then
|
||||||
|
error_log "Konnte Docker-Image nicht speichern."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe Dateigröße
|
||||||
|
local filesize=$(stat -c%s "$save_file")
|
||||||
|
if [ "$filesize" -lt 1000000 ]; then # Kleiner als 1MB ist verdächtig
|
||||||
|
error_log "Gespeichertes Image ist ungewöhnlich klein ($filesize Bytes). Möglicherweise ist etwas schief gelaufen."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Docker-Image erfolgreich gespeichert: $save_file (Größe: $(du -h "$save_file" | cut -f1))"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Laden des Images
|
||||||
|
load_image() {
|
||||||
|
header "Docker-Image laden"
|
||||||
|
local load_dir="${1:-$DOCKER_DIR/images}"
|
||||||
|
local load_file="$load_dir/myp-frontend.tar"
|
||||||
|
|
||||||
|
# Prüfen, ob die Datei existiert
|
||||||
|
if [ ! -f "$load_file" ]; then
|
||||||
|
error_log "Image-Datei nicht gefunden: $load_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe Dateigröße
|
||||||
|
local filesize=$(stat -c%s "$load_file")
|
||||||
|
if [ "$filesize" -lt 1000000 ]; then # Kleiner als 1MB ist verdächtig
|
||||||
|
error_log "Image-Datei ist ungewöhnlich klein ($filesize Bytes). Möglicherweise ist sie beschädigt."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Lade Docker-Image aus: $load_file"
|
||||||
|
log "${YELLOW}Dies kann einige Minuten dauern...${NC}"
|
||||||
|
|
||||||
|
if ! docker load -i "$load_file"; then
|
||||||
|
error_log "Fehler beim Laden des Docker-Images. Die Datei könnte beschädigt sein."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Docker-Image erfolgreich geladen."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Starten des Containers mit Docker Compose
|
||||||
|
start_container_compose() {
|
||||||
|
header "Container mit Docker Compose starten"
|
||||||
|
|
||||||
|
# Erstellen der vereinfachten docker-compose.yml-Datei
|
||||||
|
local compose_file="$DOCKER_DIR/compose.simple.yml"
|
||||||
|
|
||||||
|
# Lade OAuth-Konfiguration aus /srv, falls noch nicht geschehen
|
||||||
|
if [ -z "$OAUTH_CLIENT_ID" ]; then
|
||||||
|
load_env_from_srv
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Docker-Verzeichnis erstellen, falls nicht vorhanden
|
||||||
|
mkdir -p "$DOCKER_DIR"
|
||||||
|
|
||||||
|
log "Erstelle vereinfachte Docker-Compose-Datei: $compose_file"
|
||||||
|
cat > "$compose_file" << EOL
|
||||||
|
services:
|
||||||
|
myp-rp:
|
||||||
|
image: ${IMAGE_NAME}
|
||||||
|
container_name: ${CONTAINER_NAME}
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_API_URL=${BACKEND_URL}
|
||||||
|
- NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}
|
||||||
|
- NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}
|
||||||
|
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
|
||||||
|
- OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
|
||||||
|
env_file: "${ENV_FILE_PATH}"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ${DB_VOLUME_DIR}:/app/db
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:3000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Prüfen, ob die Datei erstellt wurde
|
||||||
|
if [ ! -f "$compose_file" ]; then
|
||||||
|
error_log "Konnte Docker-Compose-Datei nicht erstellen."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stoppen des vorhandenen Containers
|
||||||
|
if docker ps -a | grep -q "$CONTAINER_NAME"; then
|
||||||
|
log "Stoppe und entferne existierenden Container..."
|
||||||
|
docker stop "$CONTAINER_NAME" &>/dev/null || true
|
||||||
|
docker rm "$CONTAINER_NAME" &>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Container starten
|
||||||
|
log "Starte Container..."
|
||||||
|
cd "$DOCKER_DIR" || return 1
|
||||||
|
|
||||||
|
if ! docker compose -f "$(basename "$compose_file")" up -d; then
|
||||||
|
# Versuche mit docker-compose, falls docker compose nicht funktioniert
|
||||||
|
if ! docker-compose -f "$(basename "$compose_file")" up -d; then
|
||||||
|
error_log "Fehler beim Starten des Containers."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Container erfolgreich gestartet: $CONTAINER_NAME"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Starten des Containers mit Docker Run
|
||||||
|
start_container_run() {
|
||||||
|
header "Container direkt starten"
|
||||||
|
|
||||||
|
# Stoppen des vorhandenen Containers
|
||||||
|
if docker ps -a | grep -q "$CONTAINER_NAME"; then
|
||||||
|
log "Stoppe und entferne existierenden Container..."
|
||||||
|
docker stop "$CONTAINER_NAME" &>/dev/null || true
|
||||||
|
docker rm "$CONTAINER_NAME" &>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lade OAuth-Konfiguration aus /srv, falls noch nicht geschehen
|
||||||
|
if [ -z "$OAUTH_CLIENT_ID" ]; then
|
||||||
|
load_env_from_srv
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Container starten
|
||||||
|
log "Starte Container mit 'docker run'..."
|
||||||
|
|
||||||
|
if ! docker run -d --name "$CONTAINER_NAME" \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-e "NEXT_PUBLIC_API_URL=$BACKEND_URL" \
|
||||||
|
-e "NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}" \
|
||||||
|
-e "NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}" \
|
||||||
|
-e "OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}" \
|
||||||
|
-e "OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}" \
|
||||||
|
--env-file "$ENV_FILE_PATH" \
|
||||||
|
-v "$DB_VOLUME_DIR:/app/db" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
"$IMAGE_NAME"; then
|
||||||
|
error_log "Fehler beim Starten des Containers."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Container erfolgreich gestartet: $CONTAINER_NAME"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion zum Starten der Anwendung ohne Docker
|
||||||
|
start_without_docker() {
|
||||||
|
header "Anwendung ohne Docker starten"
|
||||||
|
|
||||||
|
cd "$FRONTEND_DIR" || return 1
|
||||||
|
|
||||||
|
# Prüfen, ob Node.js und pnpm installiert sind
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
error_log "Node.js ist nicht installiert. Bitte installieren Sie Node.js zuerst."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v pnpm &> /dev/null; then
|
||||||
|
log "pnpm ist nicht installiert. Installiere pnpm..."
|
||||||
|
npm install -g pnpm
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
error_log "Fehler beim Installieren von pnpm."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Installiere Abhängigkeiten
|
||||||
|
log "Installiere Abhängigkeiten..."
|
||||||
|
if ! pnpm install; then
|
||||||
|
error_log "Fehler beim Installieren der Abhängigkeiten."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lade OAuth-Konfiguration aus /srv und konfiguriere die Backend-URL
|
||||||
|
load_env_from_srv
|
||||||
|
if ! configure_backend_url "$BACKEND_URL"; then
|
||||||
|
error_log "Fehler beim Konfigurieren der Backend-URL."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Baue und starte die Anwendung
|
||||||
|
log "Baue und starte die Anwendung..."
|
||||||
|
if ! pnpm build; then
|
||||||
|
log "${YELLOW}Warnung: Build fehlgeschlagen. Versuche, im Dev-Modus zu starten...${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Starte im Screen-Session, damit die Anwendung im Hintergrund läuft
|
||||||
|
if command -v screen &> /dev/null; then
|
||||||
|
log "Starte Anwendung in Screen-Session..."
|
||||||
|
screen -dmS myp-frontend bash -c "cd $FRONTEND_DIR && \
|
||||||
|
NEXT_PUBLIC_API_URL=$BACKEND_URL \
|
||||||
|
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME} \
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL} \
|
||||||
|
OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} \
|
||||||
|
OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} \
|
||||||
|
pnpm start || \
|
||||||
|
NEXT_PUBLIC_API_URL=$BACKEND_URL \
|
||||||
|
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME} \
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL} \
|
||||||
|
OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} \
|
||||||
|
OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} \
|
||||||
|
pnpm dev"
|
||||||
|
success_log "Anwendung im Hintergrund gestartet. Verbinden mit: screen -r myp-frontend"
|
||||||
|
else
|
||||||
|
log "${YELLOW}Screen ist nicht installiert. Starte Anwendung im Vordergrund...${NC}"
|
||||||
|
log "${YELLOW}Beenden mit Strg+C. Die Anwendung wird dann beendet.${NC}"
|
||||||
|
sleep 3
|
||||||
|
export NEXT_PUBLIC_API_URL="$BACKEND_URL"
|
||||||
|
export NEXT_PUBLIC_FRONTEND_URL="http://${FRONTEND_HOSTNAME}"
|
||||||
|
export NEXT_PUBLIC_OAUTH_CALLBACK_URL="${OAUTH_URL}"
|
||||||
|
export OAUTH_CLIENT_ID="${OAUTH_CLIENT_ID}"
|
||||||
|
export OAUTH_CLIENT_SECRET="${OAUTH_CLIENT_SECRET}"
|
||||||
|
pnpm start || pnpm dev
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Funktion für das Hauptmenü
|
||||||
|
main_menu() {
|
||||||
|
local choice
|
||||||
|
header "MYP Frontend Deployment"
|
||||||
|
echo "Bitte wählen Sie eine Option:"
|
||||||
|
echo ""
|
||||||
|
echo "1) Alles automatisch (Build, Deploy, Starten)"
|
||||||
|
echo "2) Docker-Image bauen"
|
||||||
|
echo "3) Docker-Image speichern"
|
||||||
|
echo "4) Docker-Image laden"
|
||||||
|
echo "5) Container mit Docker Compose starten"
|
||||||
|
echo "6) Container direkt mit Docker Run starten"
|
||||||
|
echo "7) Anwendung ohne Docker starten"
|
||||||
|
echo "8) Nur Backend-URL konfigurieren"
|
||||||
|
echo "9) Beenden"
|
||||||
|
echo ""
|
||||||
|
read -p "Ihre Wahl (1-9): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1) auto_deploy ;;
|
||||||
|
2) build_image ;;
|
||||||
|
3) save_image ;;
|
||||||
|
4) load_image ;;
|
||||||
|
5) configure_backend_url && start_container_compose ;;
|
||||||
|
6) configure_backend_url && start_container_run ;;
|
||||||
|
7) start_without_docker ;;
|
||||||
|
8) configure_backend_url ;;
|
||||||
|
9) log "Beende das Programm." && exit 0 ;;
|
||||||
|
*) error_log "Ungültige Auswahl. Bitte versuchen Sie es erneut." && main_menu ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Zurück zum Hauptmenü, es sei denn, der Benutzer hat das Programm beendet
|
||||||
|
if [ $choice -ne 9 ]; then
|
||||||
|
read -p "Drücken Sie Enter, um zum Hauptmenü zurückzukehren..."
|
||||||
|
main_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Automatischer Deployment-Workflow
|
||||||
|
auto_deploy() {
|
||||||
|
header "Automatisches Deployment"
|
||||||
|
log "Starte automatischen Deployment-Workflow..."
|
||||||
|
|
||||||
|
# Konfiguriere Backend-URL
|
||||||
|
if ! configure_backend_url; then
|
||||||
|
error_log "Fehler beim Konfigurieren der Backend-URL."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Versuche zunächst, das Image zu laden
|
||||||
|
local load_dir="$DOCKER_DIR/images"
|
||||||
|
local load_file="$load_dir/myp-frontend.tar"
|
||||||
|
|
||||||
|
if [ -f "$load_file" ]; then
|
||||||
|
log "Image-Datei gefunden. Versuche zu laden..."
|
||||||
|
if load_image; then
|
||||||
|
log "Image erfolgreich geladen. Überspringe Bauen."
|
||||||
|
else
|
||||||
|
log "Konnte Image nicht laden. Versuche zu bauen..."
|
||||||
|
if ! build_image; then
|
||||||
|
error_log "Automatisches Deployment fehlgeschlagen beim Bauen des Images."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Keine Image-Datei gefunden. Baue neues Image..."
|
||||||
|
if ! build_image; then
|
||||||
|
error_log "Automatisches Deployment fehlgeschlagen beim Bauen des Images."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Speichere das Image für zukünftige Verwendung
|
||||||
|
log "Speichere Image für zukünftige Verwendung..."
|
||||||
|
save_image
|
||||||
|
|
||||||
|
# Starte den Container
|
||||||
|
log "Starte Container..."
|
||||||
|
if ! start_container_compose; then
|
||||||
|
error_log "Konnte Container nicht mit Docker Compose starten. Versuche direkten Start..."
|
||||||
|
if ! start_container_run; then
|
||||||
|
error_log "Automatisches Deployment fehlgeschlagen beim Starten des Containers."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
success_log "Automatisches Deployment erfolgreich abgeschlossen!"
|
||||||
|
log "Frontend ist unter http://localhost:3000 erreichbar"
|
||||||
|
log "API-Kommunikation mit Backend: $BACKEND_URL"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hauptanwendung
|
||||||
|
# Zuerst nach Backend-URL fragen
|
||||||
|
header "Backend-URL Konfiguration"
|
||||||
|
log "Standard-Backend-URL: $DEFAULT_BACKEND_URL"
|
||||||
|
read -p "Möchten Sie eine andere Backend-URL verwenden? (j/n): " change_url_choice
|
||||||
|
|
||||||
|
if [[ "$change_url_choice" == "j" ]]; then
|
||||||
|
read -p "Geben Sie die neue Backend-URL ein (z.B. http://192.168.0.105:5000): " custom_url
|
||||||
|
if [ -n "$custom_url" ]; then
|
||||||
|
BACKEND_URL="$custom_url"
|
||||||
|
log "Verwende benutzerdefinierte Backend-URL: $BACKEND_URL"
|
||||||
|
else
|
||||||
|
BACKEND_URL="$DEFAULT_BACKEND_URL"
|
||||||
|
log "Leere Eingabe. Verwende Standard-Backend-URL: $BACKEND_URL"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
BACKEND_URL="$DEFAULT_BACKEND_URL"
|
||||||
|
log "Verwende Standard-Backend-URL: $BACKEND_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Anzeigen des Hauptmenüs
|
||||||
|
main_menu
|
26
frontend/next.config.mjs
Normal file
26
frontend/next.config.mjs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/:path*",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "Access-Control-Allow-Origin",
|
||||||
|
value: "m040tbaraspi001.de040.corpintra.net",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Access-Control-Allow-Methods",
|
||||||
|
value: "GET, POST, PUT, DELETE, OPTIONS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Access-Control-Allow-Headers",
|
||||||
|
value: "Content-Type, Authorization",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
83
frontend/package.json
Normal file
83
frontend/package.json
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"name": "myp-rp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@9.12.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "node update-package.js && next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"db:create-default": "mkdir -p db/",
|
||||||
|
"db:generate-sqlite": "pnpm drizzle-kit generate",
|
||||||
|
"db:clean": "rm -rf db/ drizzle/",
|
||||||
|
"db:migrate": "pnpm drizzle-kit migrate",
|
||||||
|
"db": "pnpm db:create-default && pnpm db:generate-sqlite && pnpm db:migrate",
|
||||||
|
"db:reset": "pnpm db:clean && pnpm db"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@faker-js/faker": "^9.2.0",
|
||||||
|
"@headlessui/react": "^2.1.10",
|
||||||
|
"@headlessui/tailwindcss": "^0.2.1",
|
||||||
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@libsql/client": "^0.14.0",
|
||||||
|
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.2",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||||
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
|
"@remixicon/react": "^4.3.0",
|
||||||
|
"@tanstack/react-table": "^8.20.5",
|
||||||
|
"@tremor/react": "^3.18.3",
|
||||||
|
"arctic": "^1.9.2",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"drizzle-orm": "^0.30.10",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lucia": "^3.2.1",
|
||||||
|
"lucide-react": "^0.378.0",
|
||||||
|
"luxon": "^3.5.0",
|
||||||
|
"next": "14.2.3",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
|
"oslo": "^1.2.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.53.0",
|
||||||
|
"react-if": "^4.1.5",
|
||||||
|
"react-timer-hook": "^3.0.7",
|
||||||
|
"recharts": "^2.13.3",
|
||||||
|
"regression": "^2.0.1",
|
||||||
|
"sonner": "^1.5.0",
|
||||||
|
"sqlite": "^5.1.1",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
|
"swr": "^2.2.5",
|
||||||
|
"tailwind-merge": "^2.5.3",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"use-debounce": "^10.0.3",
|
||||||
|
"uuid": "^11.0.2",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.3",
|
||||||
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
|
"@types/lodash": "^4.17.13",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
|
"@types/node": "^20.16.11",
|
||||||
|
"@types/react": "^18.3.11",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"drizzle-kit": "^0.21.4",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"tailwindcss": "^3.4.13",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
}
|
||||||
|
}
|
5707
frontend/pnpm-lock.yaml
generated
Normal file
5707
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
frontend/postcss.config.mjs
Normal file
8
frontend/postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
1
frontend/public/next.svg
Normal file
1
frontend/public/next.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/public/vercel.svg
Normal file
1
frontend/public/vercel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
After Width: | Height: | Size: 629 B |
9279
frontend/repomix-output.txt
Normal file
9279
frontend/repomix-output.txt
Normal file
File diff suppressed because it is too large
Load Diff
367
frontend/scripts/generate-data.js
Normal file
367
frontend/scripts/generate-data.js
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
const sqlite3 = require("sqlite3");
|
||||||
|
const faker = require("@faker-js/faker").faker;
|
||||||
|
const { random, sample, sampleSize, sum } = require("lodash");
|
||||||
|
const { DateTime } = require("luxon");
|
||||||
|
const { open } = require("sqlite");
|
||||||
|
const { v4: uuidv4 } = require("uuid");
|
||||||
|
|
||||||
|
const dbPath = "./db/sqlite.db";
|
||||||
|
|
||||||
|
// Configuration for test data generation
|
||||||
|
let startDate = DateTime.fromISO("2024-10-08");
|
||||||
|
let endDate = DateTime.fromISO("2024-11-08");
|
||||||
|
let numberOfPrinters = 5;
|
||||||
|
|
||||||
|
// Use weekday names for better readability and ease of setting trends
|
||||||
|
let avgPrintTimesPerDay = {
|
||||||
|
Monday: 4,
|
||||||
|
Tuesday: 2,
|
||||||
|
Wednesday: 5,
|
||||||
|
Thursday: 2,
|
||||||
|
Friday: 3,
|
||||||
|
Saturday: 0,
|
||||||
|
Sunday: 0,
|
||||||
|
}; // Average number of prints for each weekday
|
||||||
|
|
||||||
|
let avgPrintDurationPerDay = {
|
||||||
|
Monday: 240, // Total average duration in minutes for Monday
|
||||||
|
Tuesday: 30,
|
||||||
|
Wednesday: 45,
|
||||||
|
Thursday: 40,
|
||||||
|
Friday: 120,
|
||||||
|
Saturday: 0,
|
||||||
|
Sunday: 0,
|
||||||
|
}; // Average total duration of prints for each weekday
|
||||||
|
|
||||||
|
let printerUsage = {
|
||||||
|
"Drucker 1": 0.5,
|
||||||
|
"Drucker 2": 0.7,
|
||||||
|
"Drucker 3": 0.6,
|
||||||
|
"Drucker 4": 0.3,
|
||||||
|
"Drucker 5": 0.4,
|
||||||
|
}; // Usage percentages for each printer
|
||||||
|
|
||||||
|
// **New Configurations for Error Rates**
|
||||||
|
let generalErrorRate = 0.05; // 5% chance any print job may fail
|
||||||
|
let printerErrorRates = {
|
||||||
|
"Drucker 1": 0.02, // 2% error rate for Printer 1
|
||||||
|
"Drucker 2": 0.03,
|
||||||
|
"Drucker 3": 0.01,
|
||||||
|
"Drucker 4": 0.05,
|
||||||
|
"Drucker 5": 0.04,
|
||||||
|
}; // Error rates for each printer
|
||||||
|
|
||||||
|
const holidays = []; // Example holidays
|
||||||
|
const existingJobs = [];
|
||||||
|
|
||||||
|
const initDB = async () => {
|
||||||
|
console.log("Initializing database connection...");
|
||||||
|
return open({
|
||||||
|
filename: dbPath,
|
||||||
|
driver: sqlite3.Database,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUser = (isPowerUser = false) => {
|
||||||
|
const name = [faker.person.firstName(), faker.person.lastName()];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
id: uuidv4(),
|
||||||
|
github_id: faker.number.int(),
|
||||||
|
username: `${name[0].slice(0, 2)}${name[1].slice(0, 6)}`.toUpperCase(),
|
||||||
|
displayName: `${name[0]} ${name[1]}`.toUpperCase(),
|
||||||
|
email: `${name[0]}.${name[1]}@example.com`,
|
||||||
|
role: sample(["user", "admin"]),
|
||||||
|
isPowerUser,
|
||||||
|
};
|
||||||
|
console.log("Created user:", user);
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPrinter = (index) => {
|
||||||
|
const printer = {
|
||||||
|
id: uuidv4(),
|
||||||
|
name: `Drucker ${index}`,
|
||||||
|
description: faker.lorem.sentence(),
|
||||||
|
status: random(0, 2),
|
||||||
|
};
|
||||||
|
console.log("Created printer:", printer);
|
||||||
|
return printer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPrinterAvailable = (printer, startAt, duration) => {
|
||||||
|
const endAt = startAt + duration * 60 * 1000; // Convert minutes to milliseconds
|
||||||
|
return !existingJobs.some((job) => {
|
||||||
|
const jobStart = job.startAt;
|
||||||
|
const jobEnd = job.startAt + job.durationInMinutes * 60 * 1000;
|
||||||
|
return (
|
||||||
|
printer.id === job.printerId &&
|
||||||
|
((startAt >= jobStart && startAt < jobEnd) ||
|
||||||
|
(endAt > jobStart && endAt <= jobEnd) ||
|
||||||
|
(startAt <= jobStart && endAt >= jobEnd))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPrintJob = (users, printers, startAt, duration) => {
|
||||||
|
const user = sample(users);
|
||||||
|
let printer;
|
||||||
|
|
||||||
|
// Weighted selection based on printer usage
|
||||||
|
const printerNames = Object.keys(printerUsage);
|
||||||
|
const weightedPrinters = printers.filter((p) => printerNames.includes(p.name));
|
||||||
|
|
||||||
|
// Create a weighted array of printers based on usage percentages
|
||||||
|
const printerWeights = weightedPrinters.map((p) => ({
|
||||||
|
printer: p,
|
||||||
|
weight: printerUsage[p.name],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totalWeight = sum(printerWeights.map((pw) => pw.weight));
|
||||||
|
const randomWeight = Math.random() * totalWeight;
|
||||||
|
let accumulatedWeight = 0;
|
||||||
|
for (const pw of printerWeights) {
|
||||||
|
accumulatedWeight += pw.weight;
|
||||||
|
if (randomWeight <= accumulatedWeight) {
|
||||||
|
printer = pw.printer;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!printer) {
|
||||||
|
printer = sample(printers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPrinterAvailable(printer, startAt, duration)) {
|
||||||
|
console.log("Printer not available, skipping job creation.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// **Determine if the job should be aborted based on error rates**
|
||||||
|
let aborted = false;
|
||||||
|
let abortReason = null;
|
||||||
|
|
||||||
|
// Calculate the combined error rate
|
||||||
|
const printerErrorRate = printerErrorRates[printer.name] || 0;
|
||||||
|
const combinedErrorRate = 1 - (1 - generalErrorRate) * (1 - printerErrorRate);
|
||||||
|
|
||||||
|
if (Math.random() < combinedErrorRate) {
|
||||||
|
aborted = true;
|
||||||
|
const errorMessages = [
|
||||||
|
"Unbekannt",
|
||||||
|
"Keine Ahnung",
|
||||||
|
"Falsch gebucht",
|
||||||
|
"Filament gelöst",
|
||||||
|
"Druckabbruch",
|
||||||
|
"Düsenverstopfung",
|
||||||
|
"Schichthaftung fehlgeschlagen",
|
||||||
|
"Materialmangel",
|
||||||
|
"Dateifehler",
|
||||||
|
"Temperaturproblem",
|
||||||
|
"Mechanischer Fehler",
|
||||||
|
"Softwarefehler",
|
||||||
|
"Kalibrierungsfehler",
|
||||||
|
"Überhitzung",
|
||||||
|
];
|
||||||
|
abortReason = sample(errorMessages); // Generate a random abort reason
|
||||||
|
}
|
||||||
|
|
||||||
|
const printJob = {
|
||||||
|
id: uuidv4(),
|
||||||
|
printerId: printer.id,
|
||||||
|
userId: user.id,
|
||||||
|
startAt,
|
||||||
|
durationInMinutes: duration,
|
||||||
|
comments: faker.lorem.sentence(),
|
||||||
|
aborted,
|
||||||
|
abortReason,
|
||||||
|
};
|
||||||
|
console.log("Created print job:", printJob);
|
||||||
|
return printJob;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePrintJobsForDay = async (users, printers, dayDate, totalJobsForDay, totalDurationForDay, db, dryRun) => {
|
||||||
|
console.log(`Generating print jobs for ${dayDate.toISODate()}...`);
|
||||||
|
|
||||||
|
// Generate random durations that sum up approximately to totalDurationForDay
|
||||||
|
const durations = [];
|
||||||
|
let remainingDuration = totalDurationForDay;
|
||||||
|
for (let i = 0; i < totalJobsForDay; i++) {
|
||||||
|
const avgJobDuration = remainingDuration / (totalJobsForDay - i);
|
||||||
|
const jobDuration = Math.max(
|
||||||
|
Math.round(random(avgJobDuration * 0.8, avgJobDuration * 1.2)),
|
||||||
|
5, // Minimum duration of 5 minutes
|
||||||
|
);
|
||||||
|
durations.push(jobDuration);
|
||||||
|
remainingDuration -= jobDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle durations to randomize job lengths
|
||||||
|
const shuffledDurations = sampleSize(durations, durations.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < totalJobsForDay; i++) {
|
||||||
|
const duration = shuffledDurations[i];
|
||||||
|
|
||||||
|
// Random start time between 8 AM and 6 PM, adjusted to avoid overlapping durations
|
||||||
|
const possibleStartHours = Array.from({ length: 10 }, (_, idx) => idx + 8); // 8 AM to 6 PM
|
||||||
|
let startAt;
|
||||||
|
let attempts = 0;
|
||||||
|
do {
|
||||||
|
const hour = sample(possibleStartHours);
|
||||||
|
const minute = random(0, 59);
|
||||||
|
startAt = dayDate.set({ hour, minute, second: 0, millisecond: 0 }).toMillis();
|
||||||
|
attempts++;
|
||||||
|
if (attempts > 10) {
|
||||||
|
console.log("Unable to find available time slot, skipping job.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (!isPrinterAvailable(sample(printers), startAt, duration));
|
||||||
|
|
||||||
|
if (attempts > 10) continue;
|
||||||
|
|
||||||
|
const printJob = createPrintJob(users, printers, startAt, duration);
|
||||||
|
if (printJob) {
|
||||||
|
if (!dryRun) {
|
||||||
|
await db.run(
|
||||||
|
`INSERT INTO printJob (id, printerId, userId, startAt, durationInMinutes, comments, aborted, abortReason)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
[
|
||||||
|
printJob.id,
|
||||||
|
printJob.printerId,
|
||||||
|
printJob.userId,
|
||||||
|
printJob.startAt,
|
||||||
|
printJob.durationInMinutes,
|
||||||
|
printJob.comments,
|
||||||
|
printJob.aborted ? 1 : 0,
|
||||||
|
printJob.abortReason,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
existingJobs.push(printJob);
|
||||||
|
console.log("Inserted print job into database:", printJob.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateTestData = async (dryRun = false) => {
|
||||||
|
console.log("Starting test data generation...");
|
||||||
|
const db = await initDB();
|
||||||
|
|
||||||
|
// Generate users and printers
|
||||||
|
const users = [
|
||||||
|
...Array.from({ length: 7 }, () => createUser(false)),
|
||||||
|
...Array.from({ length: 3 }, () => createUser(true)),
|
||||||
|
];
|
||||||
|
const printers = Array.from({ length: numberOfPrinters }, (_, index) => createPrinter(index + 1));
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
// Insert users into the database
|
||||||
|
for (const user of users) {
|
||||||
|
await db.run(
|
||||||
|
`INSERT INTO user (id, github_id, name, displayName, email, role)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
|
[user.id, user.github_id, user.username, user.displayName, user.email, user.role],
|
||||||
|
);
|
||||||
|
console.log("Inserted user into database:", user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert printers into the database
|
||||||
|
for (const printer of printers) {
|
||||||
|
await db.run(
|
||||||
|
`INSERT INTO printer (id, name, description, status)
|
||||||
|
VALUES (?, ?, ?, ?)`,
|
||||||
|
[printer.id, printer.name, printer.description, printer.status],
|
||||||
|
);
|
||||||
|
console.log("Inserted printer into database:", printer.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate print jobs for each day within the specified date range
|
||||||
|
let currentDay = startDate;
|
||||||
|
while (currentDay <= endDate) {
|
||||||
|
const weekdayName = currentDay.toFormat("EEEE"); // Get weekday name (e.g., 'Monday')
|
||||||
|
if (holidays.includes(currentDay.toISODate()) || avgPrintTimesPerDay[weekdayName] === 0) {
|
||||||
|
console.log(`Skipping holiday or no jobs scheduled: ${currentDay.toISODate()}`);
|
||||||
|
currentDay = currentDay.plus({ days: 1 });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalJobsForDay = avgPrintTimesPerDay[weekdayName];
|
||||||
|
const totalDurationForDay = avgPrintDurationPerDay[weekdayName];
|
||||||
|
|
||||||
|
await generatePrintJobsForDay(users, printers, currentDay, totalJobsForDay, totalDurationForDay, db, dryRun);
|
||||||
|
currentDay = currentDay.plus({ days: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
await db.close();
|
||||||
|
console.log("Database connection closed. Test data generation complete.");
|
||||||
|
} else {
|
||||||
|
console.log("Dry run complete. No data was written to the database.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setConfigurations = (config) => {
|
||||||
|
if (config.startDate) startDate = DateTime.fromISO(config.startDate);
|
||||||
|
if (config.endDate) endDate = DateTime.fromISO(config.endDate);
|
||||||
|
if (config.numberOfPrinters) numberOfPrinters = config.numberOfPrinters;
|
||||||
|
if (config.avgPrintTimesPerDay) avgPrintTimesPerDay = config.avgPrintTimesPerDay;
|
||||||
|
if (config.avgPrintDurationPerDay) avgPrintDurationPerDay = config.avgPrintDurationPerDay;
|
||||||
|
if (config.printerUsage) printerUsage = config.printerUsage;
|
||||||
|
if (config.generalErrorRate !== undefined) generalErrorRate = config.generalErrorRate;
|
||||||
|
if (config.printerErrorRates) printerErrorRates = config.printerErrorRates;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example usage
|
||||||
|
setConfigurations({
|
||||||
|
startDate: "2024-10-08",
|
||||||
|
endDate: "2024-11-08",
|
||||||
|
numberOfPrinters: 6,
|
||||||
|
avgPrintTimesPerDay: {
|
||||||
|
Monday: 4, // High usage
|
||||||
|
Tuesday: 2, // Low usage
|
||||||
|
Wednesday: 3, // Low usage
|
||||||
|
Thursday: 2, // Low usage
|
||||||
|
Friday: 8, // High usage
|
||||||
|
Saturday: 0,
|
||||||
|
Sunday: 0,
|
||||||
|
},
|
||||||
|
avgPrintDurationPerDay: {
|
||||||
|
Monday: 300, // High total duration
|
||||||
|
Tuesday: 60, // Low total duration
|
||||||
|
Wednesday: 90,
|
||||||
|
Thursday: 60,
|
||||||
|
Friday: 240,
|
||||||
|
Saturday: 0,
|
||||||
|
Sunday: 0,
|
||||||
|
},
|
||||||
|
printerUsage: {
|
||||||
|
"Drucker 1": 2.3,
|
||||||
|
"Drucker 2": 1.7,
|
||||||
|
"Drucker 3": 0.1,
|
||||||
|
"Drucker 4": 1.5,
|
||||||
|
"Drucker 5": 2.4,
|
||||||
|
"Drucker 6": 0.3,
|
||||||
|
"Drucker 7": 0.9,
|
||||||
|
"Drucker 8": 0.1,
|
||||||
|
},
|
||||||
|
generalErrorRate: 0.05, // 5% general error rate
|
||||||
|
printerErrorRates: {
|
||||||
|
"Drucker 1": 0.02,
|
||||||
|
"Drucker 2": 0.03,
|
||||||
|
"Drucker 3": 0.1,
|
||||||
|
"Drucker 4": 0.05,
|
||||||
|
"Drucker 5": 0.04,
|
||||||
|
"Drucker 6": 0.02,
|
||||||
|
"Drucker 7": 0.01,
|
||||||
|
"PrinteDrucker 8": 0.03,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
generateTestData(process.argv.includes("--dry-run"))
|
||||||
|
.then(() => {
|
||||||
|
console.log("Test data generation script finished.");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error generating test data:", err);
|
||||||
|
});
|
82
frontend/setup-backend-url.sh
Normal file
82
frontend/setup-backend-url.sh
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Skript zum Setzen der Backend-URL in der Frontend-Konfiguration
|
||||||
|
# Verwendet für die Verbindung zum Backend-Server unter 192.168.0.105:5000
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# Definiere Variablen
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
ENV_FILE="$SCRIPT_DIR/.env.local"
|
||||||
|
DEFAULT_BACKEND_URL="http://192.168.0.105:5000"
|
||||||
|
|
||||||
|
# Falls übergebene Parameter vorhanden sind, Backend-URL anpassen
|
||||||
|
if [ -n "$1" ]; then
|
||||||
|
BACKEND_URL="$1"
|
||||||
|
log "Verwende übergebene Backend-URL: ${BACKEND_URL}"
|
||||||
|
else
|
||||||
|
BACKEND_URL="$DEFAULT_BACKEND_URL"
|
||||||
|
log "Verwende Standard-Backend-URL: ${BACKEND_URL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bestimme den Hostnamen für OAuth
|
||||||
|
HOSTNAME=$(hostname)
|
||||||
|
if [[ "$HOSTNAME" == *"m040tbaraspi001"* ]] || [[ "$HOSTNAME" == *"corpintra"* ]]; then
|
||||||
|
FRONTEND_HOSTNAME="m040tbaraspi001.de040.corpintra.net"
|
||||||
|
OAUTH_URL="http://m040tbaraspi001.de040.corpintra.net/auth/login/callback"
|
||||||
|
log "Erkannt: Unternehmens-Hostname: $FRONTEND_HOSTNAME"
|
||||||
|
else
|
||||||
|
FRONTEND_HOSTNAME="$HOSTNAME"
|
||||||
|
OAUTH_URL="http://$HOSTNAME:3000/auth/login/callback"
|
||||||
|
log "Lokaler Hostname: $FRONTEND_HOSTNAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle .env.local Datei mit Backend-URL
|
||||||
|
log "${YELLOW}Erstelle .env.local Datei...${NC}"
|
||||||
|
cat > "$ENV_FILE" << EOL
|
||||||
|
# Backend API Konfiguration
|
||||||
|
NEXT_PUBLIC_API_URL=${BACKEND_URL}
|
||||||
|
|
||||||
|
# Frontend-URL für OAuth Callback
|
||||||
|
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}
|
||||||
|
|
||||||
|
# Explizite OAuth Callback URL für GitHub
|
||||||
|
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}
|
||||||
|
|
||||||
|
# OAuth Konfiguration (falls nötig)
|
||||||
|
OAUTH_CLIENT_ID=client_id
|
||||||
|
OAUTH_CLIENT_SECRET=client_secret
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Überprüfe, ob die Datei erstellt wurde
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
log "${GREEN}Erfolgreich .env.local Datei mit Backend-URL erstellt: ${BACKEND_URL}${NC}"
|
||||||
|
else
|
||||||
|
error_log "Konnte .env.local Datei nicht erstellen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Hinweis für Docker-Installation
|
||||||
|
log "${YELLOW}WICHTIG: Wenn Sie Docker verwenden, stellen Sie sicher, dass Sie die Umgebungsvariable setzen:${NC}"
|
||||||
|
log "NEXT_PUBLIC_API_URL=${BACKEND_URL}"
|
||||||
|
log ""
|
||||||
|
log "${GREEN}Backend-URL wurde erfolgreich konfiguriert. Nach einem Neustart der Anwendung sollte die Verbindung hergestellt werden.${NC}"
|
||||||
|
|
||||||
|
# Berechtigungen setzen
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
|
||||||
|
exit 0
|
32
frontend/src/app/admin/about/page.tsx
Normal file
32
frontend/src/app/admin/about/page.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Über MYP",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function AdminPage() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Über MYP</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
<i className="italic">MYP — Manage Your Printer</i>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="gap-y-2 flex flex-col">
|
||||||
|
<p className="max-w-[80ch]">
|
||||||
|
<strong>MYP</strong> ist eine Webanwendung zur Reservierung von 3D-Druckern. Sie wurde im Rahmen des
|
||||||
|
Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische
|
||||||
|
Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
© 2024{" "}
|
||||||
|
<a href="https://linkedin.com/in/torben-haack" target="_blank" rel="noreferrer">
|
||||||
|
Torben Haack
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
66
frontend/src/app/admin/admin-sidebar.tsx
Normal file
66
frontend/src/app/admin/admin-sidebar.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/utils/styles";
|
||||||
|
import { FileIcon, HeartIcon, LayoutDashboardIcon, PrinterIcon, UsersIcon, WrenchIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
interface AdminSite {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminSidebar() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const adminSites: AdminSite[] = [
|
||||||
|
{
|
||||||
|
name: "Dashboard",
|
||||||
|
path: "/admin",
|
||||||
|
icon: <LayoutDashboardIcon className="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Benutzer",
|
||||||
|
path: "/admin/users",
|
||||||
|
icon: <UsersIcon className="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Drucker",
|
||||||
|
path: "/admin/printers",
|
||||||
|
icon: <PrinterIcon className="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Druckaufträge",
|
||||||
|
path: "/admin/jobs",
|
||||||
|
icon: <FileIcon className="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Einstellungen",
|
||||||
|
path: "/admin/settings",
|
||||||
|
icon: <WrenchIcon className="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Über MYP",
|
||||||
|
path: "/admin/about",
|
||||||
|
icon: <HeartIcon className="w-4 h-4" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="w-full">
|
||||||
|
{adminSites.map((site) => (
|
||||||
|
<li key={site.path}>
|
||||||
|
<Link
|
||||||
|
href={site.path}
|
||||||
|
className={cn("flex items-center gap-2 p-2 rounded hover:bg-muted", {
|
||||||
|
"font-semibold": pathname === site.path,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{site.icon}
|
||||||
|
<span>{site.name}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
68
frontend/src/app/admin/charts/printer-error-chart.tsx
Normal file
68
frontend/src/app/admin/charts/printer-error-chart.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||||
|
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts";
|
||||||
|
|
||||||
|
export const description = "Ein Säulendiagramm zur Darstellung der Abbruchgründe und ihrer Häufigkeit";
|
||||||
|
|
||||||
|
interface AbortReasonCountChartProps {
|
||||||
|
abortReasonCount: {
|
||||||
|
abortReason: string;
|
||||||
|
count: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
abortReason: {
|
||||||
|
label: "Abbruchgrund",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
export function AbortReasonCountChart({ abortReasonCount }: AbortReasonCountChartProps) {
|
||||||
|
// Transform data to fit the chart structure
|
||||||
|
const chartData = abortReasonCount.map((reason) => ({
|
||||||
|
abortReason: reason.abortReason,
|
||||||
|
count: reason.count,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Abbruchgründe</CardTitle>
|
||||||
|
<CardDescription>Häufigkeit der Abbruchgründe für Druckaufträge</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer config={chartConfig}>
|
||||||
|
<BarChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={chartData}
|
||||||
|
margin={{
|
||||||
|
top: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="abortReason"
|
||||||
|
tickLine={false}
|
||||||
|
tickMargin={10}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => value}
|
||||||
|
/>
|
||||||
|
<YAxis tickFormatter={(value) => `${value}`} />
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||||
|
<Bar dataKey="count" fill="hsl(var(--chart-1))" radius={8}>
|
||||||
|
<LabelList
|
||||||
|
position="top"
|
||||||
|
offset={12}
|
||||||
|
className="fill-foreground"
|
||||||
|
fontSize={12}
|
||||||
|
formatter={(value: number) => `${value}`}
|
||||||
|
/>
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
66
frontend/src/app/admin/charts/printer-error-rate.tsx
Normal file
66
frontend/src/app/admin/charts/printer-error-rate.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"use client";
|
||||||
|
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||||
|
import type { PrinterErrorRate } from "@/utils/analytics/error-rate";
|
||||||
|
|
||||||
|
export const description = "Ein Säulendiagramm zur Darstellung der Fehlerrate";
|
||||||
|
|
||||||
|
interface PrinterErrorRateChartProps {
|
||||||
|
printerErrorRate: PrinterErrorRate[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
errorRate: {
|
||||||
|
label: "Fehlerrate",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
export function PrinterErrorRateChart({ printerErrorRate }: PrinterErrorRateChartProps) {
|
||||||
|
// Transform data to fit the chart structure
|
||||||
|
const chartData = printerErrorRate.map((printer) => ({
|
||||||
|
printer: printer.name,
|
||||||
|
errorRate: printer.errorRate,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Fehlerrate</CardTitle>
|
||||||
|
<CardDescription>Fehlerrate der Drucker in Prozent</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer config={chartConfig}>
|
||||||
|
<BarChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={chartData}
|
||||||
|
margin={{
|
||||||
|
top: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="printer"
|
||||||
|
tickLine={false}
|
||||||
|
tickMargin={10}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => value}
|
||||||
|
/>
|
||||||
|
<YAxis tickFormatter={(value) => `${value}%`} />
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||||
|
<Bar dataKey="errorRate" fill="hsl(var(--chart-1))" radius={8}>
|
||||||
|
<LabelList
|
||||||
|
position="top"
|
||||||
|
offset={12}
|
||||||
|
className="fill-foreground"
|
||||||
|
fontSize={12}
|
||||||
|
formatter={(value: number) => `${value}%`}
|
||||||
|
/>
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
83
frontend/src/app/admin/charts/printer-forecast.tsx
Normal file
83
frontend/src/app/admin/charts/printer-forecast.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||||
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||||
|
|
||||||
|
export const description = "Ein Bereichsdiagramm zur Darstellung der prognostizierten Nutzung pro Wochentag";
|
||||||
|
|
||||||
|
interface ForecastData {
|
||||||
|
day: number; // 0 for Sunday, 1 for Monday, ..., 6 for Saturday
|
||||||
|
usageMinutes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForecastChartProps {
|
||||||
|
forecastData: ForecastData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
usage: {
|
||||||
|
label: "Prognostizierte Nutzung",
|
||||||
|
color: "hsl(var(--chart-1))",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
const daysOfWeek = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
|
||||||
|
|
||||||
|
export function ForecastPrinterUsageChart({ forecastData }: ForecastChartProps) {
|
||||||
|
// Transform and slice data to fit the chart structure
|
||||||
|
const chartData = forecastData.map((data) => ({
|
||||||
|
//slice(1, forecastData.length - 1).
|
||||||
|
day: daysOfWeek[data.day], // Map day number to weekday name
|
||||||
|
usage: data.usageMinutes,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Prognostizierte Nutzung pro Wochentag</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer className="h-64 w-full" config={chartConfig}>
|
||||||
|
<AreaChart accessibilityLayer data={chartData} margin={{ left: 12, right: 12, top: 12 }}>
|
||||||
|
<CartesianGrid vertical={true} />
|
||||||
|
<XAxis dataKey="day" type="category" tickLine={true} tickMargin={10} axisLine={false} />
|
||||||
|
<YAxis type="number" dataKey="usage" tickLine={false} tickMargin={10} axisLine={false} />
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||||
|
<Area
|
||||||
|
dataKey="usage"
|
||||||
|
type="step"
|
||||||
|
fill="hsl(var(--chart-1))"
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke="hsl(var(--chart-1))"
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||||
|
<div className="flex items-center gap-2 font-medium leading-none">
|
||||||
|
Zeigt die prognostizierte Nutzungszeit pro Wochentag in Minuten.
|
||||||
|
</div>
|
||||||
|
<div className="leading-none text-muted-foreground">
|
||||||
|
Besten Tage zur Wartung: {bestMaintenanceDays(forecastData)}
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bestMaintenanceDays(forecastData: ForecastData[]) {
|
||||||
|
const sortedData = forecastData.map((a) => a).sort((a, b) => a.usageMinutes - b.usageMinutes); // Sort ascending
|
||||||
|
|
||||||
|
const q1Index = Math.floor(sortedData.length * 0.33);
|
||||||
|
const q1 = sortedData[q1Index].usageMinutes; // First quartile (Q1) value
|
||||||
|
|
||||||
|
const filteredData = sortedData.filter((data) => data.usageMinutes <= q1);
|
||||||
|
|
||||||
|
return filteredData
|
||||||
|
.map((data) => {
|
||||||
|
const days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
|
||||||
|
return days[data.day];
|
||||||
|
})
|
||||||
|
.join(", ");
|
||||||
|
}
|
80
frontend/src/app/admin/charts/printer-utilization.tsx
Normal file
80
frontend/src/app/admin/charts/printer-utilization.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { TrendingUp } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Label, Pie, PieChart } from "recharts";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||||
|
|
||||||
|
export const description = "Nutzung des Druckers";
|
||||||
|
|
||||||
|
interface ComponentProps {
|
||||||
|
data: {
|
||||||
|
printerId: string;
|
||||||
|
utilizationPercentage: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartConfig = {} satisfies ChartConfig;
|
||||||
|
|
||||||
|
export function PrinterUtilizationChart({ data }: ComponentProps) {
|
||||||
|
const totalUtilization = React.useMemo(() => data.utilizationPercentage, [data]);
|
||||||
|
const dataWithColor = {
|
||||||
|
...data,
|
||||||
|
fill: "rgb(34 197 94)",
|
||||||
|
};
|
||||||
|
const free = {
|
||||||
|
printerId: "-",
|
||||||
|
utilizationPercentage: 1 - data.utilizationPercentage,
|
||||||
|
name: "(Frei)",
|
||||||
|
fill: "rgb(212 212 212)",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="flex flex-col">
|
||||||
|
<CardHeader className="items-center pb-0">
|
||||||
|
<CardTitle>{data.name}</CardTitle>
|
||||||
|
<CardDescription>Nutzung des ausgewählten Druckers</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-1 pb-0">
|
||||||
|
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||||
|
<PieChart>
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||||
|
<Pie
|
||||||
|
data={[dataWithColor, free]}
|
||||||
|
dataKey="utilizationPercentage"
|
||||||
|
nameKey="name"
|
||||||
|
innerRadius={60}
|
||||||
|
strokeWidth={5}
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
content={({ viewBox }) => {
|
||||||
|
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||||
|
return (
|
||||||
|
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
|
||||||
|
<tspan x={viewBox.cx} y={viewBox.cy} className="fill-foreground text-3xl font-bold">
|
||||||
|
{(totalUtilization * 100).toFixed(2)}%
|
||||||
|
</tspan>
|
||||||
|
<tspan x={viewBox.cx} y={(viewBox.cy || 0) + 24} className="fill-muted-foreground">
|
||||||
|
Gesamt-Nutzung
|
||||||
|
</tspan>
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex-col gap-2 text-sm">
|
||||||
|
<div className="flex items-center gap-2 font-medium leading-none">
|
||||||
|
Übersicht der Nutzung <TrendingUp className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div className="leading-none text-muted-foreground">Aktuelle Auslastung des Druckers</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
69
frontend/src/app/admin/charts/printer-volume.tsx
Normal file
69
frontend/src/app/admin/charts/printer-volume.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from "recharts";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||||
|
|
||||||
|
export const description = "Ein Balkendiagramm mit Beschriftung";
|
||||||
|
|
||||||
|
interface PrintVolumes {
|
||||||
|
today: number;
|
||||||
|
thisWeek: number;
|
||||||
|
thisMonth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
volume: {
|
||||||
|
label: "Volumen",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
interface PrinterVolumeChartProps {
|
||||||
|
printerVolume: PrintVolumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PrinterVolumeChart({ printerVolume }: PrinterVolumeChartProps) {
|
||||||
|
const chartData = [
|
||||||
|
{ period: "Heute", volume: printerVolume.today, color: "hsl(var(--chart-1))" },
|
||||||
|
{ period: "Diese Woche", volume: printerVolume.thisWeek, color: "hsl(var(--chart-2))" },
|
||||||
|
{ period: "Diesen Monat", volume: printerVolume.thisMonth, color: "hsl(var(--chart-3))" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Druckvolumen</CardTitle>
|
||||||
|
<CardDescription>Vergleich: Heute, Diese Woche, Diesen Monat</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer className="h-64 w-full" config={chartConfig}>
|
||||||
|
<BarChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={chartData}
|
||||||
|
margin={{
|
||||||
|
top: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="period"
|
||||||
|
tickLine={false}
|
||||||
|
tickMargin={10}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => value}
|
||||||
|
/>
|
||||||
|
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||||
|
<Bar dataKey="volume" fill="var(--color-volume)" radius={8}>
|
||||||
|
<LabelList position="top" offset={12} className="fill-foreground" fontSize={12} />
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||||
|
<div className="leading-none text-muted-foreground">
|
||||||
|
Zeigt das Druckvolumen für heute, diese Woche und diesen Monat
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
35
frontend/src/app/admin/jobs/page.tsx
Normal file
35
frontend/src/app/admin/jobs/page.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { columns } from "@/app/my/jobs/columns";
|
||||||
|
import { JobsTable } from "@/app/my/jobs/data-table";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { printJobs } from "@/server/db/schema";
|
||||||
|
import { desc } from "drizzle-orm";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Alle Druckaufträge",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function AdminJobsPage() {
|
||||||
|
const allJobs = await db.query.printJobs.findMany({
|
||||||
|
orderBy: [desc(printJobs.startAt)],
|
||||||
|
with: {
|
||||||
|
user: true,
|
||||||
|
printer: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Druckaufträge</CardTitle>
|
||||||
|
<CardDescription>Alle Druckaufträge</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<JobsTable columns={columns} data={allJobs} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
34
frontend/src/app/admin/layout.tsx
Normal file
34
frontend/src/app/admin/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { AdminSidebar } from "@/app/admin/admin-sidebar";
|
||||||
|
import { validateRequest } from "@/server/auth";
|
||||||
|
import { UserRole } from "@/server/auth/permissions";
|
||||||
|
import { IS_NOT, guard } from "@/utils/guard";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
interface AdminLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function AdminLayout(props: AdminLayoutProps) {
|
||||||
|
const { children } = props;
|
||||||
|
const { user } = await validateRequest();
|
||||||
|
|
||||||
|
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-1 flex-col gap-4">
|
||||||
|
<div className="mx-auto grid w-full gap-2">
|
||||||
|
<h1 className="text-3xl font-semibold">Admin</h1>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto grid w-full items-start gap-4 md:gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]">
|
||||||
|
<nav className="grid gap-4 text-sm">
|
||||||
|
<AdminSidebar />
|
||||||
|
</nav>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
121
frontend/src/app/admin/page.tsx
Normal file
121
frontend/src/app/admin/page.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { AbortReasonCountChart } from "@/app/admin/charts/printer-error-chart";
|
||||||
|
import { PrinterErrorRateChart } from "@/app/admin/charts/printer-error-rate";
|
||||||
|
import { ForecastPrinterUsageChart } from "@/app/admin/charts/printer-forecast";
|
||||||
|
import { PrinterUtilizationChart } from "@/app/admin/charts/printer-utilization";
|
||||||
|
import { PrinterVolumeChart } from "@/app/admin/charts/printer-volume";
|
||||||
|
import { DataCard } from "@/components/data-card";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { calculatePrinterErrorRate } from "@/utils/analytics/error-rate";
|
||||||
|
import { calculateAbortReasonsCount } from "@/utils/analytics/errors";
|
||||||
|
import { forecastPrinterUsage } from "@/utils/analytics/forecast";
|
||||||
|
import { calculatePrinterUtilization } from "@/utils/analytics/utilization";
|
||||||
|
import { calculatePrintVolumes } from "@/utils/analytics/volume";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Admin Dashboard",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function AdminPage() {
|
||||||
|
const currentDate = new Date();
|
||||||
|
|
||||||
|
const lastMonth = new Date();
|
||||||
|
lastMonth.setDate(currentDate.getDate() - 31);
|
||||||
|
const printers = await db.query.printers.findMany({});
|
||||||
|
const printJobs = await db.query.printJobs.findMany({
|
||||||
|
where: (job, { gte }) => gte(job.startAt, lastMonth),
|
||||||
|
with: {
|
||||||
|
printer: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (printJobs.length < 1) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Druckaufträge</CardTitle>
|
||||||
|
<CardDescription>Zurzeit sind keine Druckaufträge verfügbar.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p>Aktualisieren Sie die Seite oder prüfen Sie später erneut, ob neue Druckaufträge verfügbar sind.</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPrintJobs = printJobs.filter((job) => {
|
||||||
|
if (job.aborted) return false;
|
||||||
|
|
||||||
|
const endAt = job.startAt.getTime() + job.durationInMinutes * 1000 * 60;
|
||||||
|
|
||||||
|
return endAt > currentDate.getTime();
|
||||||
|
});
|
||||||
|
const occupiedPrinters = currentPrintJobs.map((job) => job.printer.id);
|
||||||
|
const freePrinters = printers.filter((printer) => !occupiedPrinters.includes(printer.id));
|
||||||
|
const printerUtilization = calculatePrinterUtilization(printJobs);
|
||||||
|
const printerVolume = calculatePrintVolumes(printJobs);
|
||||||
|
const printerAbortReasons = calculateAbortReasonsCount(printJobs);
|
||||||
|
const printerErrorRate = calculatePrinterErrorRate(printJobs);
|
||||||
|
const printerForecast = forecastPrinterUsage(printJobs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tabs defaultValue={"@general"} className="flex flex-col gap-4 items-start">
|
||||||
|
<TabsList className="bg-neutral-100 w-full py-6">
|
||||||
|
<TabsTrigger value="@general">Allgemein</TabsTrigger>
|
||||||
|
<TabsTrigger value="@capacity">Druckerauslastung</TabsTrigger>
|
||||||
|
<TabsTrigger value="@report">Fehlerberichte</TabsTrigger>
|
||||||
|
<TabsTrigger value="@forecasts">Prognosen</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="@general" className="w-full">
|
||||||
|
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-4">
|
||||||
|
<div className="w-full col-span-2">
|
||||||
|
<DataCard
|
||||||
|
title="Aktuelle Auslastung"
|
||||||
|
value={`${Math.round((occupiedPrinters.length / (freePrinters.length + occupiedPrinters.length)) * 100)}%`}
|
||||||
|
icon={"Percent"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DataCard title="Aktive Drucker" value={occupiedPrinters.length} icon={"Rotate3d"} />
|
||||||
|
<DataCard title="Freie Drucker" value={freePrinters.length} icon={"PowerOff"} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="@capacity" className="w-full">
|
||||||
|
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-4">
|
||||||
|
<div className="w-full col-span-2">
|
||||||
|
<PrinterVolumeChart printerVolume={printerVolume} />
|
||||||
|
</div>
|
||||||
|
{printerUtilization.map((data) => (
|
||||||
|
<PrinterUtilizationChart key={data.printerId} data={data} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="@report" className="w-full">
|
||||||
|
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-4">
|
||||||
|
<div className="w-full col-span-2">
|
||||||
|
<PrinterErrorRateChart printerErrorRate={printerErrorRate} />
|
||||||
|
</div>
|
||||||
|
<div className="w-full col-span-2">
|
||||||
|
<AbortReasonCountChart abortReasonCount={printerAbortReasons} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="@forecasts" className="w-full">
|
||||||
|
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-4">
|
||||||
|
<div className="w-full col-span-2">
|
||||||
|
<ForecastPrinterUsageChart
|
||||||
|
forecastData={printerForecast.map((usageMinutes, index) => ({
|
||||||
|
day: index,
|
||||||
|
usageMinutes,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
86
frontend/src/app/admin/printers/columns.tsx
Normal file
86
frontend/src/app/admin/printers/columns.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
"use client";
|
||||||
|
import type { printers } from "@/server/db/schema";
|
||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import type { InferSelectModel } from "drizzle-orm";
|
||||||
|
import { ArrowUpDown, MoreHorizontal, PencilIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { EditPrinterDialogContent, EditPrinterDialogTrigger } from "@/app/admin/printers/dialogs/edit-printer";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dialog } from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { type PrinterStatus, translatePrinterStatus } from "@/utils/printers";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
// This type is used to define the shape of our data.
|
||||||
|
// You can use a Zod schema here if you want.
|
||||||
|
|
||||||
|
export const columns: ColumnDef<InferSelectModel<typeof printers>>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
||||||
|
ID
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: "Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "description",
|
||||||
|
header: "Beschreibung",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.getValue("status");
|
||||||
|
const translated = translatePrinterStatus(status as PrinterStatus);
|
||||||
|
|
||||||
|
return translated;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const printer = row.original;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Menu öffnen</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Aktionen</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem asChild>ABC</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<EditPrinterDialogTrigger>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<PencilIcon className="w-4 h-4" />
|
||||||
|
<span>Bearbeiten</span>
|
||||||
|
</div>
|
||||||
|
</EditPrinterDialogTrigger>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<EditPrinterDialogContent setOpen={setOpen} printer={printer} />
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
135
frontend/src/app/admin/printers/data-table.tsx
Normal file
135
frontend/src/app/admin/printers/data-table.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ColumnDef,
|
||||||
|
type ColumnFiltersState,
|
||||||
|
type SortingState,
|
||||||
|
type VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
import { SlidersHorizontalIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface DataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
data: TData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnFilters,
|
||||||
|
columnVisibility,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center py-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Name filtern..."
|
||||||
|
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
||||||
|
onChange={(event) => table.getColumn("name")?.setFilterValue(event.target.value)}
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className="ml-auto flex items-center gap-2">
|
||||||
|
<SlidersHorizontalIcon className="h-4 w-4" />
|
||||||
|
<span>Spalten</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter((column) => column.getCanHide())
|
||||||
|
.map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
className="capitalize"
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
Keine Ergebnisse gefunden.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end space-x-2 py-4">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
|
||||||
|
Zurück
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
|
||||||
|
Nächste Seite
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
26
frontend/src/app/admin/printers/dialogs/create-printer.tsx
Normal file
26
frontend/src/app/admin/printers/dialogs/create-printer.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { PrinterForm } from "@/app/admin/printers/form";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface CreatePrinterDialogProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreatePrinterDialog(props: CreatePrinterDialogProps) {
|
||||||
|
const { children } = props;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Drucker erstellen</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<PrinterForm setOpen={setOpen} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
83
frontend/src/app/admin/printers/dialogs/delete-printer.tsx
Normal file
83
frontend/src/app/admin/printers/dialogs/delete-printer.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { deletePrinter } from "@/server/actions/printers";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
|
||||||
|
interface DeletePrinterDialogProps {
|
||||||
|
printerId: string;
|
||||||
|
setOpen: (state: boolean) => void;
|
||||||
|
}
|
||||||
|
export function DeletePrinterDialog(props: DeletePrinterDialogProps) {
|
||||||
|
const { printerId, setOpen } = props;
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
toast({
|
||||||
|
description: "Drucker wird gelöscht...",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const result = await deletePrinter(printerId);
|
||||||
|
if (result?.error) {
|
||||||
|
toast({
|
||||||
|
description: result.error,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
toast({
|
||||||
|
description: "Drucker wurde gelöscht.",
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast({
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
description: "Ein unbekannter Fehler ist aufgetreten.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="destructive" className="gap-2 flex items-center">
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
<span>Drucker löschen</span>
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Bist Du dir sicher?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Diese Aktion kann nicht rückgängig gemacht werden. Der Drucker und die damit verbundenen Daten werden
|
||||||
|
unwiderruflich gelöscht.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
|
||||||
|
<AlertDialogAction className="bg-red-500" onClick={onSubmit}>
|
||||||
|
Ja, löschen
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
30
frontend/src/app/admin/printers/dialogs/edit-printer.tsx
Normal file
30
frontend/src/app/admin/printers/dialogs/edit-printer.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { PrinterForm } from "@/app/admin/printers/form";
|
||||||
|
import { DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import type { InferResultType } from "@/utils/drizzle";
|
||||||
|
|
||||||
|
interface EditPrinterDialogTriggerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditPrinterDialogTrigger(props: EditPrinterDialogTriggerProps) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return <DialogTrigger asChild>{children}</DialogTrigger>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditPrinterDialogContentProps {
|
||||||
|
printer: InferResultType<"printers">;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
export function EditPrinterDialogContent(props: EditPrinterDialogContentProps) {
|
||||||
|
const { printer, setOpen } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Drucker bearbeiten</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<PrinterForm setOpen={setOpen} printer={printer} />
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
204
frontend/src/app/admin/printers/form.tsx
Normal file
204
frontend/src/app/admin/printers/form.tsx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
"use client";
|
||||||
|
import { DeletePrinterDialog } from "@/app/admin/printers/dialogs/delete-printer";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { DialogClose } from "@/components/ui/dialog";
|
||||||
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { createPrinter, updatePrinter } from "@/server/actions/printers";
|
||||||
|
import type { InferResultType } from "@/utils/drizzle";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { SaveIcon, XCircleIcon } from "lucide-react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(2, {
|
||||||
|
message: "Der Name muss mindestens 2 Zeichen lang sein.",
|
||||||
|
})
|
||||||
|
.max(50),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.min(2, {
|
||||||
|
message: "Die Beschreibung muss mindestens 2 Zeichen lang sein.",
|
||||||
|
})
|
||||||
|
.max(50),
|
||||||
|
status: z.coerce.number().int().min(0).max(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PrinterFormProps {
|
||||||
|
printer?: InferResultType<"printers">;
|
||||||
|
setOpen: (state: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PrinterForm(props: PrinterFormProps) {
|
||||||
|
const { printer, setOpen } = props;
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: printer?.name ?? "",
|
||||||
|
description: printer?.description ?? "",
|
||||||
|
status: printer?.status ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Define a submit handler.
|
||||||
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
// TODO: create or update
|
||||||
|
if (printer) {
|
||||||
|
toast({
|
||||||
|
description: "Drucker wird aktualisiert...",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update
|
||||||
|
try {
|
||||||
|
const result = await updatePrinter(printer.id, {
|
||||||
|
description: values.description,
|
||||||
|
name: values.name,
|
||||||
|
status: values.status,
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
toast({
|
||||||
|
description: result.error,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
description: "Drucker wurde aktualisiert.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast({
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
description: "Ein unbekannter Fehler ist aufgetreten.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
description: "Drucker wird erstellt...",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create
|
||||||
|
try {
|
||||||
|
const result = await createPrinter({
|
||||||
|
description: values.description,
|
||||||
|
name: values.name,
|
||||||
|
status: values.status,
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
toast({
|
||||||
|
description: result.error,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
description: "Drucker wurde erstellt.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast({
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
description: "Ein unbekannter Fehler ist aufgetreten.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Anycubic Kobra 2 Pro" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Bitte gib einen eindeutigen Namen für den Drucker ein.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Beschreibung</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="80x80x80 Druckfläche, langsam" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Füge eine kurze Beschreibung des Druckers hinzu.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="status"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Status</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={field.value.toString()}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a verified email to display" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={"0"}>Verfügbar</SelectItem>
|
||||||
|
<SelectItem value={"1"}>Außer Betrieb</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>Wähle den aktuellen Status des Druckers.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
{printer && <DeletePrinterDialog setOpen={setOpen} printerId={printer?.id} />}
|
||||||
|
{!printer && (
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="secondary" className="gap-2 flex items-center">
|
||||||
|
<XCircleIcon className="w-4 h-4" />
|
||||||
|
<span>Abbrechen</span>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
)}
|
||||||
|
<Button type="submit" className="gap-2 flex items-center">
|
||||||
|
<SaveIcon className="w-4 h-4" />
|
||||||
|
<span>Speichern</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
31
frontend/src/app/admin/printers/page.tsx
Normal file
31
frontend/src/app/admin/printers/page.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { columns } from "@/app/admin/printers/columns";
|
||||||
|
import { DataTable } from "@/app/admin/printers/data-table";
|
||||||
|
import { CreatePrinterDialog } from "@/app/admin/printers/dialogs/create-printer";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { PlusCircleIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export default async function AdminPage() {
|
||||||
|
const data = await db.query.printers.findMany();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Druckerverwaltung</CardTitle>
|
||||||
|
<CardDescription>Suche, Bearbeite, Lösche und Erstelle Drucker</CardDescription>
|
||||||
|
</div>
|
||||||
|
<CreatePrinterDialog>
|
||||||
|
<Button variant={"default"} className="flex gap-2 items-center">
|
||||||
|
<PlusCircleIcon className="w-4 h-4" />
|
||||||
|
<span>Drucker erstellen</span>
|
||||||
|
</Button>
|
||||||
|
</CreatePrinterDialog>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<DataTable columns={columns} data={data} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
7
frontend/src/app/admin/settings/download/route.ts
Normal file
7
frontend/src/app/admin/settings/download/route.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return new Response(fs.readFileSync("./db/sqlite.db"));
|
||||||
|
}
|
30
frontend/src/app/admin/settings/page.tsx
Normal file
30
frontend/src/app/admin/settings/page.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Systemeinstellungen",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdminPage() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Einstellungen</CardTitle>
|
||||||
|
<CardDescription>Systemeinstellungen</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex gap-8 items-center">
|
||||||
|
<p>Datenbank herunterladen</p>
|
||||||
|
<Button variant="default" asChild>
|
||||||
|
<Link href="/admin/settings/download" target="_blank">
|
||||||
|
Herunterladen
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
137
frontend/src/app/admin/users/columns.tsx
Normal file
137
frontend/src/app/admin/users/columns.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { type UserRole, translateUserRole } from "@/server/auth/permissions";
|
||||||
|
import type { users } from "@/server/db/schema";
|
||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import type { InferSelectModel } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
ArrowUpDown,
|
||||||
|
MailIcon,
|
||||||
|
MessageCircleIcon,
|
||||||
|
MoreHorizontal,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
EditUserDialogContent,
|
||||||
|
EditUserDialogRoot,
|
||||||
|
EditUserDialogTrigger,
|
||||||
|
} from "@/app/admin/users/dialog";
|
||||||
|
|
||||||
|
// This type is used to define the shape of our data.
|
||||||
|
// You can use a Zod schema here if you want.
|
||||||
|
export type User = {
|
||||||
|
id: string;
|
||||||
|
github_id: number;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const columns: ColumnDef<InferSelectModel<typeof users>>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
ID
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "github_id",
|
||||||
|
header: "GitHub ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "username",
|
||||||
|
header: "Username",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "displayName",
|
||||||
|
header: "Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "email",
|
||||||
|
header: "E-Mail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "role",
|
||||||
|
header: "Rolle",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const role = row.getValue("role");
|
||||||
|
const translated = translateUserRole(role as UserRole);
|
||||||
|
|
||||||
|
return translated;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const user = row.original;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditUserDialogRoot>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Menu öffnen</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Aktionen</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={generateTeamsChatURL(user.email)}
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
>
|
||||||
|
<MessageCircleIcon className="w-4 h-4" />
|
||||||
|
<span>Teams-Chat öffnen</span>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={generateEMailURL(user.email)}
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
>
|
||||||
|
<MailIcon className="w-4 h-4" />
|
||||||
|
<span>E-Mail schicken</span>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<EditUserDialogTrigger />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<EditUserDialogContent user={user as User} />
|
||||||
|
</EditUserDialogRoot>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function generateTeamsChatURL(email: string) {
|
||||||
|
return `https://teams.microsoft.com/l/chat/0/0?users=${email}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateEMailURL(email: string) {
|
||||||
|
return `mailto:${email}`;
|
||||||
|
}
|
135
frontend/src/app/admin/users/data-table.tsx
Normal file
135
frontend/src/app/admin/users/data-table.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ColumnDef,
|
||||||
|
type ColumnFiltersState,
|
||||||
|
type SortingState,
|
||||||
|
type VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
import { SlidersHorizontalIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface DataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
data: TData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnFilters,
|
||||||
|
columnVisibility,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center py-4">
|
||||||
|
<Input
|
||||||
|
placeholder="E-Mails filtern..."
|
||||||
|
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
|
||||||
|
onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value)}
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className="ml-auto flex items-center gap-2">
|
||||||
|
<SlidersHorizontalIcon className="h-4 w-4" />
|
||||||
|
<span>Spalten</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter((column) => column.getCanHide())
|
||||||
|
.map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
className="capitalize"
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
Keine Ergebnisse gefunden.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end space-x-2 py-4">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
|
||||||
|
Zurück
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
|
||||||
|
Nächste Seite
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
56
frontend/src/app/admin/users/dialog.tsx
Normal file
56
frontend/src/app/admin/users/dialog.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import type { User } from "@/app/admin/users/columns";
|
||||||
|
import { ProfileForm } from "@/app/admin/users/form";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { PencilIcon } from "lucide-react";
|
||||||
|
|
||||||
|
interface EditUserDialogRootProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditUserDialogRoot(props: EditUserDialogRootProps) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return <Dialog>{children}</Dialog>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditUserDialogTrigger() {
|
||||||
|
return (
|
||||||
|
<DialogTrigger className="flex gap-2 items-center">
|
||||||
|
<PencilIcon className="w-4 h-4" />
|
||||||
|
<span>Benutzer bearbeiten</span>
|
||||||
|
</DialogTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditUserDialogContentProps {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditUserDialogContent(props: EditUserDialogContentProps) {
|
||||||
|
const { user } = props;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Benutzer bearbeiten</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<strong>Hinweis:</strong> In den seltensten Fällen sollten die Daten
|
||||||
|
eines Benutzers geändert werden. Dies kann zu unerwarteten Problemen
|
||||||
|
führen.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<ProfileForm user={user} />
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
212
frontend/src/app/admin/users/form.tsx
Normal file
212
frontend/src/app/admin/users/form.tsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { User } from "@/app/admin/users/columns";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { DialogClose } from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { deleteUser, updateUser } from "@/server/actions/users";
|
||||||
|
import type { UserRole } from "@/server/auth/permissions";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { SaveIcon, TrashIcon } from "lucide-react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.min(2, {
|
||||||
|
message: "Der Benutzername muss mindestens 2 Zeichen lang sein.",
|
||||||
|
})
|
||||||
|
.max(50),
|
||||||
|
displayName: z
|
||||||
|
.string()
|
||||||
|
.min(2, {
|
||||||
|
message: "Der Anzeigename muss mindestens 2 Zeichen lang sein.",
|
||||||
|
})
|
||||||
|
.max(50),
|
||||||
|
email: z.string().email(),
|
||||||
|
role: z.enum(["admin", "user", "guest"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ProfileFormProps {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileForm(props: ProfileFormProps) {
|
||||||
|
const { user } = props;
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
// 1. Define your form.
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role as UserRole,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Define a submit handler.
|
||||||
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
toast({ description: "Benutzerprofil wird aktualisiert..." });
|
||||||
|
|
||||||
|
await updateUser(user.id, values);
|
||||||
|
|
||||||
|
toast({ description: "Benutzerprofil wurde aktualisiert." });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Benutzername</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="MAXMUS" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Nur in Ausnahmefällen sollte der Benutzername geändert werden.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="displayName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Anzeigename</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Max Mustermann" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Der Anzeigename darf frei verändert werden.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>E-Mail Adresse</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="max.mustermann@mercedes-benz.com"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Nur in Ausnahmefällen sollte die E-Mail Adresse geändert werden.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="role"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Benutzerrolle</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a verified email to display" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="admin">Administrator</SelectItem>
|
||||||
|
<SelectItem value="user">Benutzer</SelectItem>
|
||||||
|
<SelectItem value="guest">Gast</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Die Benutzerrolle bestimmt die Berechtigungen des Benutzers.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
|
className="gap-2 flex items-center"
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
<span>Benutzer löschen</span>
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Bist du dir sicher?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Diese Aktion kann nicht rückgängig gemacht werden. Das
|
||||||
|
Benutzerprofil und die damit verbundenen Daten werden
|
||||||
|
unwiderruflich gelöscht.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className="bg-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
toast({ description: "Benutzerprofil wird gelöscht..." });
|
||||||
|
deleteUser(user.id);
|
||||||
|
toast({ description: "Benutzerprofil wurde gelöscht." });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ja, löschen
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="submit" className="gap-2 flex items-center">
|
||||||
|
<SaveIcon className="w-4 h-4" />
|
||||||
|
<span>Speichern</span>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
26
frontend/src/app/admin/users/page.tsx
Normal file
26
frontend/src/app/admin/users/page.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { columns } from "@/app/admin/users/columns";
|
||||||
|
import { DataTable } from "@/app/admin/users/data-table";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Alle Benutzer",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function AdminPage() {
|
||||||
|
const data = await db.query.users.findMany();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Benutzerverwaltung</CardTitle>
|
||||||
|
<CardDescription>Suche, Bearbeite und Lösche Benutzer</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<DataTable columns={columns} data={data} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
41
frontend/src/app/api/job/[jobId]/remaining-time/route.ts
Normal file
41
frontend/src/app/api/job/[jobId]/remaining-time/route.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { db } from "@/server/db";
|
||||||
|
import { printJobs } from "@/server/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
interface RemainingTimeRouteProps {
|
||||||
|
params: {
|
||||||
|
jobId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export async function GET(request: Request, { params }: RemainingTimeRouteProps) {
|
||||||
|
// Trying to fix build error in container...
|
||||||
|
if (params.jobId === undefined) {
|
||||||
|
return Response.json({});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the job details
|
||||||
|
const jobDetails = await db.query.printJobs.findFirst({
|
||||||
|
where: eq(printJobs.id, params.jobId),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the job exists
|
||||||
|
if (!jobDetails) {
|
||||||
|
return Response.json({
|
||||||
|
id: params.jobId,
|
||||||
|
error: "Job not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the remaining time
|
||||||
|
const startAt = new Date(jobDetails.startAt).getTime();
|
||||||
|
const endAt = startAt + jobDetails.durationInMinutes * 60 * 1000;
|
||||||
|
const remainingTime = Math.max(0, endAt - Date.now());
|
||||||
|
|
||||||
|
// Return the remaining time
|
||||||
|
return Response.json({
|
||||||
|
id: params.jobId,
|
||||||
|
remainingTime,
|
||||||
|
});
|
||||||
|
}
|
99
frontend/src/app/api/jobs/[id]/route.ts
Normal file
99
frontend/src/app/api/jobs/[id]/route.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { API_ENDPOINTS } from "@/utils/api-config";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const id = params.id;
|
||||||
|
|
||||||
|
// Rufe einzelnen Job vom externen Backend ab
|
||||||
|
const response = await fetch(`${API_ENDPOINTS.JOBS}/${id}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = await response.json();
|
||||||
|
return Response.json(job);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen des Jobs vom Backend:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const id = params.id;
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// Sende Job-Aktualisierung an das externe Backend
|
||||||
|
const response = await fetch(`${API_ENDPOINTS.JOBS}/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return Response.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Aktualisieren des Jobs:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Aktualisieren des Jobs' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const id = params.id;
|
||||||
|
|
||||||
|
// Sende Job-Löschung an das externe Backend
|
||||||
|
const response = await fetch(`${API_ENDPOINTS.JOBS}/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return Response.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Löschen des Jobs:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Löschen des Jobs' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
59
frontend/src/app/api/jobs/route.ts
Normal file
59
frontend/src/app/api/jobs/route.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { API_ENDPOINTS } from "@/utils/api-config";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Rufe Jobs vom externen Backend ab
|
||||||
|
const response = await fetch(API_ENDPOINTS.JOBS);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobs = await response.json();
|
||||||
|
return Response.json(jobs);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der Jobs vom Backend:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// Sende Job-Erstellung an das externe Backend
|
||||||
|
const response = await fetch(API_ENDPOINTS.JOBS, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return Response.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Erstellen des Jobs:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Erstellen des Jobs' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
27
frontend/src/app/api/printers/route.ts
Normal file
27
frontend/src/app/api/printers/route.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { API_ENDPOINTS } from "@/utils/api-config";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Rufe Drucker vom externen Backend ab statt von der lokalen Datenbank
|
||||||
|
const response = await fetch(API_ENDPOINTS.PRINTERS);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 502,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const printers = await response.json();
|
||||||
|
return Response.json(printers);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der Drucker vom Backend:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
123
frontend/src/app/auth/login/callback/route.ts
Normal file
123
frontend/src/app/auth/login/callback/route.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { lucia } from "@/server/auth";
|
||||||
|
import { type GitHubUserResult, github, isValidCallbackHost, USED_CALLBACK_URL } from "@/server/auth/oauth";
|
||||||
|
import { ALLOWED_CALLBACK_HOSTS } from "@/utils/api-config";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { users } from "@/server/db/schema";
|
||||||
|
import { OAuth2RequestError } from "arctic";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { generateIdFromEntropySize } from "lucia";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
interface GithubEmailResponse {
|
||||||
|
email: string;
|
||||||
|
primary: boolean;
|
||||||
|
verified: boolean;
|
||||||
|
visibility: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: Request): Promise<Response> {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const code = url.searchParams.get("code");
|
||||||
|
const state = url.searchParams.get("state");
|
||||||
|
const storedState = cookies().get("github_oauth_state")?.value ?? null;
|
||||||
|
|
||||||
|
// Log für Debugging
|
||||||
|
console.log("OAuth Callback erhalten:", url.toString());
|
||||||
|
console.log("Callback URL Validierung:", isValidCallbackHost(url.toString()));
|
||||||
|
console.log("Erlaubte Hosts:", ALLOWED_CALLBACK_HOSTS);
|
||||||
|
|
||||||
|
if (!code || !state || !storedState || state !== storedState) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
status_text: "Ungültiger OAuth-Callback",
|
||||||
|
data: { code, state, storedState, url: url.toString() },
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// GitHub OAuth Code validieren - die redirectURI ist bereits im GitHub Client konfiguriert
|
||||||
|
const tokens = await github.validateAuthorizationCode(code);
|
||||||
|
|
||||||
|
// Log zur Fehlersuche
|
||||||
|
console.log(`GitHub OAuth Token-Validierung erfolgreich, verwendete Callback-URL: ${USED_CALLBACK_URL}`);
|
||||||
|
|
||||||
|
const githubUserResponse = await fetch("https://git.i.mercedes-benz.com/api/v3/user", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens.accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const githubUser: GitHubUserResult = await githubUserResponse.json();
|
||||||
|
|
||||||
|
// Sometimes email can be null in the user query.
|
||||||
|
if (githubUser.email === null || githubUser.email === undefined) {
|
||||||
|
const githubEmailResponse = await fetch("https://git.i.mercedes-benz.com/api/v3/user/emails", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens.accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const githubUserEmail: GithubEmailResponse[] = await githubEmailResponse.json();
|
||||||
|
githubUser.email = githubUserEmail[0].email;
|
||||||
|
}
|
||||||
|
const existingUser = await db.query.users.findFirst({
|
||||||
|
where: eq(users.github_id, githubUser.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
const session = await lucia.createSession(existingUser.id, {});
|
||||||
|
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||||
|
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
|
||||||
|
return new Response(null, {
|
||||||
|
status: 302,
|
||||||
|
headers: {
|
||||||
|
Location: "/",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = generateIdFromEntropySize(10); // 16 characters long
|
||||||
|
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: userId,
|
||||||
|
github_id: githubUser.id,
|
||||||
|
username: githubUser.login,
|
||||||
|
displayName: githubUser.name,
|
||||||
|
email: githubUser.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await lucia.createSession(userId, {});
|
||||||
|
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||||
|
cookies().set(sessionCookie.name, sessionCookie.value, {
|
||||||
|
...sessionCookie.attributes,
|
||||||
|
secure: false, // Else cookie does not get set cause IT has not provided us an SSL certificate yet
|
||||||
|
});
|
||||||
|
return new Response(null, {
|
||||||
|
status: 302,
|
||||||
|
headers: {
|
||||||
|
Location: "/",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// the specific error message depends on the provider
|
||||||
|
if (e instanceof OAuth2RequestError) {
|
||||||
|
// invalid code
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
status_text: "Invalid code",
|
||||||
|
error: JSON.stringify(e),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Response(null, {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user