"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
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
# ========================================================================================
|
||||
# 🏗️ INFRASTRUKTUR UND CONTAINER
|
||||
# ========================================================================================
|
||||
|
||||
# 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/
|
||||
*.env
|
||||
*.pem
|
||||
*.key
|
||||
*.cer
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
infrastructure/ssl/
|
||||
**/secrets/
|
||||
**/private/
|
||||
|
||||
# Python spezifische Dateien
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
venv/
|
||||
ENV/
|
||||
# SSH-Schlüssel
|
||||
**/.ssh/
|
||||
**/id_rsa*
|
||||
**/id_ed25519*
|
||||
|
||||
# JavaScript/Node spezifische Dateien
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
.next/
|
||||
out/
|
||||
.vercel
|
||||
*.tsbuildinfo
|
||||
# ========================================================================================
|
||||
# 🐍 PYTHON/FLASK BACKEND
|
||||
# ========================================================================================
|
||||
|
||||
# IDE Dateien
|
||||
.idea/
|
||||
# Python-Bytecode
|
||||
**/__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/
|
||||
*.code-workspace
|
||||
|
||||
# JetBrains IDEs
|
||||
.idea/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
# Sublime Text
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
.vimrc.local
|
||||
|
||||
# Logs und temporäre Dateien
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
temp/
|
||||
# Emacs
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Eclipse
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
|
||||
# Datenbanken und SQLite Dateien
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.db
|
||||
instance/
|
||||
# NetBeans
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
# Kompilierte Dateien und Binaries
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
*.exe
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
*.dylib
|
||||
# ========================================================================================
|
||||
# 🖥️ BETRIEBSSYSTEM
|
||||
# ========================================================================================
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
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_cors import CORS
|
||||
from flask import Flask, request, jsonify, g, redirect, url_for, session as flask_session, render_template, flash, send_from_directory
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import secrets # Für bessere Salt-Generierung
|
||||
from functools import wraps
|
||||
@ -17,20 +16,82 @@ from datetime import timedelta
|
||||
from PyP100 import PyP100
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Importiere Konfiguration
|
||||
from config import config
|
||||
|
||||
# Importiere Netzwerkkonfiguration
|
||||
from network_config import NetworkConfig
|
||||
|
||||
# Importiere Frontend V2 Blueprint
|
||||
from frontend_v2_routes import frontend_v2, set_app_functions
|
||||
|
||||
# Lade Umgebungsvariablen
|
||||
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__)
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
# Initialisiere Netzwerkkonfiguration
|
||||
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['DATABASE'] = os.environ.get('DATABASE_PATH', 'instance/myp.db')
|
||||
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['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
|
||||
TAPO_USERNAME = os.environ.get('TAPO_USERNAME')
|
||||
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'):
|
||||
os.mkdir('logs')
|
||||
file_handler = RotatingFileHandler('logs/myp.log', maxBytes=10240, backupCount=10)
|
||||
@ -1686,9 +1801,33 @@ def stats_page():
|
||||
return redirect(url_for('index'))
|
||||
return render_template('stats.html', current_user=current_user, active_page='stats')
|
||||
|
||||
# Initialisierung und Start des Hintergrund-Threads beim ersten Request
|
||||
with app.app_context():
|
||||
# Diese Funktion wird nach dem App-Start aber vor dem ersten Request ausgeführt
|
||||
# Registrierungsfunktionen für modularen Aufbau
|
||||
def register_database_functions(app):
|
||||
"""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
|
||||
def initialize_background_tasks():
|
||||
"""Startet den Hintergrund-Thread für Job-Überprüfung beim ersten Request."""
|
||||
@ -1711,6 +1850,7 @@ with app.app_context():
|
||||
|
||||
# Server starten
|
||||
if __name__ == '__main__':
|
||||
# Legacy-Modus für direkte Ausführung
|
||||
with app.app_context():
|
||||
init_db()
|
||||
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.start()
|
||||
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-cors==4.0.0
|
||||
pyjwt==2.8.0
|
||||
python-dotenv==1.0.0
|
||||
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
|
||||
waitress==2.1.2
|
||||
|
||||
# Netzwerk und Hardware-Integration
|
||||
PyP100==0.0.19
|
||||
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