"feat: Added debug server and related components for improved development experience"

This commit is contained in:
Till Tomczak 2025-05-23 07:24:51 +02:00
parent d457a8d86b
commit 9f6219832c
189 changed files with 35730 additions and 133 deletions

47
.gitattributes vendored
View File

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

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

@ -0,0 +1 @@

View File

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

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

View 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">&times;</span>
</div>
<div class="modal-body">
<pre>${data.logs}</pre>
</div>
</div>
`;
document.body.appendChild(modal);
// Modal schließen, wenn auf X geklickt wird
modal.querySelector('.close').onclick = function() {
document.body.removeChild(modal);
};
// Modal schließen, wenn außerhalb geklickt wird
window.onclick = function(event) {
if (event.target === modal) {
document.body.removeChild(modal);
}
};
// Modal anzeigen
modal.style.display = 'block';
} else {
showMessage('Keine Logs verfügbar', true);
}
})
.catch(error => {
showMessage('Fehler beim Abrufen der Container-Logs', true);
console.error('Fehler beim Abrufen der Container-Logs:', error);
});
}
// Zeige Fehlermeldung oder Erfolgsmeldung
function showMessage(message, isError = false) {
const messageEl = document.getElementById('message');
if (messageEl) {
messageEl.textContent = message;
messageEl.className = isError ? 'message message-error' : 'message message-success';
messageEl.style.display = 'block';
// Verstecke Nachricht nach 5 Sekunden
setTimeout(() => {
messageEl.style.display = 'none';
}, 5000);
}
}
// Initialisiere alle Diagramme
function initAllCharts() {
// Überprüfe, ob Chart.js geladen ist
if (typeof Chart !== 'undefined') {
if (document.getElementById('cpu-usage-chart')) {
initCpuUsageChart();
}
if (document.getElementById('memory-usage-chart')) {
initMemoryUsageChart();
}
if (document.getElementById('disk-usage-chart')) {
initDiskUsageChart();
}
if (document.getElementById('container-status-chart')) {
initContainerStatusChart();
}
// Initialen Datenabruf starten
updateSystemCharts();
updateContainerStatus();
// Regelmäßige Aktualisierung der Diagramme
setInterval(updateSystemCharts, 5000); // Alle 5 Sekunden aktualisieren
setInterval(updateContainerStatus, 10000); // Alle 10 Sekunden aktualisieren
} else {
console.error('Chart.js konnte nicht geladen werden.');
}
}
// Automatischer Start beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
initAllCharts();
});

File diff suppressed because it is too large Load Diff

View File

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

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

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

View File

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

@ -0,0 +1 @@

View 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

View 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

View 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

View 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

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

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

View 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

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

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

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

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

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

View 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}`);
});

View 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}`);
});

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

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

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

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

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

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

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

View 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",
},
});

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

View 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": {}
}
}

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

File diff suppressed because it is too large Load Diff

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

View 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

File diff suppressed because it is too large Load Diff

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

View 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

View 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 &mdash; 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>
&copy; 2024{" "}
<a href="https://linkedin.com/in/torben-haack" target="_blank" rel="noreferrer">
Torben Haack
</a>
</p>
</CardContent>
</Card>
);
}

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

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

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

View 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(", ");
}

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

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

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

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

View 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>
</>
);
}

View 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>
);
},
},
];

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

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

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

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

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

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

View 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"));
}

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

View 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}`;
}

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

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

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

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

View 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,
});
}

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

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

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

View 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