Backend aufgeräumt und Steckdosen-Einschaltfunktion behoben
- TAPO_PASSWORD in .env korrigiert (Agent045) - Unnötige Verzeichnisse entfernt (node_modules, archiv in backend/, etc.) - .gitignore erstellt um .env-Dateien zu schützen - Projektstruktur bereinigt (von 1.5MB auf 186KB reduziert) - Flask Web UI vollständig funktionsfähig 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
packages/reservation-platform/docker/images/*.tar.xz filter=lfs diff=lfs merge=lfs -text
|
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Environment
|
||||
.env
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Flask
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Node
|
||||
node_modules/
|
53
CLAUDE.md
Normal file
53
CLAUDE.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# MYP Project Development Guidelines
|
||||
|
||||
## System Architecture
|
||||
- **Frontend**:
|
||||
- Located in `packages/reservation-platform`
|
||||
- Runs on a Raspberry Pi connected to company network
|
||||
- Has internet access on one interface
|
||||
- Connected via LAN to an offline network
|
||||
- Serves as the user interface
|
||||
- Developed by another apprentice as part of IHK project work
|
||||
|
||||
- **Backend**:
|
||||
- Located in `backend` directory
|
||||
- Flask application running on a separate Raspberry Pi
|
||||
- Connected only to the offline network
|
||||
- Communicates with WiFi smart plugs
|
||||
- Part of my IHK project work for digital networking qualification
|
||||
|
||||
- **Printers/Smart Plugs**:
|
||||
- Printers can only be controlled (on/off) via WiFi smart plugs
|
||||
- No other control mechanisms available
|
||||
- Smart plugs and printers are equivalent in the system context
|
||||
|
||||
## Build/Run Commands
|
||||
- Backend: `cd backend && source venv/bin/activate && python app.py`
|
||||
- Frontend: `cd packages/reservation-platform && pnpm dev`
|
||||
- Run tests: `cd backend && python -m unittest development/tests/tests.py`
|
||||
- Run single test: `cd backend && python -m unittest development.tests.tests.MYPBackendTestCase.test_name`
|
||||
- Check jobs manually: `cd backend && source venv/bin/activate && flask check-jobs`
|
||||
- Lint frontend: `cd packages/reservation-platform && pnpm lint`
|
||||
- Format frontend: `cd packages/reservation-platform && npx @biomejs/biome format --write ./src`
|
||||
|
||||
## Code Style
|
||||
- **Python Backend**:
|
||||
- Use PEP 8 conventions, 4-space indentation
|
||||
- Line width: 100 characters max
|
||||
- Add docstrings to functions and classes
|
||||
- Error handling: Use try/except with specific exceptions
|
||||
- Naming: snake_case for functions/variables, PascalCase for classes
|
||||
|
||||
- **Frontend (Next.js/TypeScript)**:
|
||||
- Use Biome for formatting and linting (line width: 120 chars)
|
||||
- Organize imports automatically with Biome
|
||||
- Use TypeScript types for all code
|
||||
- Use React hooks for state management
|
||||
- Naming: camelCase for functions/variables, PascalCase for components
|
||||
|
||||
## Work Guidelines
|
||||
- All changes must be committed to git
|
||||
- Work efficiently and cost-effectively
|
||||
- Don't repeatedly try the same solution if it doesn't work
|
||||
- Create and check notes when encountering issues
|
||||
- Clearly communicate if something is not possible so I can handle it manually
|
3
CREDENTIALS
Normal file
3
CREDENTIALS
Normal file
@@ -0,0 +1,3 @@
|
||||
TAPO ADMIN: vT6Vsd^p
|
||||
Admin-PW: 744563017196
|
||||
Tapo: 744563017196A
|
11
Dokumentation.md
Normal file
11
Dokumentation.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Dokumentation
|
||||
|
||||
Komplikationen:
|
||||
- Netzwerkanbindung
|
||||
- Ermitteln der Schnittstellen der Drucker
|
||||
- Auswahl der Anbindung, Entwickeln eines Netzwerkkonzeptes
|
||||
- Beschaffung der Hardware (beschränkte Auswahlmöglichkeiten)
|
||||
- Welches Betriebssystem? OpenSuse, NixOS, Debian
|
||||
- Frontend verstehen lernen
|
||||
- Netzwerk einrichten, Frontend anbinden
|
||||
|
51
README.md
Normal file
51
README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
<<<<<<< HEAD
|
||||
# 📦 MYP
|
||||
|
||||
> Frontend: https://git.i.mercedes-benz.com/TBA-Berlin-FI/MYP/tree/main/packages/reservation-platform
|
||||
|
||||
> :warning: MYP ist zzt. in Entwicklung
|
||||
|
||||
MYP *(Manage your Printer)* ist eine Plattform zur Reservierung von 3D-Druckern, die für die TBA im Werk 040, Berlin-Marienfelde, entwickelt wurde.
|
||||
|
||||
> ‼ Datenbank aus Blueprint steht im Konflikt zu MYP.sql - Integration muss besprochen werden
|
||||
|
||||
> 💡 Vorschlag wenn machbar @Torben: Aufteilung der Datenbanken? API ist ja nur für Drucker zuständig; WebInterface für Authentifizierung, Sessions etc.
|
||||
|
||||
> UltiMaker HTTP API: https://support.makerbot.com/s/article/1667412427787
|
||||
|
||||
---
|
||||
|
||||
# Datenbankstruktur
|
||||
|
||||
(MYP.sql - Gerät: Reservation Pi)
|
||||
|
||||
### Printer
|
||||
- Speichert Informationen zu Druckern.
|
||||
- Beinhaltet Details wie Namen, Beschreibung und Betriebsstatus.
|
||||
- Verknüpft mit Druckaufträgen.
|
||||
|
||||
### PrintJob
|
||||
- Enthält alle Druckaufträge.
|
||||
- Jeder Auftrag ist einem Drucker und einem Benutzer zugeordnet.
|
||||
- Speichert Startzeit, Dauer und Kommentare zu den Aufträgen.
|
||||
- Erfassung, ob ein Auftrag abgebrochen wurde und die dazugehörige Begründung.
|
||||
|
||||
### Account
|
||||
- Verwaltet Benutzerkonten.
|
||||
- Speichert Authentifizierungsdetails wie Tokens und deren Ablaufzeiten.
|
||||
|
||||
### Session
|
||||
- Erfasst Session-Daten.
|
||||
- Beinhaltet eindeutige Session-Tokens und Ablaufdaten.
|
||||
|
||||
### User
|
||||
- Speichert Benutzerinformationen.
|
||||
- Verknüpft mit Druckaufträgen, Accounts und Sessions.
|
||||
|
||||
## Fremdschlüsselbeziehungen
|
||||
- `User` ist verknüpft mit `PrintJob`, `Account` und `Session` über Benutzer-ID.
|
||||
- `Printer` ist verknüpft mit `PrintJob` über die Drucker-ID.
|
||||
=======
|
||||
# Projektarbeit-MYP
|
||||
|
||||
>>>>>>> dfd63d7c9ddf4b3a654f06dff38bebdbec7395d7
|
106
archiv/NETWORK-api-backend_blueprint/README.md
Normal file
106
archiv/NETWORK-api-backend_blueprint/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 🖨️ 3D-Drucker Status API 📊
|
||||
|
||||
Willkommen beim Blueprint der 3D-Drucker Status API! Diese API ermöglicht es Ihnen, den Status mehrerer über LAN verbundener 3D-Drucker zu überwachen und Druckaufträge an sie zu senden.
|
||||
|
||||
## 🌟 Funktionen
|
||||
|
||||
- 🔍 Abrufen des Status von 3D-Druckern, einschließlich ihres aktuellen Status, Fortschrittes und Temperatur.
|
||||
- 📥 Senden von Druckaufträgen an verfügbare 3D-Drucker.
|
||||
- 💾 Speichern und Aktualisieren des Status jedes Druckers in einer SQLite-Datenbank.
|
||||
|
||||
## 🛠️Verwendete Technologien
|
||||
|
||||
- 🐍 Python
|
||||
- 🌶️ Flask
|
||||
- 🗄️ SQLite
|
||||
- 🌐 HTTP-Anfragen
|
||||
|
||||
## 📋 Verordnungen
|
||||
|
||||
Bevor Sie die API starten, stellen Sie sicher, dass Sie folgendes haben:
|
||||
|
||||
- Python 3.x installiert
|
||||
- Flask und python-dotenv-Bibliotheken installiert (`pip install flask python-dotenv`)
|
||||
- Eine Liste von IP-Adressen der 3D-Drucker, die Sie überwachen möchten
|
||||
|
||||
## 🚀 Erste Schritte
|
||||
|
||||
1. Klonen Sie das Repository:
|
||||
```
|
||||
git clone https://git.i.mercedes-benz.com/TBA-Berlin-FI/MYP
|
||||
```
|
||||
|
||||
2. Installieren Sie die erforderlichen Abhängigkeiten:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Erstellen Sie eine `.env`-Datei im Projektverzeichnis und geben Sie die IP-Adressen Ihrer 3D-Drucker an:
|
||||
```
|
||||
PRINTER_IPS=192.168.0.10,192.168.0.11,192.168.0.12
|
||||
```
|
||||
|
||||
4. Starten Sie das Skript, um die SQLite-Datenbank zu erstellen:
|
||||
```
|
||||
python create_db.py
|
||||
```
|
||||
|
||||
5. Starten Sie den API-Server:
|
||||
```
|
||||
python app.py
|
||||
```
|
||||
|
||||
6. Die API ist unter `http://localhost:5000` erreichbar.
|
||||
|
||||
## 📡 API-Endpunkte
|
||||
|
||||
- `GET /printer_status`: Rufen Sie den Status aller 3D-Drucker ab.
|
||||
- `POST /print_job`: Senden Sie einen Druckauftrag an einen bestimmten 3D-Drucker.
|
||||
|
||||
## 📝 API-Nutzung
|
||||
|
||||
### Druckerstatus abrufen
|
||||
|
||||
Senden Sie eine `GET`-Anfrage an `/printer_status`, um den Status aller 3D-Drucker abzurufen.
|
||||
|
||||
Antwort:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"ip": "192.168.0.10",
|
||||
"status": "frei",
|
||||
"progress": 0,
|
||||
"temperature": 25
|
||||
},
|
||||
{
|
||||
"ip": "192.168.0.11",
|
||||
"status": "besetzt",
|
||||
"progress": 50,
|
||||
"temperature": 180
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### Druckauftrag senden
|
||||
|
||||
Senden Sie eine `POST`-Anfrage an `/print_job` mit der folgenden JSON-Last, um einen Druckauftrag an einen bestimmten 3D-Drucker zu senden:
|
||||
|
||||
```json
|
||||
{
|
||||
"printer_ip": "192.168.0.10",
|
||||
"file_url": "http://example.com/print_file.gcode"
|
||||
}
|
||||
```
|
||||
|
||||
Antwort:
|
||||
```json
|
||||
{
|
||||
"message": "Druckauftrag gestartet"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 📄 Lizenz
|
||||
|
||||
- --> Noch nicht verfügbar
|
25
archiv/NETWORK-api-backend_blueprint/datenbank_erstellen.py
Normal file
25
archiv/NETWORK-api-backend_blueprint/datenbank_erstellen.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import sqlite3
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
printers = os.getenv('PRINTER_IPS').split(',')
|
||||
|
||||
def create_db():
|
||||
conn = sqlite3.connect('printers.db')
|
||||
c = conn.cursor()
|
||||
|
||||
# Tabelle 'printers' erstellen, falls sie nicht existiert
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS printers
|
||||
(ip TEXT PRIMARY KEY, status TEXT)''')
|
||||
|
||||
# Drucker-IPs in die Tabelle einfügen, falls sie noch nicht vorhanden sind
|
||||
for printer_ip in printers:
|
||||
c.execute("INSERT OR IGNORE INTO printers (ip, status) VALUES (?, ?)", (printer_ip, "frei"))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("Datenbank 'printers.db' erfolgreich erstellt.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
create_db()
|
3
archiv/NETWORK-api-backend_blueprint/requirements.txt
Normal file
3
archiv/NETWORK-api-backend_blueprint/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
flask==2.1.0
|
||||
requests==2.25.1
|
||||
python-dotenv==0.20.0
|
94
archiv/NETWORK-api-backend_blueprint/server.py
Normal file
94
archiv/NETWORK-api-backend_blueprint/server.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from flask import Flask, jsonify, request
|
||||
import requests
|
||||
import sqlite3
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
printers = os.getenv('PRINTER_IPS').split(',')
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# SQLite-Datenbank initialisieren
|
||||
def init_db():
|
||||
conn = sqlite3.connect('printers.db')
|
||||
c = conn.cursor()
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS printers
|
||||
(ip TEXT PRIMARY KEY, status TEXT)''')
|
||||
for printer_ip in printers:
|
||||
c.execute("INSERT OR IGNORE INTO printers (ip, status) VALUES (?, ?)", (printer_ip, "frei"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@app.route('/printer_status', methods=['GET'])
|
||||
def get_printer_status():
|
||||
printer_status = []
|
||||
conn = sqlite3.connect('printers.db')
|
||||
c = conn.cursor()
|
||||
|
||||
for printer_ip in printers:
|
||||
c.execute("SELECT status FROM printers WHERE ip = ?", (printer_ip,))
|
||||
status = c.fetchone()[0]
|
||||
|
||||
try:
|
||||
response = requests.get(f"http://{printer_ip}/api/printer/status")
|
||||
|
||||
if response.status_code == 200:
|
||||
status_data = response.json()
|
||||
printer_status.append({
|
||||
"ip": printer_ip,
|
||||
"status": status,
|
||||
"progress": status_data["progress"],
|
||||
"temperature": status_data["temperature"]
|
||||
})
|
||||
else:
|
||||
printer_status.append({
|
||||
"ip": printer_ip,
|
||||
"status": "Fehler bei der Abfrage",
|
||||
"progress": None,
|
||||
"temperature": None
|
||||
})
|
||||
except:
|
||||
printer_status.append({
|
||||
"ip": printer_ip,
|
||||
"status": "Drucker nicht erreichbar",
|
||||
"progress": None,
|
||||
"temperature": None
|
||||
})
|
||||
|
||||
conn.close()
|
||||
return jsonify(printer_status)
|
||||
|
||||
@app.route('/print_job', methods=['POST'])
|
||||
def submit_print_job():
|
||||
print_job = request.json
|
||||
printer_ip = print_job["printer_ip"]
|
||||
file_url = print_job["file_url"]
|
||||
|
||||
conn = sqlite3.connect('printers.db')
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT status FROM printers WHERE ip = ?", (printer_ip,))
|
||||
status = c.fetchone()[0]
|
||||
|
||||
if status == "frei":
|
||||
try:
|
||||
response = requests.post(f"http://{printer_ip}/api/print_job", json={"file_url": file_url})
|
||||
|
||||
if response.status_code == 200:
|
||||
c.execute("UPDATE printers SET status = 'besetzt' WHERE ip = ?", (printer_ip,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"message": "Druckauftrag gestartet"}), 200
|
||||
else:
|
||||
conn.close()
|
||||
return jsonify({"message": "Fehler beim Starten des Druckauftrags"}), 500
|
||||
except:
|
||||
conn.close()
|
||||
return jsonify({"message": "Drucker nicht erreichbar"}), 500
|
||||
else:
|
||||
conn.close()
|
||||
return jsonify({"message": "Drucker ist nicht frei"}), 400
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
app.run(host='0.0.0.0', port=5000)
|
@@ -0,0 +1,38 @@
|
||||
# entwendet aus:
|
||||
# https://github.com/ut-hnl-lab/ultimakerpy
|
||||
|
||||
# auch zum lesen:
|
||||
# https://github.com/MartinBienz/SDPremote?tab=readme-ov-file
|
||||
|
||||
import time
|
||||
from ultimakerpy import UMS3, JobState
|
||||
|
||||
def print_started(state):
|
||||
if state == JobState.PRINTING:
|
||||
time.sleep(6.0)
|
||||
return True
|
||||
return False
|
||||
|
||||
def layer_reached(pos, n):
|
||||
if round(pos / 0.2) >= n: # set layer pitch: 0.2 mm
|
||||
return True
|
||||
return False
|
||||
|
||||
printer = UMS3(name='MyPrinterName')
|
||||
targets = {
|
||||
'job_state': printer.job_state,
|
||||
'bed_pos': printer.bed.position,
|
||||
}
|
||||
|
||||
printer.print_from_dialog() # select file to print
|
||||
printer.peripherals.camera_streaming()
|
||||
with printer.data_logger('output2.csv', targets) as dl:
|
||||
timer = dl.get_timer()
|
||||
|
||||
# sleep until active leveling finishes
|
||||
timer.wait_for_datalog('job_state', print_started)
|
||||
|
||||
for n in range(1, 101):
|
||||
# sleep until the printing of specified layer to start
|
||||
timer.wait_for_datalog('bed_pos', lambda v: layer_reached(v, n))
|
||||
print('printing layer:', n)
|
0
archiv/backend/myp_backend.db
Normal file
0
archiv/backend/myp_backend.db
Normal file
148
archiv/backend/myp_backend.py
Normal file
148
archiv/backend/myp_backend.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for, jsonify, session
|
||||
import sqlite3
|
||||
import bcrypt
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'supersecretkey'
|
||||
|
||||
# Database setup
|
||||
def init_db():
|
||||
conn = sqlite3.connect('database.db')
|
||||
c = conn.cursor()
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)''')
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS printers (id INTEGER PRIMARY KEY, name TEXT, status TEXT)''')
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS jobs (id INTEGER PRIMARY KEY, printer_id INTEGER, user TEXT, date TEXT, status TEXT)''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
init_db()
|
||||
|
||||
# User registration (Admin setup)
|
||||
def add_admin():
|
||||
conn = sqlite3.connect('database.db')
|
||||
c = conn.cursor()
|
||||
hashed_pw = bcrypt.hashpw('adminpassword'.encode('utf-8'), bcrypt.gensalt())
|
||||
c.execute("INSERT INTO users (username, password) VALUES (?, ?)", ('admin', hashed_pw))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Comment the next line after the first run
|
||||
# add_admin()
|
||||
|
||||
# API Endpoints
|
||||
@app.route('/api/printers/status', methods=['GET'])
|
||||
def get_printer_status():
|
||||
conn = sqlite3.connect('database.db')
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT * FROM printers")
|
||||
printers = c.fetchall()
|
||||
conn.close()
|
||||
return jsonify(printers)
|
||||
|
||||
@app.route('/api/printers/job', methods=['POST'])
|
||||
def create_job():
|
||||
if not session.get('logged_in'):
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
data = request.json
|
||||
user = session['username']
|
||||
printer_id = data['printer_id']
|
||||
conn = sqlite3.connect('database.db')
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute("SELECT status FROM printers WHERE id=?", (printer_id,))
|
||||
status = c.fetchone()[0]
|
||||
|
||||
if status == 'frei':
|
||||
c.execute("INSERT INTO jobs (printer_id, user, date, status) VALUES (?, ?, datetime('now'), 'in progress')",
|
||||
(printer_id, user))
|
||||
c.execute("UPDATE printers SET status='belegt' WHERE id=?", (printer_id,))
|
||||
conn.commit()
|
||||
elif status == 'belegt':
|
||||
return jsonify({'error': 'Printer already in use'}), 409
|
||||
else:
|
||||
return jsonify({'error': 'Invalid printer status'}), 400
|
||||
|
||||
conn.close()
|
||||
return jsonify({'message': 'Job created and printer turned on'}), 200
|
||||
|
||||
@app.route('/api/printers/reserve', methods=['POST'])
|
||||
def reserve_printer():
|
||||
if not session.get('logged_in'):
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
data = request.json
|
||||
printer_id = data['printer_id']
|
||||
conn = sqlite3.connect('database.db')
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute("SELECT status FROM printers WHERE id=?", (printer_id,))
|
||||
status = c.fetchone()[0]
|
||||
|
||||
if status == 'frei':
|
||||
c.execute("UPDATE printers SET status='reserviert' WHERE id=?", (printer_id,))
|
||||
conn.commit()
|
||||
message = 'Printer reserved'
|
||||
else:
|
||||
message = 'Printer cannot be reserved'
|
||||
|
||||
conn.close()
|
||||
return jsonify({'message': message}), 200
|
||||
|
||||
@app.route('/api/printers/release', methods=['POST'])
|
||||
def release_printer():
|
||||
if not session.get('logged_in'):
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
data = request.json
|
||||
printer_id = data['printer_id']
|
||||
conn = sqlite3.connect('database.db')
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute("UPDATE printers SET status='frei' WHERE id=?", (printer_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'message': 'Printer released'}), 200
|
||||
|
||||
# Authentication routes
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password'].encode('utf-8')
|
||||
|
||||
conn = sqlite3.connect('database.db')
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT * FROM users WHERE username=?", (username,))
|
||||
user = c.fetchone()
|
||||
conn.close()
|
||||
|
||||
if user and bcrypt.checkpw(password, user[2].encode('utf-8')):
|
||||
session['logged_in'] = True
|
||||
session['username'] = username
|
||||
return redirect(url_for('dashboard'))
|
||||
else:
|
||||
return render_template('login.html', error='Invalid Credentials')
|
||||
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/dashboard')
|
||||
def dashboard():
|
||||
if not session.get('logged_in'):
|
||||
return redirect(url_for('login'))
|
||||
|
||||
conn = sqlite3.connect('database.db')
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT * FROM printers")
|
||||
printers = c.fetchall()
|
||||
conn.close()
|
||||
|
||||
return render_template('dashboard.html', printers=printers)
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
20
archiv/backend/templates/base.html
Normal file
20
archiv/backend/templates/base.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>3D Printer Management</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@1.14.0/dist/full.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-black text-white">
|
||||
<nav class="bg-gray-800 p-4">
|
||||
<div class="container mx-auto">
|
||||
<h1 class="text-xl">3D Printer Management Dashboard</h1>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container mx-auto mt-5">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
29
archiv/backend/templates/dashboard.html
Normal file
29
archiv/backend/templates/dashboard.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="text-2xl mb-4">Printer Status</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for printer in printers %}
|
||||
<div class="card bg-gray-900 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ printer[1] }}</h2>
|
||||
<p>Status: {{ printer[2] }}</p>
|
||||
{% if printer[2] == 'frei' %}
|
||||
<form method="POST" action="/api/printers/job">
|
||||
<input type="hidden" name="printer_id" value="{{ printer[0] }}">
|
||||
<button class="btn btn-success mt-4 w-full">Start Job</button>
|
||||
</form>
|
||||
{% elif printer[2] == 'belegt' %}
|
||||
<button class="btn btn-warning mt-4 w-full" disabled>In Use</button>
|
||||
{% elif printer[2] == 'reserviert' %}
|
||||
<form method="POST" action="/api/printers/release">
|
||||
<input type="hidden" name="printer_id" value="{{ printer[0] }}">
|
||||
<button class="btn btn-info mt-4 w-full">Release</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a href="/logout" class="btn btn-secondary mt-4">Logout</a>
|
||||
{% endblock %}
|
33
archiv/backend/templates/login.html
Normal file
33
archiv/backend/templates/login.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex justify-center items-center h-screen">
|
||||
<div class="card w-96 bg-gray-900 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Login</h2>
|
||||
<form method="POST">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Username</span>
|
||||
</label>
|
||||
<input type="text" name="username" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Password</span>
|
||||
</label>
|
||||
<input type="password" name="password" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div class="form-control mt-6">
|
||||
<button class="btn btn-primary w-full">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if error %}
|
||||
<div class="mt-4 text-red-500">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
3
archiv/flask-backend/.env.example
Normal file
3
archiv/flask-backend/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
SECRET_KEY=change-me-to-a-real-secret-key
|
||||
DATABASE_URL=sqlite:///app.db
|
||||
JWT_SECRET=change-me-to-a-real-jwt-secret
|
20
archiv/flask-backend/Dockerfile
Normal file
20
archiv/flask-backend/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Run database migrations
|
||||
RUN mkdir -p /app/instance
|
||||
ENV FLASK_APP=wsgi.py
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5000
|
||||
|
||||
# Run the application
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "wsgi:app"]
|
96
archiv/flask-backend/README.md
Normal file
96
archiv/flask-backend/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Reservation Platform Backend
|
||||
|
||||
This is the Flask backend for the 3D Printer Reservation Platform, providing a RESTful API for managing printers, reservations, and users.
|
||||
|
||||
## Features
|
||||
|
||||
- User authentication with email/password
|
||||
- Role-based permission system (admin, user)
|
||||
- Printer management
|
||||
- Reservation system
|
||||
- User management
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
- `POST /auth/register` - Register a new user
|
||||
- `POST /auth/login` - Login with username/email and password
|
||||
- `POST /auth/logout` - Log out a user by invalidating their session
|
||||
|
||||
### Printers
|
||||
- `GET /api/printers` - Get all printers
|
||||
- `GET /api/printers/<printer_id>` - Get a specific printer
|
||||
- `POST /api/printers` - Create a new printer (admin only)
|
||||
- `PUT /api/printers/<printer_id>` - Update a printer (admin only)
|
||||
- `DELETE /api/printers/<printer_id>` - Delete a printer (admin only)
|
||||
- `GET /api/printers/availability` - Get availability information for all printers
|
||||
|
||||
### Print Jobs
|
||||
- `GET /api/jobs` - Get jobs for the current user or all jobs for admin
|
||||
- `GET /api/jobs/<job_id>` - Get a specific job
|
||||
- `POST /api/jobs` - Create a new print job (reserve a printer)
|
||||
- `PUT /api/jobs/<job_id>` - Update a job
|
||||
- `DELETE /api/jobs/<job_id>` - Delete a job (cancel reservation)
|
||||
- `GET /api/jobs/<job_id>/remaining-time` - Get remaining time for a job (public endpoint)
|
||||
|
||||
### Users
|
||||
- `GET /api/users` - Get all users (admin only)
|
||||
- `GET /api/users/<user_id>` - Get a specific user (admin only)
|
||||
- `PUT /api/users/<user_id>` - Update a user (admin only)
|
||||
- `DELETE /api/users/<user_id>` - Delete a user (admin only)
|
||||
- `GET /api/me` - Get the current user's profile
|
||||
- `PUT /api/me` - Update the current user's profile
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.11 or higher
|
||||
- pip
|
||||
|
||||
### Setup
|
||||
|
||||
1. Clone the repository
|
||||
```bash
|
||||
git clone https://github.com/your-repo/reservation-platform.git
|
||||
cd reservation-platform/packages/flask-backend
|
||||
```
|
||||
|
||||
2. Install dependencies
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Create a `.env` file with the following variables:
|
||||
```
|
||||
SECRET_KEY=your-secret-key
|
||||
DATABASE_URL=sqlite:///app.db
|
||||
JWT_SECRET=your-jwt-secret
|
||||
```
|
||||
|
||||
4. Initialize the database
|
||||
```bash
|
||||
flask db upgrade
|
||||
python scripts/init_db.py
|
||||
```
|
||||
|
||||
5. Run the development server
|
||||
```bash
|
||||
python wsgi.py
|
||||
```
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
1. Build and run with Docker Compose
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running Migrations
|
||||
|
||||
To create a new migration after updating models:
|
||||
```bash
|
||||
flask db migrate -m "Description of changes"
|
||||
flask db upgrade
|
||||
```
|
32
archiv/flask-backend/app/__init__.py
Normal file
32
archiv/flask-backend/app/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_cors import CORS
|
||||
from config import Config
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
|
||||
def create_app(config_class=Config):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
CORS(app)
|
||||
|
||||
# Register blueprints
|
||||
from app.api import bp as api_bp
|
||||
app.register_blueprint(api_bp, url_prefix='/api')
|
||||
|
||||
from app.auth import bp as auth_bp
|
||||
app.register_blueprint(auth_bp, url_prefix='/auth')
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
return {'status': 'ok'}
|
||||
|
||||
return app
|
||||
|
||||
from app import models
|
5
archiv/flask-backend/app/api/__init__.py
Normal file
5
archiv/flask-backend/app/api/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('api', __name__)
|
||||
|
||||
from app.api import printers, jobs, users
|
219
archiv/flask-backend/app/api/jobs.py
Normal file
219
archiv/flask-backend/app/api/jobs.py
Normal file
@@ -0,0 +1,219 @@
|
||||
from flask import request, jsonify
|
||||
from app import db
|
||||
from app.api import bp
|
||||
from app.models import PrintJob, Printer, User
|
||||
from app.auth.routes import token_required, admin_required
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@bp.route('/jobs', methods=['GET'])
|
||||
@token_required
|
||||
def get_jobs():
|
||||
"""Get jobs for the current user or all jobs for admin"""
|
||||
is_admin = request.user_role == 'admin'
|
||||
user_id = request.user_id
|
||||
|
||||
# Parse query parameters
|
||||
status = request.args.get('status') # active, upcoming, completed, aborted, all
|
||||
printer_id = request.args.get('printer_id')
|
||||
|
||||
# Base query
|
||||
query = PrintJob.query
|
||||
|
||||
# Filter by user unless admin
|
||||
if not is_admin:
|
||||
query = query.filter_by(user_id=user_id)
|
||||
|
||||
# Filter by printer if provided
|
||||
if printer_id:
|
||||
query = query.filter_by(printer_id=printer_id)
|
||||
|
||||
# Apply status filter
|
||||
now = datetime.utcnow()
|
||||
if status == 'active':
|
||||
query = query.filter_by(aborted=False) \
|
||||
.filter(PrintJob.start_at <= now) \
|
||||
.filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > now)
|
||||
elif status == 'upcoming':
|
||||
query = query.filter_by(aborted=False) \
|
||||
.filter(PrintJob.start_at > now)
|
||||
elif status == 'completed':
|
||||
query = query.filter_by(aborted=False) \
|
||||
.filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) <= now)
|
||||
elif status == 'aborted':
|
||||
query = query.filter_by(aborted=True)
|
||||
|
||||
# Order by start time, most recent first
|
||||
query = query.order_by(PrintJob.start_at.desc())
|
||||
|
||||
# Execute query
|
||||
jobs = query.all()
|
||||
result = [job.to_dict() for job in jobs]
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/jobs/<job_id>', methods=['GET'])
|
||||
@token_required
|
||||
def get_job(job_id):
|
||||
"""Get a specific job"""
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
|
||||
# Check permissions
|
||||
is_admin = request.user_role == 'admin'
|
||||
user_id = request.user_id
|
||||
|
||||
if not is_admin and job.user_id != user_id:
|
||||
return jsonify({'error': 'Not authorized to view this job'}), 403
|
||||
|
||||
return jsonify(job.to_dict())
|
||||
|
||||
@bp.route('/jobs', methods=['POST'])
|
||||
@token_required
|
||||
def create_job():
|
||||
"""Create a new print job (reserve a printer)"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
required_fields = ['printer_id', 'start_at', 'duration_in_minutes']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||
|
||||
# Validate printer
|
||||
printer = Printer.query.get(data['printer_id'])
|
||||
if not printer:
|
||||
return jsonify({'error': 'Printer not found'}), 404
|
||||
|
||||
if printer.status != 0: # Not operational
|
||||
return jsonify({'error': 'Printer is not operational'}), 400
|
||||
|
||||
# Parse start time
|
||||
try:
|
||||
start_at = datetime.fromisoformat(data['start_at'].replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Invalid start_at format'}), 400
|
||||
|
||||
# Validate duration
|
||||
try:
|
||||
duration = int(data['duration_in_minutes'])
|
||||
if duration <= 0 or duration > 480: # Max 8 hours
|
||||
return jsonify({'error': 'Invalid duration (must be between 1 and 480 minutes)'}), 400
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Duration must be a number'}), 400
|
||||
|
||||
end_at = start_at + timedelta(minutes=duration)
|
||||
|
||||
# Check if the printer is available during the requested time
|
||||
conflicting_jobs = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(
|
||||
(PrintJob.start_at < end_at) &
|
||||
(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > start_at)
|
||||
) \
|
||||
.all()
|
||||
|
||||
if conflicting_jobs:
|
||||
return jsonify({'error': 'Printer is not available during the requested time'}), 409
|
||||
|
||||
# Create job
|
||||
job = PrintJob(
|
||||
printer_id=data['printer_id'],
|
||||
user_id=request.user_id,
|
||||
start_at=start_at,
|
||||
duration_in_minutes=duration,
|
||||
comments=data.get('comments', '')
|
||||
)
|
||||
|
||||
db.session.add(job)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(job.to_dict()), 201
|
||||
|
||||
@bp.route('/jobs/<job_id>', methods=['PUT'])
|
||||
@token_required
|
||||
def update_job(job_id):
|
||||
"""Update a job"""
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
|
||||
# Check permissions
|
||||
is_admin = request.user_role == 'admin'
|
||||
user_id = request.user_id
|
||||
|
||||
if not is_admin and job.user_id != user_id:
|
||||
return jsonify({'error': 'Not authorized to update this job'}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Only allow certain fields to be updated
|
||||
if 'comments' in data:
|
||||
job.comments = data['comments']
|
||||
|
||||
# Admin or owner can abort a job
|
||||
if 'aborted' in data and data['aborted'] and not job.aborted:
|
||||
job.aborted = True
|
||||
job.abort_reason = data.get('abort_reason', '')
|
||||
|
||||
# Admin or owner can extend a job if it's active
|
||||
now = datetime.utcnow()
|
||||
is_active = (not job.aborted and
|
||||
job.start_at <= now and
|
||||
job.get_end_time() > now)
|
||||
|
||||
if 'extend_minutes' in data and is_active:
|
||||
try:
|
||||
extend_minutes = int(data['extend_minutes'])
|
||||
if extend_minutes <= 0 or extend_minutes > 120: # Max extend 2 hours
|
||||
return jsonify({'error': 'Invalid extension (must be between 1 and 120 minutes)'}), 400
|
||||
|
||||
new_end_time = job.get_end_time() + timedelta(minutes=extend_minutes)
|
||||
|
||||
# Check for conflicts with the extension
|
||||
conflicting_jobs = PrintJob.query.filter_by(printer_id=job.printer_id, aborted=False) \
|
||||
.filter(PrintJob.id != job.id) \
|
||||
.filter(PrintJob.start_at < new_end_time) \
|
||||
.filter(PrintJob.start_at > job.get_end_time()) \
|
||||
.all()
|
||||
|
||||
if conflicting_jobs:
|
||||
return jsonify({'error': 'Cannot extend job due to conflicts with other reservations'}), 409
|
||||
|
||||
job.duration_in_minutes += extend_minutes
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Extend minutes must be a number'}), 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(job.to_dict())
|
||||
|
||||
@bp.route('/jobs/<job_id>', methods=['DELETE'])
|
||||
@token_required
|
||||
def delete_job(job_id):
|
||||
"""Delete a job (cancel reservation)"""
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
|
||||
# Check permissions
|
||||
is_admin = request.user_role == 'admin'
|
||||
user_id = request.user_id
|
||||
|
||||
if not is_admin and job.user_id != user_id:
|
||||
return jsonify({'error': 'Not authorized to delete this job'}), 403
|
||||
|
||||
# Only allow deletion of upcoming jobs
|
||||
now = datetime.utcnow()
|
||||
if job.start_at <= now and not is_admin:
|
||||
return jsonify({'error': 'Cannot delete an active or completed job'}), 400
|
||||
|
||||
db.session.delete(job)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Job deleted successfully'})
|
||||
|
||||
@bp.route('/jobs/<job_id>/remaining-time', methods=['GET'])
|
||||
def get_remaining_time(job_id):
|
||||
"""Get remaining time for a job (public endpoint)"""
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
|
||||
remaining_seconds = job.get_remaining_time()
|
||||
|
||||
return jsonify({
|
||||
'job_id': job.id,
|
||||
'remaining_seconds': remaining_seconds,
|
||||
'is_active': job.is_active()
|
||||
})
|
177
archiv/flask-backend/app/api/printers.py
Normal file
177
archiv/flask-backend/app/api/printers.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from flask import request, jsonify
|
||||
from app import db
|
||||
from app.api import bp
|
||||
from app.models import Printer, PrintJob
|
||||
from app.auth.routes import token_required, admin_required
|
||||
from datetime import datetime
|
||||
|
||||
@bp.route('/printers', methods=['GET'])
|
||||
def get_printers():
|
||||
"""Get all printers"""
|
||||
printers = Printer.query.all()
|
||||
result = []
|
||||
|
||||
for printer in printers:
|
||||
# Get active job for the printer if any
|
||||
now = datetime.utcnow()
|
||||
active_job = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(PrintJob.start_at <= now) \
|
||||
.filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > now) \
|
||||
.first()
|
||||
|
||||
printer_data = {
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'description': printer.description,
|
||||
'status': printer.status,
|
||||
'is_available': printer.status == 0 and active_job is None,
|
||||
'active_job': active_job.to_dict() if active_job else None
|
||||
}
|
||||
result.append(printer_data)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/printers/<printer_id>', methods=['GET'])
|
||||
def get_printer(printer_id):
|
||||
"""Get a specific printer"""
|
||||
printer = Printer.query.get_or_404(printer_id)
|
||||
|
||||
# Get active job for the printer if any
|
||||
now = datetime.utcnow()
|
||||
active_job = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(PrintJob.start_at <= now) \
|
||||
.filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > now) \
|
||||
.first()
|
||||
|
||||
# Get upcoming jobs
|
||||
upcoming_jobs = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(PrintJob.start_at > now) \
|
||||
.order_by(PrintJob.start_at) \
|
||||
.limit(5) \
|
||||
.all()
|
||||
|
||||
result = {
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'description': printer.description,
|
||||
'status': printer.status,
|
||||
'is_available': printer.status == 0 and active_job is None,
|
||||
'active_job': active_job.to_dict() if active_job else None,
|
||||
'upcoming_jobs': [job.to_dict() for job in upcoming_jobs]
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/printers', methods=['POST'])
|
||||
@admin_required
|
||||
def create_printer():
|
||||
"""Create a new printer (admin only)"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
required_fields = ['name', 'description']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||
|
||||
printer = Printer(
|
||||
name=data['name'],
|
||||
description=data['description'],
|
||||
status=data.get('status', 0)
|
||||
)
|
||||
|
||||
db.session.add(printer)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'description': printer.description,
|
||||
'status': printer.status
|
||||
}), 201
|
||||
|
||||
@bp.route('/printers/<printer_id>', methods=['PUT'])
|
||||
@admin_required
|
||||
def update_printer(printer_id):
|
||||
"""Update a printer (admin only)"""
|
||||
printer = Printer.query.get_or_404(printer_id)
|
||||
data = request.get_json() or {}
|
||||
|
||||
if 'name' in data:
|
||||
printer.name = data['name']
|
||||
if 'description' in data:
|
||||
printer.description = data['description']
|
||||
if 'status' in data:
|
||||
printer.status = data['status']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'description': printer.description,
|
||||
'status': printer.status
|
||||
})
|
||||
|
||||
@bp.route('/printers/<printer_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def delete_printer(printer_id):
|
||||
"""Delete a printer (admin only)"""
|
||||
printer = Printer.query.get_or_404(printer_id)
|
||||
|
||||
# Check if the printer has active jobs
|
||||
now = datetime.utcnow()
|
||||
active_jobs = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(PrintJob.start_at <= now) \
|
||||
.filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > now) \
|
||||
.all()
|
||||
|
||||
if active_jobs:
|
||||
return jsonify({'error': 'Cannot delete printer with active jobs'}), 400
|
||||
|
||||
db.session.delete(printer)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Printer deleted successfully'})
|
||||
|
||||
@bp.route('/printers/availability', methods=['GET'])
|
||||
def get_availability():
|
||||
"""Get availability information for all printers"""
|
||||
start_date = request.args.get('start_date')
|
||||
end_date = request.args.get('end_date')
|
||||
|
||||
if not start_date or not end_date:
|
||||
return jsonify({'error': 'start_date and end_date are required'}), 400
|
||||
|
||||
try:
|
||||
start = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
||||
end = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Invalid date format'}), 400
|
||||
|
||||
if start >= end:
|
||||
return jsonify({'error': 'start_date must be before end_date'}), 400
|
||||
|
||||
printers = Printer.query.all()
|
||||
result = []
|
||||
|
||||
for printer in printers:
|
||||
# Get all jobs for this printer in the date range
|
||||
jobs = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(
|
||||
(PrintJob.start_at <= end) &
|
||||
(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) >= start)
|
||||
) \
|
||||
.order_by(PrintJob.start_at) \
|
||||
.all()
|
||||
|
||||
# Convert to availability slots
|
||||
availability = {
|
||||
'printer_id': printer.id,
|
||||
'printer_name': printer.name,
|
||||
'status': printer.status,
|
||||
'jobs': [job.to_dict() for job in jobs]
|
||||
}
|
||||
|
||||
result.append(availability)
|
||||
|
||||
return jsonify(result)
|
139
archiv/flask-backend/app/api/users.py
Normal file
139
archiv/flask-backend/app/api/users.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from flask import request, jsonify
|
||||
from app import db
|
||||
from app.api import bp
|
||||
from app.models import User, PrintJob
|
||||
from app.auth.routes import admin_required, token_required
|
||||
|
||||
@bp.route('/users', methods=['GET'])
|
||||
@admin_required
|
||||
def get_users():
|
||||
"""Get all users (admin only)"""
|
||||
users = User.query.all()
|
||||
result = []
|
||||
|
||||
for user in users:
|
||||
# Count jobs
|
||||
total_jobs = PrintJob.query.filter_by(user_id=user.id).count()
|
||||
active_jobs = PrintJob.query.filter_by(user_id=user.id, aborted=False).count()
|
||||
|
||||
user_data = {
|
||||
'id': user.id,
|
||||
'github_id': user.github_id,
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'email': user.email,
|
||||
'role': user.role,
|
||||
'job_count': total_jobs,
|
||||
'active_job_count': active_jobs
|
||||
}
|
||||
result.append(user_data)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/users/<user_id>', methods=['GET'])
|
||||
@admin_required
|
||||
def get_user(user_id):
|
||||
"""Get a specific user (admin only)"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
# Count jobs
|
||||
total_jobs = PrintJob.query.filter_by(user_id=user.id).count()
|
||||
active_jobs = PrintJob.query.filter_by(user_id=user.id, aborted=False).count()
|
||||
|
||||
result = {
|
||||
'id': user.id,
|
||||
'github_id': user.github_id,
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'email': user.email,
|
||||
'role': user.role,
|
||||
'job_count': total_jobs,
|
||||
'active_job_count': active_jobs
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/users/<user_id>', methods=['PUT'])
|
||||
@admin_required
|
||||
def update_user(user_id):
|
||||
"""Update a user (admin only)"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
data = request.get_json() or {}
|
||||
|
||||
if 'role' in data and data['role'] in ['admin', 'user', 'guest']:
|
||||
user.role = data['role']
|
||||
|
||||
if 'display_name' in data:
|
||||
user.display_name = data['display_name']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'id': user.id,
|
||||
'github_id': user.github_id,
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'email': user.email,
|
||||
'role': user.role
|
||||
})
|
||||
|
||||
@bp.route('/users/<user_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def delete_user(user_id):
|
||||
"""Delete a user (admin only)"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
# Check if user has active jobs
|
||||
active_jobs = PrintJob.query.filter_by(user_id=user.id, aborted=False).first()
|
||||
if active_jobs:
|
||||
return jsonify({'error': 'Cannot delete user with active jobs'}), 400
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'User deleted successfully'})
|
||||
|
||||
@bp.route('/me', methods=['GET'])
|
||||
@token_required
|
||||
def get_current_user():
|
||||
"""Get the current user's profile"""
|
||||
user = User.query.get(request.user_id)
|
||||
if not user:
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
|
||||
result = {
|
||||
'id': user.id,
|
||||
'github_id': user.github_id,
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'email': user.email,
|
||||
'role': user.role
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/me', methods=['PUT'])
|
||||
@token_required
|
||||
def update_current_user():
|
||||
"""Update the current user's profile"""
|
||||
user = User.query.get(request.user_id)
|
||||
if not user:
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
if 'display_name' in data:
|
||||
user.display_name = data['display_name']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
result = {
|
||||
'id': user.id,
|
||||
'github_id': user.github_id,
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'email': user.email,
|
||||
'role': user.role
|
||||
}
|
||||
|
||||
return jsonify(result)
|
5
archiv/flask-backend/app/auth/__init__.py
Normal file
5
archiv/flask-backend/app/auth/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('auth', __name__)
|
||||
|
||||
from app.auth import routes
|
156
archiv/flask-backend/app/auth/routes.py
Normal file
156
archiv/flask-backend/app/auth/routes.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from flask import request, jsonify, current_app
|
||||
from app import db
|
||||
from app.auth import bp
|
||||
from app.models import User, Session
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
import functools
|
||||
import re
|
||||
|
||||
@bp.route('/register', methods=['POST'])
|
||||
def register():
|
||||
"""Register a new user"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Validate required fields
|
||||
required_fields = ['username', 'email', 'password']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||
|
||||
# Validate email format
|
||||
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
if not re.match(email_regex, data['email']):
|
||||
return jsonify({'error': 'Invalid email format'}), 400
|
||||
|
||||
# Validate password strength (at least 8 characters)
|
||||
if len(data['password']) < 8:
|
||||
return jsonify({'error': 'Password must be at least 8 characters long'}), 400
|
||||
|
||||
# Check if username already exists
|
||||
if User.query.filter_by(username=data['username']).first():
|
||||
return jsonify({'error': 'Username already exists'}), 400
|
||||
|
||||
# Check if email already exists
|
||||
if User.query.filter_by(email=data['email']).first():
|
||||
return jsonify({'error': 'Email already exists'}), 400
|
||||
|
||||
# Create new user
|
||||
user = User(
|
||||
username=data['username'],
|
||||
email=data['email'],
|
||||
display_name=data.get('display_name', data['username']),
|
||||
role='user' # Default role
|
||||
)
|
||||
user.set_password(data['password'])
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'display_name': user.display_name,
|
||||
'role': user.role
|
||||
}), 201
|
||||
|
||||
@bp.route('/login', methods=['POST'])
|
||||
def login():
|
||||
"""Login a user with username/email and password"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Validate required fields
|
||||
if 'password' not in data:
|
||||
return jsonify({'error': 'Password is required'}), 400
|
||||
|
||||
if 'username' not in data and 'email' not in data:
|
||||
return jsonify({'error': 'Username or email is required'}), 400
|
||||
|
||||
# Find user by username or email
|
||||
user = None
|
||||
if 'username' in data:
|
||||
user = User.query.filter_by(username=data['username']).first()
|
||||
else:
|
||||
user = User.query.filter_by(email=data['email']).first()
|
||||
|
||||
# Check if user exists and verify password
|
||||
if not user or not user.check_password(data['password']):
|
||||
return jsonify({'error': 'Invalid credentials'}), 401
|
||||
|
||||
# Create a session for the user
|
||||
expires_at = int((datetime.utcnow() + timedelta(days=7)).timestamp())
|
||||
session = Session(
|
||||
user_id=user.id,
|
||||
expires_at=expires_at
|
||||
)
|
||||
db.session.add(session)
|
||||
db.session.commit()
|
||||
|
||||
# Generate JWT token
|
||||
token = user.generate_token()
|
||||
|
||||
return jsonify({
|
||||
'token': token,
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'display_name': user.display_name,
|
||||
'role': user.role
|
||||
}
|
||||
})
|
||||
|
||||
@bp.route('/logout', methods=['POST'])
|
||||
def logout():
|
||||
"""Log out a user by invalidating their session"""
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return jsonify({'error': 'Authorization header required'}), 401
|
||||
|
||||
token = auth_header.split(' ')[1]
|
||||
payload = User.verify_token(token)
|
||||
if not payload:
|
||||
return jsonify({'error': 'Invalid token'}), 401
|
||||
|
||||
# Delete all sessions for this user
|
||||
Session.query.filter_by(user_id=payload['user_id']).delete()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Successfully logged out'})
|
||||
|
||||
def token_required(f):
|
||||
@functools.wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return jsonify({'error': 'Authorization header required'}), 401
|
||||
|
||||
token = auth_header.split(' ')[1]
|
||||
payload = User.verify_token(token)
|
||||
if not payload:
|
||||
return jsonify({'error': 'Invalid token'}), 401
|
||||
|
||||
# Check if user has an active session
|
||||
user_id = payload['user_id']
|
||||
current_time = int(time.time())
|
||||
session = Session.query.filter_by(user_id=user_id).filter(Session.expires_at > current_time).first()
|
||||
if not session:
|
||||
return jsonify({'error': 'No active session found'}), 401
|
||||
|
||||
# Add user to request context
|
||||
request.user_id = user_id
|
||||
request.user_role = payload['role']
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
def admin_required(f):
|
||||
@functools.wraps(f)
|
||||
@token_required
|
||||
def decorated(*args, **kwargs):
|
||||
if request.user_role != 'admin':
|
||||
return jsonify({'error': 'Admin privileges required'}), 403
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
124
archiv/flask-backend/app/models.py
Normal file
124
archiv/flask-backend/app/models.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from app import db
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
import jwt
|
||||
from config import Config
|
||||
import bcrypt
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = 'user'
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
username = db.Column(db.String(64), index=True, unique=True, nullable=False)
|
||||
display_name = db.Column(db.String(120))
|
||||
email = db.Column(db.String(120), index=True, unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
role = db.Column(db.String(20), default='user')
|
||||
|
||||
print_jobs = db.relationship('PrintJob', backref='user', lazy='dynamic', cascade='all, delete-orphan')
|
||||
sessions = db.relationship('Session', backref='user', lazy='dynamic', cascade='all, delete-orphan')
|
||||
|
||||
def set_password(self, password):
|
||||
"""Hash and set the user's password"""
|
||||
password_bytes = password.encode('utf-8')
|
||||
salt = bcrypt.gensalt()
|
||||
self.password_hash = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
|
||||
|
||||
def check_password(self, password):
|
||||
"""Check if the provided password matches the stored hash"""
|
||||
password_bytes = password.encode('utf-8')
|
||||
stored_hash = self.password_hash.encode('utf-8')
|
||||
return bcrypt.checkpw(password_bytes, stored_hash)
|
||||
|
||||
def generate_token(self):
|
||||
"""Generate a JWT token for this user"""
|
||||
payload = {
|
||||
'user_id': self.id,
|
||||
'username': self.username,
|
||||
'email': self.email,
|
||||
'role': self.role,
|
||||
'exp': datetime.utcnow() + timedelta(seconds=Config.JWT_ACCESS_TOKEN_EXPIRES)
|
||||
}
|
||||
return jwt.encode(payload, Config.JWT_SECRET, algorithm='HS256')
|
||||
|
||||
@staticmethod
|
||||
def verify_token(token):
|
||||
"""Verify and decode a JWT token"""
|
||||
try:
|
||||
payload = jwt.decode(token, Config.JWT_SECRET, algorithms=['HS256'])
|
||||
return payload
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class Session(db.Model):
|
||||
__tablename__ = 'session'
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = db.Column(db.String(36), db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
|
||||
expires_at = db.Column(db.Integer, nullable=False)
|
||||
|
||||
|
||||
class Printer(db.Model):
|
||||
__tablename__ = 'printer'
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
name = db.Column(db.String(120), nullable=False)
|
||||
description = db.Column(db.Text, nullable=False)
|
||||
status = db.Column(db.Integer, nullable=False, default=0) # 0: OPERATIONAL, 1: OUT_OF_ORDER
|
||||
|
||||
print_jobs = db.relationship('PrintJob', backref='printer', lazy='dynamic', cascade='all, delete-orphan')
|
||||
|
||||
|
||||
class PrintJob(db.Model):
|
||||
__tablename__ = 'printJob'
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
printer_id = db.Column(db.String(36), db.ForeignKey('printer.id', ondelete='CASCADE'), nullable=False)
|
||||
user_id = db.Column(db.String(36), db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
|
||||
start_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
duration_in_minutes = db.Column(db.Integer, nullable=False)
|
||||
comments = db.Column(db.Text)
|
||||
aborted = db.Column(db.Boolean, nullable=False, default=False)
|
||||
abort_reason = db.Column(db.Text)
|
||||
|
||||
def get_end_time(self):
|
||||
return self.start_at + timedelta(minutes=self.duration_in_minutes)
|
||||
|
||||
def is_active(self):
|
||||
now = datetime.utcnow()
|
||||
return (not self.aborted and
|
||||
self.start_at <= now and
|
||||
now < self.get_end_time())
|
||||
|
||||
def get_remaining_time(self):
|
||||
if self.aborted:
|
||||
return 0
|
||||
|
||||
now = datetime.utcnow()
|
||||
if now < self.start_at:
|
||||
# Job hasn't started yet
|
||||
return self.duration_in_minutes * 60
|
||||
|
||||
end_time = self.get_end_time()
|
||||
if now >= end_time:
|
||||
# Job has ended
|
||||
return 0
|
||||
|
||||
# Job is ongoing
|
||||
remaining_seconds = (end_time - now).total_seconds()
|
||||
return int(remaining_seconds)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'printer_id': self.printer_id,
|
||||
'user_id': self.user_id,
|
||||
'start_at': self.start_at.isoformat(),
|
||||
'duration_in_minutes': self.duration_in_minutes,
|
||||
'comments': self.comments,
|
||||
'aborted': self.aborted,
|
||||
'abort_reason': self.abort_reason,
|
||||
'remaining_time': self.get_remaining_time(),
|
||||
'is_active': self.is_active()
|
||||
}
|
13
archiv/flask-backend/config.py
Normal file
13
archiv/flask-backend/config.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
load_dotenv(os.path.join(basedir, '.env'))
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
|
||||
'sqlite:///' + os.path.join(basedir, 'app.db')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
JWT_SECRET = os.environ.get('JWT_SECRET') or 'jwt-secret-key'
|
||||
JWT_ACCESS_TOKEN_EXPIRES = 3600 # 1 hour in seconds
|
20
archiv/flask-backend/docker-compose.yml
Normal file
20
archiv/flask-backend/docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
flask-backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- SECRET_KEY=your-secret-key
|
||||
- DATABASE_URL=sqlite:///app.db
|
||||
- JWT_SECRET=your-jwt-secret
|
||||
volumes:
|
||||
- ./instance:/app/instance
|
||||
command: >
|
||||
bash -c "python -m flask db upgrade &&
|
||||
python scripts/init_db.py &&
|
||||
gunicorn --bind 0.0.0.0:5000 wsgi:app"
|
89
archiv/flask-backend/migrations/alembic.ini
Normal file
89
archiv/flask-backend/migrations/alembic.ini
Normal file
@@ -0,0 +1,89 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = migrations
|
||||
|
||||
# template used to generate migration files
|
||||
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date
|
||||
# within the migration file as well as the filename.
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to migrations/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path
|
||||
# version_locations = %(here)s/bar %(here)s/bat migrations/versions
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
91
archiv/flask-backend/migrations/env.py
Normal file
91
archiv/flask-backend/migrations/env.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
str(current_app.extensions['migrate'].db.get_engine().url).replace(
|
||||
'%', '%%'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
connectable = current_app.extensions['migrate'].db.get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
archiv/flask-backend/migrations/script.py.mako
Normal file
24
archiv/flask-backend/migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
@@ -0,0 +1,75 @@
|
||||
"""Initial migration
|
||||
|
||||
Revision ID: initial_migration
|
||||
Revises:
|
||||
Create Date: 2025-03-06 12:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'initial_migration'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Create user table
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('username', sa.String(length=64), nullable=False),
|
||||
sa.Column('display_name', sa.String(length=120), nullable=True),
|
||||
sa.Column('email', sa.String(length=120), nullable=False),
|
||||
sa.Column('password_hash', sa.String(length=128), nullable=False),
|
||||
sa.Column('role', sa.String(length=20), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
|
||||
op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True)
|
||||
|
||||
# Create session table
|
||||
op.create_table('session',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('user_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('expires_at', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
# Create printer table
|
||||
op.create_table('printer',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('name', sa.String(length=120), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=False),
|
||||
sa.Column('status', sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
# Create printJob table
|
||||
op.create_table('printJob',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('printer_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('user_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('start_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('duration_in_minutes', sa.Integer(), nullable=False),
|
||||
sa.Column('comments', sa.Text(), nullable=True),
|
||||
sa.Column('aborted', sa.Boolean(), nullable=False),
|
||||
sa.Column('abort_reason', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['printer_id'], ['printer.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('printJob')
|
||||
op.drop_table('printer')
|
||||
op.drop_table('session')
|
||||
op.drop_index(op.f('ix_user_username'), table_name='user')
|
||||
op.drop_index(op.f('ix_user_email'), table_name='user')
|
||||
op.drop_table('user')
|
9
archiv/flask-backend/requirements.txt
Normal file
9
archiv/flask-backend/requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
Flask==2.3.3
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
Flask-Migrate==4.0.5
|
||||
Flask-CORS==4.0.0
|
||||
python-dotenv==1.0.0
|
||||
SQLAlchemy==2.0.25
|
||||
pyjwt==2.8.0
|
||||
bcrypt==4.1.2
|
||||
gunicorn==21.2.0
|
23
archiv/flask-backend/run.sh
Normal file
23
archiv/flask-backend/run.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Initialize virtual environment if it doesn't exist
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Initialize database
|
||||
echo "Initializing database..."
|
||||
flask db upgrade
|
||||
python scripts/init_db.py
|
||||
|
||||
# Run the application
|
||||
echo "Starting Flask application..."
|
||||
python wsgi.py
|
55
archiv/flask-backend/scripts/init_db.py
Normal file
55
archiv/flask-backend/scripts/init_db.py
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python
|
||||
from app import create_app, db
|
||||
from app.models import User, Printer
|
||||
import uuid
|
||||
|
||||
def init_db():
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
# Create tables
|
||||
db.create_all()
|
||||
|
||||
# Check if we already have an admin user
|
||||
admin = User.query.filter_by(role='admin').first()
|
||||
if not admin:
|
||||
# Create admin user
|
||||
admin = User(
|
||||
id=str(uuid.uuid4()),
|
||||
username='admin',
|
||||
display_name='Administrator',
|
||||
email='admin@example.com',
|
||||
role='admin'
|
||||
)
|
||||
admin.set_password('admin123') # Default password, change in production!
|
||||
db.session.add(admin)
|
||||
print("Created admin user with username 'admin' and password 'admin123'")
|
||||
|
||||
# Check if we have any printers
|
||||
printer_count = Printer.query.count()
|
||||
if printer_count == 0:
|
||||
# Create sample printers
|
||||
printers = [
|
||||
Printer(
|
||||
name='Printer 1',
|
||||
description='3D Printer for general use',
|
||||
status=0 # OPERATIONAL
|
||||
),
|
||||
Printer(
|
||||
name='Printer 2',
|
||||
description='High resolution printer for detailed work',
|
||||
status=0 # OPERATIONAL
|
||||
),
|
||||
Printer(
|
||||
name='Printer 3',
|
||||
description='Large format printer for big projects',
|
||||
status=0 # OPERATIONAL
|
||||
)
|
||||
]
|
||||
db.session.add_all(printers)
|
||||
print("Created sample printers")
|
||||
|
||||
db.session.commit()
|
||||
print("Database initialized successfully!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
6
archiv/flask-backend/wsgi.py
Normal file
6
archiv/flask-backend/wsgi.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
48
backend/.gitignore
vendored
Normal file
48
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Flask
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# SQLite Datenbank-Dateien
|
||||
*.db
|
||||
*.db-journal
|
||||
|
||||
# Virtuelle Umgebungen
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Betriebssystem
|
||||
.DS_Store
|
||||
Thumbs.db
|
18
backend/Dockerfile
Normal file
18
backend/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p logs
|
||||
|
||||
ENV FLASK_APP=app.py
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
1719
backend/app.py
Normal file
1719
backend/app.py
Normal file
File diff suppressed because it is too large
Load Diff
8
backend/development/crontab-example
Normal file
8
backend/development/crontab-example
Normal file
@@ -0,0 +1,8 @@
|
||||
# MYP Backend Cron-Jobs
|
||||
# Installiere mit: crontab crontab-example
|
||||
|
||||
# Prüfe alle 5 Minuten auf abgelaufene Reservierungen und schalte Steckdosen aus
|
||||
*/5 * * * * cd /pfad/zum/projektarbeit-myp/backend && /pfad/zur/venv/bin/flask check-jobs >> /pfad/zum/projektarbeit-myp/backend/logs/cron.log 2>&1
|
||||
|
||||
# Tägliche Sicherung der Datenbank um 3:00 Uhr
|
||||
0 3 * * * cd /pfad/zum/projektarbeit-myp/backend && cp instance/myp.db instance/backups/myp-$(date +\%Y\%m\%d).db
|
84
backend/development/initialize_myp_database.sh
Normal file
84
backend/development/initialize_myp_database.sh
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
|
||||
# MYP Datenbank Initialisierungs-Skript
|
||||
# Dieses Skript erstellt die erforderlichen Datenbanktabellen für das MYP Backend
|
||||
|
||||
echo "=== MYP Datenbank Initialisierung ==="
|
||||
echo ""
|
||||
|
||||
# Prüfe, ob sqlite3 installiert ist
|
||||
if ! command -v sqlite3 &> /dev/null; then
|
||||
echo "FEHLER: sqlite3 ist nicht installiert."
|
||||
echo "Bitte installiere sqlite3 mit deinem Paketmanager, z.B. 'apt install sqlite3'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Erstelle Instance-Ordner, falls nicht vorhanden
|
||||
echo "Erstelle instance-Ordner, falls nicht vorhanden..."
|
||||
mkdir -p instance/backups
|
||||
|
||||
# Prüfen, ob die Datenbank bereits existiert
|
||||
if [ -f "instance/myp.db" ]; then
|
||||
echo "Datenbank existiert bereits."
|
||||
echo "Erstelle Backup in instance/backups..."
|
||||
cp instance/myp.db "instance/backups/myp_$(date '+%Y%m%d_%H%M%S').db"
|
||||
fi
|
||||
|
||||
# Erstelle die Datenbank und ihre Tabellen
|
||||
echo "Erstelle neue Datenbank..."
|
||||
sqlite3 instance/myp.db <<EOF
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
email TEXT UNIQUE,
|
||||
role TEXT DEFAULT 'user'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS socket (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0,
|
||||
ip_address TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS job (
|
||||
id TEXT PRIMARY KEY,
|
||||
socket_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
start_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
duration_in_minutes INTEGER NOT NULL,
|
||||
comments TEXT,
|
||||
aborted INTEGER DEFAULT 0,
|
||||
abort_reason TEXT,
|
||||
FOREIGN KEY (socket_id) REFERENCES socket (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
EOF
|
||||
|
||||
# Setze Berechtigungen für die Datenbankdatei
|
||||
chmod 644 instance/myp.db
|
||||
|
||||
echo ""
|
||||
echo "=== Datenbank-Initialisierung abgeschlossen ==="
|
||||
echo ""
|
||||
echo "Du kannst jetzt einen Admin-Benutzer über die Web-Oberfläche registrieren."
|
||||
echo "Der erste registrierte Benutzer wird automatisch zum Admin."
|
||||
echo ""
|
||||
echo "Starte den Server mit:"
|
||||
echo "python app.py"
|
||||
echo ""
|
||||
echo "Alternativ kannst du einen Admin-Benutzer über die API erstellen mit:"
|
||||
echo "curl -X POST http://localhost:5000/api/create-initial-admin -H \"Content-Type: application/json\" -d '{\"username\":\"admin\",\"password\":\"password\",\"displayName\":\"Administrator\"}'"
|
||||
echo ""
|
73
backend/development/install.sh
Normal file
73
backend/development/install.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Installation Script für MYP Backend
|
||||
|
||||
echo "=== MYP Backend Installation ==="
|
||||
echo ""
|
||||
|
||||
# Prüfe Python-Version
|
||||
python_version=$(python3 --version 2>&1 | awk '{print $2}')
|
||||
echo "Python-Version: $python_version"
|
||||
|
||||
# Prüfe, ob die Python-Version mindestens 3.8 ist
|
||||
required_version="3.8.0"
|
||||
if [[ "$(printf '%s\n' "$required_version" "$python_version" | sort -V | head -n1)" != "$required_version" ]]; then
|
||||
echo "FEHLER: Python $required_version oder höher wird benötigt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prüfe, ob sqlite3 installiert ist
|
||||
if ! command -v sqlite3 &> /dev/null; then
|
||||
echo "FEHLER: sqlite3 ist nicht installiert."
|
||||
echo "Bitte installiere sqlite3 mit deinem Paketmanager, z.B. 'apt install sqlite3'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Erstelle virtuelle Umgebung
|
||||
echo ""
|
||||
echo "Erstelle virtuelle Python-Umgebung..."
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Installiere Abhängigkeiten
|
||||
echo ""
|
||||
echo "Installiere Abhängigkeiten..."
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Erstelle .env-Datei
|
||||
echo ""
|
||||
echo "Erstelle .env-Datei..."
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
echo "Die .env-Datei wurde aus der Beispieldatei erstellt."
|
||||
echo "Bitte passe die Konfiguration an, falls nötig."
|
||||
else
|
||||
echo ".env-Datei existiert bereits."
|
||||
fi
|
||||
|
||||
# Erstelle Logs-Ordner
|
||||
echo ""
|
||||
echo "Erstelle logs-Ordner..."
|
||||
mkdir -p logs
|
||||
|
||||
# Initialisiere die Datenbank
|
||||
echo ""
|
||||
echo "Initialisiere die Datenbank..."
|
||||
bash initialize_myp_database.sh
|
||||
|
||||
echo ""
|
||||
echo "=== Installation abgeschlossen ==="
|
||||
echo ""
|
||||
echo "Wichtige Schritte vor dem Start:"
|
||||
echo "1. Passe die Konfigurationen in der .env-Datei an"
|
||||
echo "2. Konfiguriere die Tapo-Steckdosen-Zugangsdaten in der .env-Datei (optional)"
|
||||
echo "3. Passe die crontab-example an und installiere den Cron-Job (optional)"
|
||||
echo ""
|
||||
echo "Starte den Server mit:"
|
||||
echo "source venv/bin/activate"
|
||||
echo "python app.py"
|
||||
echo ""
|
||||
echo "Oder mit Gunicorn für Produktion:"
|
||||
echo "gunicorn --bind 0.0.0.0:5000 app:app"
|
||||
echo ""
|
95
backend/development/tests/api-test.drucker.py
Normal file
95
backend/development/tests/api-test.drucker.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Basis-URL inkl. Token
|
||||
url = "http://192.168.0.102:80/app?token=9DFAC92C53CEC92E67A9CB2E00B3CB2F"
|
||||
|
||||
# HTTP-Header wie in der Originalanfrage
|
||||
headers = {
|
||||
"Referer": "http://192.168.0.102:80",
|
||||
"Accept": "application/json",
|
||||
"requestByApp": "true",
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"Host": "192.168.0.102",
|
||||
"Connection": "Keep-Alive",
|
||||
"Accept-Encoding": "gzip",
|
||||
"User-Agent": "okhttp/3.14.9"
|
||||
}
|
||||
|
||||
# Liste der Payloads (als Python-Dictionaries)
|
||||
payloads = [
|
||||
{
|
||||
"method": "securePassthrough",
|
||||
"params": {
|
||||
"request": (
|
||||
"ZC4CHp6bbfBO1rtmuH6I+TStBIiFRfQpayYPwet5NBmL35dib5xXHeEeLM7c0OSQSyxO6fnbXrC1\n"
|
||||
"gXdfowwwq4Fum9ispgt8yT7cgbDcqnoVrhxEtHIDfuwLh8YAGmDSfTMo/JlsGspWPYMKd1EWXtb5\n"
|
||||
"gP9FA9LHnV2kxKsNSPQ=\n"
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "securePassthrough",
|
||||
"params": {
|
||||
"request": (
|
||||
"k111EbfCcfVzAouNbu1vyos9Ltsg+a97n4xUUQMviQVJfhqxvKOhv1SrvEk2LvpD0LwNVUNPZdwU\n"
|
||||
"6pH5E/NOwdc1WzTPeqHiY760GpUuqn0tToHEHEyO2HaSKdrAYnw2gN410bvHb0pM3gYWS43eOA==\n"
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "securePassthrough",
|
||||
"params": {
|
||||
"request": (
|
||||
"7/uYVDwyNfFhg9y7rHyp+4AGKBYQPyaBN6cFMl9j4ER/JpJTcGBdaUteSmx8P8Fkz+b2kkNLjYa2\n"
|
||||
"wQr2gA3m6vEq9jpnAF2V3fv9c4Yg9gja9MlTIZqM6EdMi7YbfbhLme34Bh8kMcohDR3u1F4DwFDz\n"
|
||||
"hNZPckf/CegbY9KGFeGwT4rWyX3BTk9+FE7ldtJn\n"
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "securePassthrough",
|
||||
"params": {
|
||||
"request": (
|
||||
"EjWZb+YYS9tihgLdX4x+Wwx7q+e5X/ZHicr4jOnYmpFToDANzpm5ZpzD49BITcTCdQMOHlJBis85\n"
|
||||
"9GX6Hv8j66OITyH0XmfG9dQo2tgIykyagCZIofr/BpAWYX4aRaOkU4z14mVa2XpDtHJQjc+pXYkh\n"
|
||||
"JuWvLE+h01U5RoyPtvE=\n"
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "securePassthrough",
|
||||
"params": {
|
||||
"request": (
|
||||
"OwyTsm5HdB/ReJMhVRrkjnV0NLTanw6iXOxVrDDexT456edWuwKiBOsZUyBHmUyJKgiPQzOXqyWWi220bX8IjLX4q8YNgPwRlj+7nRbfzpC/I57wBZBTWIt626pSdIH0vpiuPq84KMfPD5BB2p78/LjsqlzyeLGYzkSsGRBMT8TnLMDFzZE864nfDUZ9muH2kk8NRMN9l6xoCXBJqGA9q8XxIWRTpsl0kTx52kUszY69hYlfFSrrCDIls1ykul14/T1NtOVF8KOgiwaSGOZf7L4QlbhYvRj9kkVVkrxhlwt8jtMqfJKEqq+CIPh3Mp4440WYMLRo6VNIEJ3pWjplkJmc+htnYC4FwVgT7mHZ8eeGGKBvsJz+78gTaHnGBnwZ26I8UdFparyp6QXpOhK9zFmGVh0yapiTHo6jOOI+4Q3Ru+aPnidX/ZASPmR7CZO70CUpvv9zIKJnrAaoTMmH7A6+kcmCRLgLFaTaM+4DFmiz6JGP+4W7MmVPJxxvn0IFlo1P/xwNDuL3T6GLUIEVNk89JG5roBm7AdchUZJO38dGZ0eFiiTK/NhKPvjj+fk9A4FGh7EDshXZhL2u50cdLcdUtcP/CAMDjgWlMm4Kk3vxMQO+UGE+jsB7NkaulmTW1jcl+PSnAE5P71oqVVQ0ng==\n"
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "securePassthrough",
|
||||
"params": {
|
||||
"request": (
|
||||
"7/uYVDwyNfFhg9y7rHyp+4AGKBYQPyaBN6cFMl9j4ER/JpJTcGBdaUteSmx8P8FkURmv/LWV1FpO\n"
|
||||
"M3RWvsiC5UAsei2G+vwTVuQpOPjKKAx+qwftr9Qs2mSkPNjNLpWHK68EZkIw+h04TQkt0Q99Dirg\n"
|
||||
"0BcrPgHTVKjiK8mdZ6w6gcld/h/FOKYMqJrP0Z+2\n"
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "securePassthrough",
|
||||
"params": {
|
||||
"request": (
|
||||
"ZE/+XlUmTA9D3DFfp4x3xhS3vdsQ+60tz4TOodtZDby/4DPoqk9EBvJZ1JtUCr5c0AHuv/sfwcvN\n"
|
||||
"Vx1zJP9RkltrAKVTWoaESAeewLozpXt/x0s/jkYC1rh7eTrxm+nYTZ5LJgNtcQq8yJxhEPez1w==\n"
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# Sende die Payloads sequenziell per POST-Anfrage
|
||||
for idx, payload in enumerate(payloads, start=1):
|
||||
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
||||
print(f"Anfrage {idx}:")
|
||||
print("Status Code:", response.status_code)
|
||||
print("Response Text:", response.text)
|
||||
print("-" * 60)
|
BIN
backend/development/tests/capture.pcap
Normal file
BIN
backend/development/tests/capture.pcap
Normal file
Binary file not shown.
128
backend/development/tests/handshake.py
Normal file
128
backend/development/tests/handshake.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Constants from the Wireshark capture
|
||||
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCMl89OZsjqE8yZ9TQhUb9h539WTX3U8Y5YCNdp
|
||||
OhuXvLFYcAT5mvC074VFROmD0xhvw5hrwESOisqpPPU9r78JpLuYUKd+/aidvykqBT8OW5rDLb6d
|
||||
O9FO6Gc+bV8L8ttHVlBFoX69EqiRhcreGPG6FQz4JqGJF4T1nFi0EvALXwIDAQAB
|
||||
-----END PUBLIC KEY-----"""
|
||||
|
||||
# Vorbereitete verschlüsselte Befehle (aus Wireshark extrahiert)
|
||||
COMMAND_ON = """ps0Puxc37EK4PhfcevceL3lyyDrjwLT1+443DDXNbcNRsltlgCQ6+oXgsrE2Pl5OhV73ZI/oM5Nj
|
||||
37cWEaHpXPiHdr1W0cD3aJ5qJ55TfTRkHP9xcMNQJHCn6aWPEHpR7xvvXW9WbJWfShnE2Xdvmw==
|
||||
"""
|
||||
|
||||
COMMAND_OFF = """FlO5i3DRcrUmu2ZwIIv8b68EisGu8VCuqfGOydaR+xCA0n3f2W/EcqVj8MurRBFXYTrZ/uwa1W26
|
||||
ftCfvhdXNebBRwHr9Rj3id4bVfltJ8eT5/R3xY8kputklW2mrw9UfdISzAJqOPp9KZcU4K9p8g==
|
||||
"""
|
||||
|
||||
class TapoP115Controller:
|
||||
def __init__(self, device_ip):
|
||||
self.device_ip = device_ip
|
||||
self.session_id = None
|
||||
self.token = None
|
||||
|
||||
def perform_handshake(self):
|
||||
"""Führt den ersten Handshake durch und speichert die Session-ID"""
|
||||
handshake_data = {
|
||||
"method": "handshake",
|
||||
"params": {
|
||||
"key": PUBLIC_KEY
|
||||
},
|
||||
"requestTimeMils": 0
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Referer": f"http://{self.device_ip}:80",
|
||||
"Accept": "application/json",
|
||||
"requestByApp": "true",
|
||||
"Content-Type": "application/json; charset=UTF-8"
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"http://{self.device_ip}/app",
|
||||
json=handshake_data,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data["error_code"] == 0:
|
||||
# Session-ID aus dem Cookie extrahieren
|
||||
self.session_id = response.cookies.get("TP_SESSIONID")
|
||||
print(f"Handshake erfolgreich, Session-ID: {self.session_id}")
|
||||
|
||||
# In einem echten Szenario würden wir hier den verschlüsselten Schlüssel entschlüsseln
|
||||
# Da wir keinen privaten Schlüssel haben, speichern wir nur die Antwort
|
||||
encrypted_key = data["result"]["key"]
|
||||
print(f"Verschlüsselter Schlüssel: {encrypted_key}")
|
||||
return True
|
||||
|
||||
print("Handshake fehlgeschlagen")
|
||||
return False
|
||||
|
||||
def send_command(self, encrypted_command):
|
||||
"""Sendet einen vorbereiteten verschlüsselten Befehl"""
|
||||
if not self.session_id:
|
||||
print("Keine Session-ID. Bitte zuerst Handshake durchführen.")
|
||||
return None
|
||||
|
||||
# Token aus der Wireshark-Aufnahme (könnte sich ändern, oder vom Gerät abhängen)
|
||||
token = "9DFAC92C53CEC92E67A9CB2E00B3CB2F"
|
||||
|
||||
secure_data = {
|
||||
"method": "securePassthrough",
|
||||
"params": {
|
||||
"request": encrypted_command
|
||||
}
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Referer": f"http://{self.device_ip}:80",
|
||||
"Accept": "application/json",
|
||||
"requestByApp": "true",
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"Cookie": f"TP_SESSIONID={self.session_id}"
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"http://{self.device_ip}/app?token={token}",
|
||||
json=secure_data,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data["error_code"] == 0:
|
||||
# In einem echten Szenario würden wir die Antwort entschlüsseln
|
||||
encrypted_response = data["result"]["response"]
|
||||
print("Befehl erfolgreich gesendet")
|
||||
return encrypted_response
|
||||
|
||||
print("Fehler beim Senden des Befehls")
|
||||
return None
|
||||
|
||||
def turn_on(self):
|
||||
"""Schaltet die Steckdose ein"""
|
||||
return self.send_command(COMMAND_ON)
|
||||
|
||||
def turn_off(self):
|
||||
"""Schaltet die Steckdose aus"""
|
||||
return self.send_command(COMMAND_OFF)
|
||||
|
||||
# Verwendungsbeispiel
|
||||
if __name__ == "__main__":
|
||||
controller = TapoP115Controller("192.168.0.102")
|
||||
|
||||
# Handshake durchführen
|
||||
if controller.perform_handshake():
|
||||
# Steckdose einschalten
|
||||
controller.turn_on()
|
||||
|
||||
# Kurze Pause (im echten Code mit time.sleep)
|
||||
print("Steckdose ist eingeschaltet")
|
||||
|
||||
# Steckdose ausschalten
|
||||
controller.turn_off()
|
||||
print("Steckdose ist ausgeschaltet")
|
9
backend/development/tests/tapo.py
Normal file
9
backend/development/tests/tapo.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from PyP100 import PyP100
|
||||
|
||||
p100 = PyP100.P100("192.168.0.102", "till.tomczak@mercedes-benz.com", "Agent045") #Creates a P100 plug object
|
||||
|
||||
p100.handshake() #Creates the cookies required for further methods
|
||||
p100.login() #Sends credentials to the plug and creates AES Key and IV for further methods
|
||||
|
||||
p100.turnOn() #Turns the connected plug on
|
||||
p100.turnOff() #Turns the connected plug off
|
253
backend/development/tests/tests.py
Normal file
253
backend/development/tests/tests.py
Normal file
@@ -0,0 +1,253 @@
|
||||
import unittest
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app import app, db, User, Printer, PrintJob
|
||||
|
||||
class MYPBackendTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Temporäre Datenbank für Tests
|
||||
self.db_fd, app.config['DATABASE'] = tempfile.mkstemp()
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE']
|
||||
app.config['TESTING'] = True
|
||||
self.app = app.test_client()
|
||||
|
||||
# Datenbank-Tabellen erstellen und Test-Daten einfügen
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
# Admin-Benutzer erstellen
|
||||
admin = User(username='admin_test', email='admin@test.com', role='admin')
|
||||
admin.set_password('admin')
|
||||
db.session.add(admin)
|
||||
|
||||
# Normaler Benutzer erstellen
|
||||
user = User(username='user_test', email='user@test.com', role='user')
|
||||
user.set_password('user')
|
||||
db.session.add(user)
|
||||
|
||||
# Drucker erstellen
|
||||
printer1 = Printer(name='Printer 1', location='Room A', type='3D',
|
||||
status='available', description='Test printer 1')
|
||||
printer2 = Printer(name='Printer 2', location='Room B', type='3D',
|
||||
status='busy', description='Test printer 2')
|
||||
db.session.add(printer1)
|
||||
db.session.add(printer2)
|
||||
|
||||
# Job erstellen
|
||||
start_time = datetime.utcnow()
|
||||
end_time = start_time + timedelta(minutes=60)
|
||||
job = PrintJob(title='Test Job', start_time=start_time, end_time=end_time,
|
||||
duration=60, status='active', comments='Test job',
|
||||
user_id=2, printer_id=2)
|
||||
db.session.add(job)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def tearDown(self):
|
||||
# Aufräumen nach dem Test
|
||||
os.close(self.db_fd)
|
||||
os.unlink(app.config['DATABASE'])
|
||||
|
||||
def get_token(self, username, password):
|
||||
response = self.app.post('/api/auth/login',
|
||||
data=json.dumps({'username': username, 'password': password}),
|
||||
content_type='application/json')
|
||||
data = json.loads(response.data)
|
||||
return data.get('token')
|
||||
|
||||
def test_login(self):
|
||||
# Test: Erfolgreicher Login
|
||||
response = self.app.post('/api/auth/login',
|
||||
data=json.dumps({'username': 'admin_test', 'password': 'admin'}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('token', data)
|
||||
self.assertIn('user', data)
|
||||
|
||||
# Test: Fehlgeschlagener Login (falsches Passwort)
|
||||
response = self.app.post('/api/auth/login',
|
||||
data=json.dumps({'username': 'admin_test', 'password': 'wrong'}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_register(self):
|
||||
# Test: Erfolgreiche Registrierung
|
||||
response = self.app.post('/api/auth/register',
|
||||
data=json.dumps({
|
||||
'username': 'new_user',
|
||||
'email': 'new@test.com',
|
||||
'password': 'password'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# Test: Doppelte Registrierung
|
||||
response = self.app.post('/api/auth/register',
|
||||
data=json.dumps({
|
||||
'username': 'new_user',
|
||||
'email': 'another@test.com',
|
||||
'password': 'password'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_get_printers(self):
|
||||
# Test: Drucker abrufen
|
||||
response = self.app.get('/api/printers')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(len(data), 2)
|
||||
|
||||
def test_get_single_printer(self):
|
||||
# Test: Einzelnen Drucker abrufen
|
||||
response = self.app.get('/api/printers/1')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['name'], 'Printer 1')
|
||||
|
||||
def test_create_printer(self):
|
||||
# Als Admin einen Drucker erstellen
|
||||
token = self.get_token('admin_test', 'admin')
|
||||
response = self.app.post('/api/printers',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
data=json.dumps({
|
||||
'name': 'New Printer',
|
||||
'location': 'Room C',
|
||||
'type': '3D',
|
||||
'description': 'New test printer'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['name'], 'New Printer')
|
||||
|
||||
def test_update_printer(self):
|
||||
# Als Admin einen Drucker aktualisieren
|
||||
token = self.get_token('admin_test', 'admin')
|
||||
response = self.app.put('/api/printers/1',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
data=json.dumps({
|
||||
'name': 'Updated Printer',
|
||||
'location': 'Room D'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['name'], 'Updated Printer')
|
||||
self.assertEqual(data['location'], 'Room D')
|
||||
|
||||
def test_delete_printer(self):
|
||||
# Als Admin einen Drucker löschen
|
||||
token = self.get_token('admin_test', 'admin')
|
||||
response = self.app.delete('/api/printers/1',
|
||||
headers={'Authorization': f'Bearer {token}'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Überprüfen, ob der Drucker wirklich gelöscht wurde
|
||||
response = self.app.get('/api/printers/1')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_get_jobs_as_admin(self):
|
||||
# Als Admin alle Jobs abrufen
|
||||
token = self.get_token('admin_test', 'admin')
|
||||
response = self.app.get('/api/jobs',
|
||||
headers={'Authorization': f'Bearer {token}'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(len(data), 1)
|
||||
|
||||
def test_get_jobs_as_user(self):
|
||||
# Als normaler Benutzer nur eigene Jobs abrufen
|
||||
token = self.get_token('user_test', 'user')
|
||||
response = self.app.get('/api/jobs',
|
||||
headers={'Authorization': f'Bearer {token}'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(len(data), 1) # Der Benutzer hat einen Job
|
||||
|
||||
def test_create_job(self):
|
||||
# Als Benutzer einen Job erstellen
|
||||
token = self.get_token('user_test', 'user')
|
||||
response = self.app.post('/api/jobs',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
data=json.dumps({
|
||||
'title': 'New Job',
|
||||
'printer_id': 1,
|
||||
'duration': 30,
|
||||
'comments': 'Test job creation'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['title'], 'New Job')
|
||||
self.assertEqual(data['duration'], 30)
|
||||
|
||||
def test_update_job(self):
|
||||
# Als Benutzer den eigenen Job aktualisieren
|
||||
token = self.get_token('user_test', 'user')
|
||||
response = self.app.put('/api/jobs/1',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
data=json.dumps({
|
||||
'comments': 'Updated comments',
|
||||
'duration': 15 # Verlängerung
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['comments'], 'Updated comments')
|
||||
self.assertEqual(data['duration'], 75) # 60 + 15
|
||||
|
||||
def test_complete_job(self):
|
||||
# Als Benutzer einen Job als abgeschlossen markieren
|
||||
token = self.get_token('user_test', 'user')
|
||||
response = self.app.put('/api/jobs/1',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
data=json.dumps({
|
||||
'status': 'completed'
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['status'], 'completed')
|
||||
|
||||
# Überprüfen, ob der Drucker wieder verfügbar ist
|
||||
response = self.app.get('/api/printers/2')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['status'], 'available')
|
||||
|
||||
def test_get_remaining_time(self):
|
||||
# Test: Verbleibende Zeit für einen aktiven Job abrufen
|
||||
response = self.app.get('/api/job/1/remaining-time')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('remaining_minutes', data)
|
||||
# Der genaue Wert kann nicht überprüft werden, da er von der Zeit abhängt
|
||||
|
||||
def test_stats(self):
|
||||
# Als Admin Statistiken abrufen
|
||||
token = self.get_token('admin_test', 'admin')
|
||||
response = self.app.get('/api/stats',
|
||||
headers={'Authorization': f'Bearer {token}'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('printers', data)
|
||||
self.assertIn('jobs', data)
|
||||
self.assertIn('users', data)
|
||||
self.assertEqual(data['printers']['total'], 2)
|
||||
self.assertEqual(data['jobs']['total'], 1)
|
||||
self.assertEqual(data['users']['total'], 2)
|
||||
|
||||
def test_test_endpoint(self):
|
||||
# Test: API-Test-Endpunkt
|
||||
response = self.app.get('/api/test')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['message'], 'MYP Backend API funktioniert!')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
15
backend/docker-compose.yml
Normal file
15
backend/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build: .
|
||||
container_name: myp-backend
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- SECRET_KEY=change_me_in_production
|
||||
- DATABASE_URL=sqlite:///myp.db
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./instance:/app/instance
|
||||
restart: unless-stopped
|
647
backend/docs/API_DOCS.md
Normal file
647
backend/docs/API_DOCS.md
Normal file
@@ -0,0 +1,647 @@
|
||||
# MYP Backend API-Dokumentation
|
||||
|
||||
Dieses Dokument beschreibt detailliert die API-Endpunkte des MYP (Manage Your Printer) Backend-Systems.
|
||||
|
||||
## Basis-URL
|
||||
|
||||
Die Basis-URL für alle API-Anfragen ist: `http://localhost:5000` (Entwicklungsumgebung) oder die URL, unter der die Anwendung gehostet wird.
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
Die meisten Endpunkte erfordern eine Authentifizierung. Diese erfolgt über Cookies/Sessions, die bei der Anmeldung erstellt werden. Die Session wird für 7 Tage gespeichert.
|
||||
|
||||
### Benutzerregistrierung
|
||||
|
||||
**Endpunkt:** `POST /auth/register`
|
||||
|
||||
**Beschreibung:** Registriert einen neuen Benutzer im System.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"username": "string", // Erforderlich
|
||||
"password": "string", // Erforderlich
|
||||
"displayName": "string", // Optional (Standard: username)
|
||||
"email": "string" // Optional
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Registrierung erfolgreich!",
|
||||
"user": {
|
||||
"id": "string",
|
||||
"username": "string",
|
||||
"displayName": "string",
|
||||
"email": "string",
|
||||
"role": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Benutzername bereits vergeben!"
|
||||
}
|
||||
```
|
||||
|
||||
### Benutzeranmeldung
|
||||
|
||||
**Endpunkt:** `POST /auth/login`
|
||||
|
||||
**Beschreibung:** Meldet einen Benutzer an und erstellt eine Session.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"username": "string", // Erforderlich
|
||||
"password": "string" // Erforderlich
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Anmeldung erfolgreich!",
|
||||
"user": {
|
||||
"id": "string",
|
||||
"username": "string",
|
||||
"displayName": "string",
|
||||
"email": "string",
|
||||
"role": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Ungültiger Benutzername oder Passwort!"
|
||||
}
|
||||
```
|
||||
|
||||
### Initialer Administrator
|
||||
|
||||
**Endpunkt:** `POST /api/create-initial-admin`
|
||||
|
||||
**Beschreibung:** Erstellt einen initialen Admin-Benutzer, falls noch keiner existiert.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"username": "string", // Erforderlich
|
||||
"password": "string", // Erforderlich
|
||||
"displayName": "string", // Optional (Standard: username)
|
||||
"email": "string" // Optional
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Administrator wurde erfolgreich erstellt!",
|
||||
"user": {
|
||||
"id": "string",
|
||||
"username": "string",
|
||||
"displayName": "string",
|
||||
"email": "string",
|
||||
"role": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Es existiert bereits ein Administrator!"
|
||||
}
|
||||
```
|
||||
|
||||
## Benutzer-Endpunkte
|
||||
|
||||
### Alle Benutzer abrufen (Admin)
|
||||
|
||||
**Endpunkt:** `GET /api/users`
|
||||
|
||||
**Beschreibung:** Gibt eine Liste aller Benutzer zurück.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"role": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Benutzer abrufen (Admin)
|
||||
|
||||
**Endpunkt:** `GET /api/users/{userId}`
|
||||
|
||||
**Beschreibung:** Gibt die Details eines bestimmten Benutzers zurück.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"role": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Nicht gefunden!"
|
||||
}
|
||||
```
|
||||
|
||||
### Benutzer aktualisieren (Admin)
|
||||
|
||||
**Endpunkt:** `PUT /api/users/{userId}`
|
||||
|
||||
**Beschreibung:** Aktualisiert die Daten eines Benutzers.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"password": "string",
|
||||
"role": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"role": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Benutzername bereits vergeben!"
|
||||
}
|
||||
```
|
||||
|
||||
### Benutzer löschen (Admin)
|
||||
|
||||
**Endpunkt:** `DELETE /api/users/{userId}`
|
||||
|
||||
**Beschreibung:** Löscht einen Benutzer.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Benutzer gelöscht!"
|
||||
}
|
||||
```
|
||||
|
||||
## Drucker-Endpunkte
|
||||
|
||||
### Alle Drucker abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/printers`
|
||||
|
||||
**Beschreibung:** Gibt eine Liste aller Drucker (Steckdosen) zurück.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"status": 0, // 0 = available, 1 = busy
|
||||
"latestJob": {
|
||||
// Job-Objekt oder null, wenn kein aktiver Job
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Drucker hinzufügen (Admin)
|
||||
|
||||
**Endpunkt:** `POST /api/printers`
|
||||
|
||||
**Beschreibung:** Fügt einen neuen Drucker hinzu.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"ipAddress": "string" // IP-Adresse der Tapo-Steckdose
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"status": 0, // 0 = available, 1 = busy
|
||||
"latestJob": null
|
||||
}
|
||||
```
|
||||
|
||||
### Drucker abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/printers/{printerId}`
|
||||
|
||||
**Beschreibung:** Gibt die Details eines bestimmten Druckers zurück.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"status": 0, // 0 = available, 1 = busy
|
||||
"latestJob": {
|
||||
// Job-Objekt oder null, wenn kein aktiver Job
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Nicht gefunden!"
|
||||
}
|
||||
```
|
||||
|
||||
### Drucker aktualisieren (Admin)
|
||||
|
||||
**Endpunkt:** `PUT /api/printers/{printerId}`
|
||||
|
||||
**Beschreibung:** Aktualisiert die Daten eines Druckers.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"ipAddress": "string", // IP-Adresse der Tapo-Steckdose
|
||||
"status": 0 // 0 = available, 1 = busy
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"status": 0, // 0 = available, 1 = busy
|
||||
"latestJob": {
|
||||
// Job-Objekt oder null, wenn kein aktiver Job
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Drucker löschen (Admin)
|
||||
|
||||
**Endpunkt:** `DELETE /api/printers/{printerId}`
|
||||
|
||||
**Beschreibung:** Löscht einen Drucker.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Drucker gelöscht!"
|
||||
}
|
||||
```
|
||||
|
||||
## Druckauftrags-Endpunkte
|
||||
|
||||
### Alle Druckaufträge abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/jobs`
|
||||
|
||||
**Beschreibung:** Gibt eine Liste aller Druckaufträge zurück (für Admins) oder der eigenen Druckaufträge (für Benutzer).
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"socketId": "uuid-string",
|
||||
"userId": "uuid-string",
|
||||
"startAt": "string (ISO 8601)",
|
||||
"durationInMinutes": 60,
|
||||
"comments": "string",
|
||||
"aborted": false,
|
||||
"abortReason": null,
|
||||
"remainingMinutes": 30
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Druckauftrag erstellen
|
||||
|
||||
**Endpunkt:** `POST /api/jobs`
|
||||
|
||||
**Beschreibung:** Erstellt einen neuen Druckauftrag.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"printerId": "uuid-string",
|
||||
"durationInMinutes": 60,
|
||||
"comments": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"socketId": "uuid-string",
|
||||
"userId": "uuid-string",
|
||||
"startAt": "string (ISO 8601)",
|
||||
"durationInMinutes": 60,
|
||||
"comments": "string",
|
||||
"aborted": false,
|
||||
"abortReason": null,
|
||||
"remainingMinutes": 60
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Drucker ist nicht verfügbar!"
|
||||
}
|
||||
```
|
||||
|
||||
### Druckauftrag abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/jobs/{jobId}`
|
||||
|
||||
**Beschreibung:** Gibt die Details eines bestimmten Druckauftrags zurück.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"socketId": "uuid-string",
|
||||
"userId": "uuid-string",
|
||||
"startAt": "string (ISO 8601)",
|
||||
"durationInMinutes": 60,
|
||||
"comments": "string",
|
||||
"aborted": false,
|
||||
"abortReason": null,
|
||||
"remainingMinutes": 30
|
||||
}
|
||||
```
|
||||
|
||||
**Fehlerantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Nicht gefunden!"
|
||||
}
|
||||
```
|
||||
|
||||
### Druckauftrag Kommentare aktualisieren
|
||||
|
||||
**Endpunkt:** `PUT /api/jobs/{jobId}/comments`
|
||||
|
||||
**Beschreibung:** Aktualisiert die Kommentare eines Druckauftrags.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"comments": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"socketId": "uuid-string",
|
||||
"userId": "uuid-string",
|
||||
"startAt": "string (ISO 8601)",
|
||||
"durationInMinutes": 60,
|
||||
"comments": "string",
|
||||
"aborted": false,
|
||||
"abortReason": null,
|
||||
"remainingMinutes": 30
|
||||
}
|
||||
```
|
||||
|
||||
### Druckauftrag abbrechen
|
||||
|
||||
**Endpunkt:** `POST /api/jobs/{jobId}/abort`
|
||||
|
||||
**Beschreibung:** Bricht einen laufenden Druckauftrag ab.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"reason": "string" // Optional
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"socketId": "uuid-string",
|
||||
"userId": "uuid-string",
|
||||
"startAt": "string (ISO 8601)",
|
||||
"durationInMinutes": 60,
|
||||
"comments": "string",
|
||||
"aborted": true,
|
||||
"abortReason": "string",
|
||||
"remainingMinutes": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Druckauftrag vorzeitig beenden
|
||||
|
||||
**Endpunkt:** `POST /api/jobs/{jobId}/finish`
|
||||
|
||||
**Beschreibung:** Beendet einen laufenden Druckauftrag vorzeitig.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"socketId": "uuid-string",
|
||||
"userId": "uuid-string",
|
||||
"startAt": "string (ISO 8601)",
|
||||
"durationInMinutes": 45, // Tatsächliche Dauer bis zum Beenden
|
||||
"comments": "string",
|
||||
"aborted": false,
|
||||
"abortReason": null,
|
||||
"remainingMinutes": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Druckauftrag verlängern
|
||||
|
||||
**Endpunkt:** `POST /api/jobs/{jobId}/extend`
|
||||
|
||||
**Beschreibung:** Verlängert die Laufzeit eines Druckauftrags.
|
||||
|
||||
**Request-Body:**
|
||||
```json
|
||||
{
|
||||
"minutes": 30, // Zusätzliche Minuten
|
||||
"hours": 0 // Zusätzliche Stunden (optional)
|
||||
}
|
||||
```
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"socketId": "uuid-string",
|
||||
"userId": "uuid-string",
|
||||
"startAt": "string (ISO 8601)",
|
||||
"durationInMinutes": 90, // Aktualisierte Gesamtdauer
|
||||
"comments": "string",
|
||||
"aborted": false,
|
||||
"abortReason": null,
|
||||
"remainingMinutes": 60
|
||||
}
|
||||
```
|
||||
|
||||
### Druckauftrag löschen
|
||||
|
||||
**Endpunkt:** `DELETE /api/jobs/{jobId}`
|
||||
|
||||
**Beschreibung:** Löscht einen Druckauftrag.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "Druckauftrag gelöscht!"
|
||||
}
|
||||
```
|
||||
|
||||
### Verbleibende Zeit eines Druckauftrags abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/job/{jobId}/remaining-time`
|
||||
|
||||
**Beschreibung:** Gibt die verbleibende Zeit eines aktiven Druckauftrags in Minuten zurück.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"remaining_minutes": 30,
|
||||
"job_status": "active", // active, completed
|
||||
"socket_status": "busy" // busy, available
|
||||
}
|
||||
```
|
||||
|
||||
### Status eines Druckauftrags abrufen
|
||||
|
||||
**Endpunkt:** `GET /api/job/{jobId}/status`
|
||||
|
||||
**Beschreibung:** Gibt detaillierte Statusinformationen zu einem Druckauftrag zurück.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"job": {
|
||||
"id": "uuid-string",
|
||||
"socketId": "uuid-string",
|
||||
"userId": "uuid-string",
|
||||
"startAt": "string (ISO 8601)",
|
||||
"durationInMinutes": 60,
|
||||
"comments": "string",
|
||||
"aborted": false,
|
||||
"abortReason": null,
|
||||
"remainingMinutes": 30
|
||||
},
|
||||
"status": "active", // active, completed, aborted
|
||||
"socketStatus": "busy", // busy, available
|
||||
"remainingMinutes": 30
|
||||
}
|
||||
```
|
||||
|
||||
## Statistik-Endpunkte
|
||||
|
||||
### Systemstatistiken abrufen (Admin)
|
||||
|
||||
**Endpunkt:** `GET /api/stats`
|
||||
|
||||
**Beschreibung:** Gibt Statistiken zu Druckern, Aufträgen und Benutzern zurück.
|
||||
|
||||
**Erforderliche Rechte:** Admin
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"printers": {
|
||||
"total": 10,
|
||||
"available": 5,
|
||||
"utilization_rate": 0.5
|
||||
},
|
||||
"jobs": {
|
||||
"total": 100,
|
||||
"active": 5,
|
||||
"completed": 90,
|
||||
"avg_duration": 120
|
||||
},
|
||||
"users": {
|
||||
"total": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test-Endpunkt
|
||||
|
||||
### API-Test
|
||||
|
||||
**Endpunkt:** `GET /api/test`
|
||||
|
||||
**Beschreibung:** Testet, ob die API funktioniert.
|
||||
|
||||
**Erfolgsantwort:**
|
||||
```json
|
||||
{
|
||||
"message": "MYP Backend API funktioniert!"
|
||||
}
|
||||
```
|
||||
|
||||
## Fehlercodes
|
||||
|
||||
| Statuscode | Beschreibung |
|
||||
|------------|-----------------------------|
|
||||
| 200 | OK - Anfrage erfolgreich |
|
||||
| 201 | Created - Ressource erstellt |
|
||||
| 400 | Bad Request - Ungültige Anfrage |
|
||||
| 401 | Unauthorized - Authentifizierung erforderlich |
|
||||
| 403 | Forbidden - Unzureichende Rechte |
|
||||
| 404 | Not Found - Ressource nicht gefunden |
|
||||
| 500 | Internal Server Error - Serverfehler |
|
213
backend/docs/PROJEKTDOKUMENTATION.md
Normal file
213
backend/docs/PROJEKTDOKUMENTATION.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# MYP - Projektdokumentation für das IHK-Abschlussprojekt
|
||||
|
||||
## Projektübersicht
|
||||
|
||||
**Projektname:** MYP (Manage Your Printer)
|
||||
**Projekttyp:** IHK-Abschlussprojekt für Fachinformatiker für digitale Vernetzung
|
||||
**Zeitraum:** [Dein Projektzeitraum]
|
||||
**Team:** 2 Personen (Frontend- und Backend-Entwicklung)
|
||||
|
||||
## Projektziel
|
||||
|
||||
Das Ziel des Projektes ist die Entwicklung einer Reservierungs- und Steuerungsplattform für 3D-Drucker, die es Benutzern ermöglicht, Drucker zu reservieren und deren Stromversorgung automatisch über WLAN-Steckdosen (Tapo P115) zu steuern. Die Plattform soll eine einfache Verwaltung der Drucker und ihrer Auslastung bieten sowie den Stromverbrauch optimieren, indem Drucker nur während aktiver Reservierungen mit Strom versorgt werden.
|
||||
|
||||
## Aufgabenbeschreibung
|
||||
|
||||
Als Fachinformatiker für digitale Vernetzung besteht meine Aufgabe in der Entwicklung des Backend-Systems, das folgende Funktionen bereitstellt:
|
||||
|
||||
1. **API-Backend für das Frontend**: Entwicklung einer RESTful API, die mit dem Frontend kommuniziert und alle notwendigen Daten bereitstellt.
|
||||
|
||||
2. **Authentifizierungssystem**: Integration einer OAuth-Authentifizierung über GitHub, um Benutzer zu identifizieren und Zugriffskontrolle zu gewährleisten.
|
||||
|
||||
3. **Datenbankverwaltung**: Erstellung und Verwaltung der Datenbankmodelle für Benutzer, Drucker und Reservierungen.
|
||||
|
||||
4. **Steckdosensteuerung**: Implementierung einer Schnittstelle zu Tapo P115 WLAN-Steckdosen, um die Stromversorgung der Drucker basierend auf Reservierungen zu steuern.
|
||||
|
||||
5. **Automatisierung**: Entwicklung von Mechanismen zur automatischen Überwachung von Reservierungen und Steuerung der Steckdosen.
|
||||
|
||||
6. **Sicherheit**: Implementierung von Sicherheitsmaßnahmen zum Schutz der Anwendung und der Daten.
|
||||
|
||||
7. **Dokumentation**: Erstellung einer umfassenden Dokumentation für Entwicklung, Installation und Nutzung des Systems.
|
||||
|
||||
## Technische Umsetzung
|
||||
|
||||
### Backend (Mein Verantwortungsbereich)
|
||||
|
||||
#### Verwendete Technologien
|
||||
|
||||
- **Programmiersprache**: Python 3.11
|
||||
- **Web-Framework**: Flask 2.3.3
|
||||
- **Datenbank-ORM**: SQLAlchemy 3.1.1
|
||||
- **Datenbank**: SQLite (für Entwicklung), erweiterbar auf PostgreSQL für Produktion
|
||||
- **Authentifizierung**: Authlib für GitHub OAuth
|
||||
- **Steckdosen-Steuerung**: Tapo Python Library
|
||||
- **Container-Technologie**: Docker und Docker Compose
|
||||
|
||||
#### Architektur
|
||||
|
||||
Die Backend-Anwendung folgt einer klassischen dreischichtigen Architektur:
|
||||
|
||||
1. **Datenmodell-Schicht**: SQLAlchemy ORM-Modelle für Benutzer, Sessions, Drucker und Druckaufträge
|
||||
2. **Business-Logic-Schicht**: Implementierung der Geschäftslogik für Reservierungsverwaltung und Steckdosensteuerung
|
||||
3. **API-Schicht**: RESTful API-Endpunkte, die vom Frontend konsumiert werden
|
||||
|
||||
Zusätzlich wurden folgende Features implementiert:
|
||||
|
||||
- **OAuth-Authentifizierung**: Implementierung einer sicheren Authentifizierung über GitHub
|
||||
- **Session-Management**: Server-seitige Session-Verwaltung für Benutzerauthentifizierung
|
||||
- **Steckdosensteuerung**: Asynchrone Steuerung der Tapo P115 WLAN-Steckdosen
|
||||
- **CLI-Befehle**: Flask CLI-Befehle für automatisierte Aufgaben wie die Überprüfung abgelaufener Reservierungen
|
||||
|
||||
#### Datenmodell
|
||||
|
||||
Das Datenmodell besteht aus vier Hauptentitäten:
|
||||
|
||||
1. **User**: Benutzer mit GitHub-Authentifizierung und Rollenverwaltung
|
||||
2. **Session**: Sitzungsdaten für die Authentifizierung
|
||||
3. **Printer**: Drucker mit Status und IP-Adresse der zugehörigen Steckdose
|
||||
4. **PrintJob**: Reservierungen mit Start- und Endzeit, Dauer und Status
|
||||
|
||||
#### API-Endpunkte
|
||||
|
||||
Die API wurde speziell entwickelt, um nahtlos mit dem bestehenden Frontend zusammenzuarbeiten. Sie bietet Endpunkte für:
|
||||
|
||||
- Authentifizierung und Benutzerverwaltung
|
||||
- Druckerverwaltung
|
||||
- Reservierungsverwaltung (Erstellen, Abbrechen, Verlängern)
|
||||
- Statusinformationen wie verbleibende Zeit
|
||||
|
||||
#### Steckdosensteuerung
|
||||
|
||||
Die Steuerung der Tapo P115 WLAN-Steckdosen erfolgt über die Tapo Python Library. Das System:
|
||||
|
||||
- Schaltet Steckdosen bei Erstellung einer Reservierung ein
|
||||
- Schaltet Steckdosen bei Abbruch oder Beendigung einer Reservierung aus
|
||||
- Überprüft regelmäßig abgelaufene Reservierungen und schaltet die entsprechenden Steckdosen aus
|
||||
|
||||
#### Automatisierung
|
||||
|
||||
Das System implementiert mehrere Automatisierungsmechanismen:
|
||||
|
||||
- **Automatische Steckdosensteuerung**: Ein- und Ausschalten der Steckdosen basierend auf Reservierungsstatus
|
||||
- **Job-Überprüfung**: CLI-Befehl `flask check-jobs` zur regelmäßigen Überprüfung abgelaufener Reservierungen
|
||||
- **Logging**: Automatische Protokollierung aller Aktionen zur Fehlerdiagnose
|
||||
|
||||
### Frontend (Verantwortungsbereich des Teampartners)
|
||||
|
||||
Das Frontend wurde von meinem Teampartner entwickelt und besteht aus:
|
||||
|
||||
- Next.js-Anwendung mit React-Komponenten
|
||||
- Tailwind CSS für das Styling
|
||||
- Serverless Functions für API-Integrationen
|
||||
- Responsive Design für Desktop- und Mobile-Nutzung
|
||||
|
||||
## Projektergebnisse
|
||||
|
||||
Das Projekt hat erfolgreich eine funktionsfähige Reservierungs- und Steuerungsplattform für 3D-Drucker geschaffen, die es Benutzern ermöglicht:
|
||||
|
||||
1. Sich über GitHub zu authentifizieren
|
||||
2. Verfügbare Drucker zu sehen und zu reservieren
|
||||
3. Ihre Reservierungen zu verwalten (verlängern, abbrechen, kommentieren)
|
||||
4. Als Administrator Drucker und Benutzer zu verwalten
|
||||
|
||||
Technische Errungenschaften:
|
||||
|
||||
1. Nahtlose Integration mit dem Frontend
|
||||
2. Erfolgreiche Implementierung der Steckdosensteuerung
|
||||
3. Sichere Authentifizierung über GitHub OAuth
|
||||
4. Optimierte Stromnutzung durch automatische Steckdosensteuerung
|
||||
|
||||
## Herausforderungen und Lösungen
|
||||
|
||||
### Herausforderung 1: GitHub OAuth-Integration
|
||||
|
||||
Die Integration der GitHub-Authentifizierung, insbesondere mit GitHub Enterprise, erforderte eine sorgfältige Konfiguration der OAuth-Einstellungen und URL-Anpassungen.
|
||||
|
||||
**Lösung:** Implementierung mit Authlib und anpassbaren Konfigurationsoptionen für verschiedene GitHub-Instanzen.
|
||||
|
||||
### Herausforderung 2: Tapo P115 Steuerung
|
||||
|
||||
Die Kommunikation mit den Tapo P115 WLAN-Steckdosen erforderte eine zuverlässige und asynchrone Implementierung.
|
||||
|
||||
**Lösung:** Verwendung der Tapo Python Library mit asynchronem Handling und robusten Fehlerbehandlungsmechanismen.
|
||||
|
||||
### Herausforderung 3: Kompatibilität mit bestehendem Frontend
|
||||
|
||||
Das Backend musste mit dem bereits entwickelten Frontend kompatibel sein, was eine genaue Anpassung der API-Endpunkte und Datenstrukturen erforderte.
|
||||
|
||||
**Lösung:** Sorgfältige Analyse des Frontend-Codes, um die erwarteten API-Strukturen zu verstehen und das Backend entsprechend zu implementieren.
|
||||
|
||||
### Herausforderung 4: Automatische Steckdosensteuerung
|
||||
|
||||
Die zuverlässige Steuerung der Steckdosen bei abgelaufenen Reservierungen war eine Herausforderung.
|
||||
|
||||
**Lösung:** Implementierung eines CLI-Befehls, der regelmäßig durch Cron-Jobs ausgeführt werden kann, um abgelaufene Reservierungen zu überprüfen.
|
||||
|
||||
## Fachliche Reflexion
|
||||
|
||||
Das Projekt erforderte ein breites Spektrum an Fähigkeiten aus dem Bereich der digitalen Vernetzung:
|
||||
|
||||
1. **Netzwerkkommunikation**: Implementierung der Kommunikation zwischen Backend, Frontend und WLAN-Steckdosen über verschiedene Protokolle.
|
||||
|
||||
2. **Systemintegration**: Integration verschiedener Systeme (GitHub OAuth, Datenbank, Tapo-Steckdosen) zu einer kohärenten Anwendung.
|
||||
|
||||
3. **API-Design**: Entwicklung einer RESTful API, die den Anforderungen des Frontends entspricht und zukunftssicher ist.
|
||||
|
||||
4. **Datenbankentwurf**: Erstellung eines optimierten Datenbankschemas für die Anwendung.
|
||||
|
||||
5. **Sicherheitskonzepte**: Implementierung von Sicherheitsmaßnahmen wie OAuth, Session-Management und Zugriffskontrollen.
|
||||
|
||||
6. **Automatisierung**: Entwicklung von Automatisierungsprozessen für die Steckdosensteuerung und Job-Überwachung.
|
||||
|
||||
Diese Aspekte entsprechen direkt den Kernkompetenzen des Berufsbildes "Fachinformatiker für digitale Vernetzung" und zeigen die praktische Anwendung dieser Fähigkeiten in einem realen Projekt.
|
||||
|
||||
## Ausblick und Weiterentwicklung
|
||||
|
||||
Das System bietet verschiedene Möglichkeiten zur Weiterentwicklung:
|
||||
|
||||
1. **Erweiterung der Steckdosenunterstützung**: Integration weiterer Smart-Home-Geräte neben Tapo P115.
|
||||
|
||||
2. **Benachrichtigungssystem**: Implementierung von E-Mail- oder Push-Benachrichtigungen für Reservierungserinnerungen.
|
||||
|
||||
3. **Erweiterte Statistiken**: Detailliertere Nutzungsstatistiken und Visualisierungen für Administratoren.
|
||||
|
||||
4. **Mobile App**: Entwicklung einer nativen mobilen App für iOS und Android.
|
||||
|
||||
5. **Verbesserte Automatisierung**: Integration mit weiteren Systemen wie 3D-Drucker-APIs für direktes Monitoring des Druckstatus.
|
||||
|
||||
## Fazit
|
||||
|
||||
Das MYP-Projekt zeigt erfolgreich, wie moderne Webtechnologien und IoT-Geräte kombiniert werden können, um eine praktische Lösung für die Verwaltung von 3D-Druckern zu schaffen.
|
||||
|
||||
Als angehender Fachinformatiker für digitale Vernetzung konnte ich meine Fähigkeiten in den Bereichen Programmierung, Systemintegration, Netzwerkkommunikation und Automatisierung anwenden und erweitern.
|
||||
|
||||
Die Zusammenarbeit im Team mit klarer Aufgabenteilung (Frontend/Backend) hat zu einem erfolgreichen Projektergebnis geführt, das die gestellten Anforderungen erfüllt und einen praktischen Nutzen bietet.
|
||||
|
||||
---
|
||||
|
||||
## Anhang
|
||||
|
||||
### Installation und Einrichtung
|
||||
|
||||
Detaillierte Anweisungen zur Installation und Einrichtung des Backend-Systems finden sich in der README.md-Datei.
|
||||
|
||||
### Wichtige Konfigurationsparameter
|
||||
|
||||
Die folgenden Umgebungsvariablen müssen konfiguriert werden:
|
||||
|
||||
- `SECRET_KEY`: Geheimer Schlüssel für die Session-Verschlüsselung
|
||||
- `DATABASE_URL`: URL zur Datenbank
|
||||
- `OAUTH_CLIENT_ID`: GitHub OAuth Client ID
|
||||
- `OAUTH_CLIENT_SECRET`: GitHub OAuth Client Secret
|
||||
- `GITHUB_API_BASE_URL`, `GITHUB_AUTHORIZE_URL`, `GITHUB_TOKEN_URL`: URLs für GitHub OAuth
|
||||
- `TAPO_USERNAME`: Benutzername für die Tapo-Steckdosen
|
||||
- `TAPO_PASSWORD`: Passwort für die Tapo-Steckdosen
|
||||
- `TAPO_DEVICES`: JSON-Objekt mit der Zuordnung von Drucker-IDs zu IP-Adressen
|
||||
|
||||
### Cron-Job-Einrichtung
|
||||
|
||||
Für die automatische Überprüfung abgelaufener Jobs kann folgender Cron-Job eingerichtet werden:
|
||||
|
||||
```
|
||||
*/5 * * * * cd /pfad/zum/projekt && /pfad/zur/venv/bin/flask check-jobs >> /pfad/zum/projekt/logs/cron.log 2>&1
|
||||
```
|
185
backend/docs/README.md
Normal file
185
backend/docs/README.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# MYP Backend-Steuerungsplattform
|
||||
|
||||
Dies ist das Backend für das MYP (Manage Your Printer) Projekt, ein IHK-Abschlussprojekt für Fachinformatiker für digitale Vernetzung. Die Plattform ist mit Python und Flask implementiert und stellt eine RESTful API zur Verfügung, die es ermöglicht, 3D-Drucker zu verwalten, zu reservieren und über WLAN-Steckdosen (Tapo P115) zu steuern.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Lokales Authentifizierungssystem (Offline-fähig)
|
||||
- Rollen-basierte Zugriffskontrolle (Admin/User/Guest)
|
||||
- Druckerverwaltung (Hinzufügen, Bearbeiten, Löschen)
|
||||
- Reservierungsverwaltung (Erstellen, Abbrechen, Verlängern)
|
||||
- Fernsteuerung von WLAN-Steckdosen (Tapo P115)
|
||||
- Statistikerfassung und -anzeige
|
||||
- RESTful API für die Kommunikation mit dem Frontend
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
- **Python**: Programmiersprache
|
||||
- **Flask**: Web-Framework
|
||||
- **SQLite**: Integrierte Datenbank (kann für Produktion durch PostgreSQL ersetzt werden)
|
||||
- **PyP100**: Python-Bibliothek zur Steuerung der Tapo P115 WLAN-Steckdosen
|
||||
- **Gunicorn**: WSGI HTTP Server für die Produktionsumgebung
|
||||
- **Docker**: Containerisierung der Anwendung
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
- `app.py`: Hauptanwendungsdatei mit allen Routen und Modellen
|
||||
- `requirements.txt`: Liste aller Python-Abhängigkeiten
|
||||
- `Dockerfile`: Docker-Konfiguration
|
||||
- `docker-compose.yml`: Docker Compose Konfiguration für einfaches Deployment
|
||||
- `.env.example`: Beispiel für die Umgebungsvariablen
|
||||
- `logs/`: Logdateien (automatisch erstellt)
|
||||
- `instance/`: SQLite-Datenbank (automatisch erstellt)
|
||||
|
||||
## Installation und Ausführung
|
||||
|
||||
### Lokal (Entwicklung)
|
||||
|
||||
1. Python 3.8 oder höher installieren
|
||||
2. Repository klonen
|
||||
3. Ins Projektverzeichnis wechseln
|
||||
4. Virtuelle Umgebung erstellen (optional, aber empfohlen)
|
||||
```
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Unter Windows: venv\Scripts\activate
|
||||
```
|
||||
5. Abhängigkeiten installieren
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
6. `.env.example` nach `.env` kopieren und anpassen
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
7. Anwendung starten
|
||||
```
|
||||
python app.py
|
||||
```
|
||||
|
||||
Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
||||
|
||||
### Mit Docker
|
||||
|
||||
1. Docker und Docker Compose installieren
|
||||
2. Ins Projektverzeichnis wechseln
|
||||
3. `.env.example` nach `.env` kopieren und anpassen
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
4. Anwendung starten
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
### Authentifizierung
|
||||
|
||||
- `POST /auth/register`: Neuen Benutzer registrieren
|
||||
- `POST /auth/login`: Benutzer anmelden
|
||||
- `POST /auth/logout`: Abmelden und Session beenden
|
||||
- `POST /api/create-initial-admin`: Initialen Administrator erstellen
|
||||
- `GET /api/me`: Aktuelle Benutzerinformationen abrufen
|
||||
|
||||
### Benutzer
|
||||
|
||||
- `GET /api/users`: Liste aller Benutzer (Admin)
|
||||
- `GET /api/users/<id>`: Details zu einem Benutzer (Admin)
|
||||
- `PUT /api/users/<id>`: Benutzer aktualisieren (Admin)
|
||||
- `DELETE /api/users/<id>`: Benutzer löschen (Admin)
|
||||
|
||||
### Drucker
|
||||
|
||||
- `GET /api/printers`: Liste aller Drucker
|
||||
- `POST /api/printers`: Drucker hinzufügen (Admin)
|
||||
- `GET /api/printers/<id>`: Details zu einem Drucker
|
||||
- `PUT /api/printers/<id>`: Drucker aktualisieren (Admin)
|
||||
- `DELETE /api/printers/<id>`: Drucker löschen (Admin)
|
||||
|
||||
### Druckaufträge
|
||||
|
||||
- `GET /api/jobs`: Liste aller Druckaufträge (Admin) oder eigener Druckaufträge (Benutzer)
|
||||
- `POST /api/jobs`: Druckauftrag erstellen
|
||||
- `GET /api/jobs/<id>`: Details zu einem Druckauftrag
|
||||
- `POST /api/jobs/<id>/abort`: Druckauftrag abbrechen
|
||||
- `POST /api/jobs/<id>/finish`: Druckauftrag vorzeitig beenden
|
||||
- `POST /api/jobs/<id>/extend`: Druckauftrag verlängern
|
||||
- `PUT /api/jobs/<id>/comments`: Kommentare aktualisieren
|
||||
- `GET /api/job/<id>/remaining-time`: Verbleibende Zeit für einen aktiven Druckauftrag
|
||||
|
||||
### Statistiken
|
||||
|
||||
- `GET /api/stats`: Statistiken zu Druckern, Aufträgen und Benutzern (Admin)
|
||||
|
||||
## Datenmodell
|
||||
|
||||
### Benutzer (User)
|
||||
- id (String UUID, Primary Key)
|
||||
- username (String, Unique)
|
||||
- password_hash (String)
|
||||
- display_name (String)
|
||||
- email (String, Unique)
|
||||
- role (String, 'admin', 'user' oder 'guest')
|
||||
|
||||
### Session
|
||||
- id (String UUID, Primary Key)
|
||||
- user_id (String UUID, Foreign Key zu User)
|
||||
- expires_at (DateTime)
|
||||
|
||||
### Drucker (Printer)
|
||||
- id (String UUID, Primary Key)
|
||||
- name (String)
|
||||
- description (Text)
|
||||
- status (Integer, 0=available, 1=busy, 2=maintenance)
|
||||
- ip_address (String, IP-Adresse der Tapo-Steckdose)
|
||||
|
||||
### Druckauftrag (PrintJob)
|
||||
- id (String UUID, Primary Key)
|
||||
- printer_id (String UUID, Foreign Key zu Printer)
|
||||
- user_id (String UUID, Foreign Key zu User)
|
||||
- start_at (DateTime)
|
||||
- duration_in_minutes (Integer)
|
||||
- comments (Text)
|
||||
- aborted (Boolean)
|
||||
- abort_reason (Text)
|
||||
|
||||
## Steckdosensteuerung
|
||||
|
||||
Die Anwendung steuert Tapo P115 WLAN-Steckdosen, um die Drucker basierend auf Reservierungen ein- und auszuschalten:
|
||||
|
||||
- Bei Erstellung eines Druckauftrags wird die Steckdose des zugehörigen Druckers automatisch eingeschaltet
|
||||
- Bei Abbruch oder vorzeitiger Beendigung eines Druckauftrags wird die Steckdose ausgeschaltet
|
||||
- Nach Ablauf der Reservierungszeit wird die Steckdose automatisch ausgeschaltet
|
||||
- Ein CLI-Befehl `flask check-jobs` überprüft regelmäßig abgelaufene Jobs und schaltet Steckdosen aus
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- Die Anwendung verwendet ein lokales Authentifizierungssystem mit Passwort-Hashing
|
||||
- Sitzungsdaten werden in Server-Side-Sessions gespeichert
|
||||
- Zugriffskontrollen sind implementiert, um sicherzustellen, dass Benutzer nur auf ihre eigenen Daten zugreifen können
|
||||
- Admin-Benutzer haben Zugriff auf alle Daten und können Systemkonfigurationen ändern
|
||||
- Der erste registrierte Benutzer wird automatisch zum Administrator
|
||||
|
||||
## Logging
|
||||
|
||||
Die Anwendung protokolliert Aktivitäten in rotierenden Logdateien in einem `logs` Verzeichnis. Dies hilft bei der Fehlersuche und Überwachung der Anwendung im Betrieb.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
Die folgenden Umgebungsvariablen müssen konfiguriert werden:
|
||||
|
||||
- `SECRET_KEY`: Geheimer Schlüssel für die Session-Verschlüsselung
|
||||
- `DATABASE_PATH`: Pfad zur Datenbank (Standard: SQLite-Datenbank im Instance-Verzeichnis)
|
||||
- `TAPO_USERNAME`: Benutzername für die Tapo-Steckdosen
|
||||
- `TAPO_PASSWORD`: Passwort für die Tapo-Steckdosen
|
||||
- `PRINTERS`: JSON-Objekt mit der Zuordnung von Drucker-Namen zu IP-Adressen der Steckdosen im Format: `{"Printer 1": {"ip": "192.168.1.100"}, "Printer 2": {"ip": "192.168.1.101"}, ...}`
|
||||
|
||||
## Automatisierung
|
||||
|
||||
Die Anwendung beinhaltet einen CLI-Befehl `flask check-jobs`, der regelmäßig ausgeführt werden sollte (z.B. als Cron-Job), um abgelaufene Druckaufträge zu überprüfen und die zugehörigen Steckdosen auszuschalten.
|
||||
|
||||
## Kompatibilität mit dem Frontend
|
||||
|
||||
Das Backend wurde speziell für die Kompatibilität mit dem bestehenden Frontend entwickelt, welches in `/packages/reservation-platform` zu finden ist. Die API-Endpunkte und Datenstrukturen sind so gestaltet, dass sie nahtlos mit dem Frontend zusammenarbeiten.
|
1
backend/migrations/README
Normal file
1
backend/migrations/README
Normal file
@@ -0,0 +1 @@
|
||||
Single-database configuration for Flask.
|
50
backend/migrations/alembic.ini
Normal file
50
backend/migrations/alembic.ini
Normal file
@@ -0,0 +1,50 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic,flask_migrate
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[logger_flask_migrate]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = flask_migrate
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
113
backend/migrations/env.py
Normal file
113
backend/migrations/env.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
|
||||
def get_engine():
|
||||
try:
|
||||
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||
return current_app.extensions['migrate'].db.get_engine()
|
||||
except (TypeError, AttributeError):
|
||||
# this works with Flask-SQLAlchemy>=3
|
||||
return current_app.extensions['migrate'].db.engine
|
||||
|
||||
|
||||
def get_engine_url():
|
||||
try:
|
||||
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||
'%', '%%')
|
||||
except AttributeError:
|
||||
return str(get_engine().url).replace('%', '%%')
|
||||
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||
target_db = current_app.extensions['migrate'].db
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def get_metadata():
|
||||
if hasattr(target_db, 'metadatas'):
|
||||
return target_db.metadatas[None]
|
||||
return target_db.metadata
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
conf_args = current_app.extensions['migrate'].configure_args
|
||||
if conf_args.get("process_revision_directives") is None:
|
||||
conf_args["process_revision_directives"] = process_revision_directives
|
||||
|
||||
connectable = get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=get_metadata(),
|
||||
**conf_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
backend/migrations/script.py.mako
Normal file
24
backend/migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
42
backend/migrations/versions/add_waiting_approval.py
Normal file
42
backend/migrations/versions/add_waiting_approval.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Add waiting_approval column to job table
|
||||
|
||||
Revision ID: add_waiting_approval
|
||||
Revises: af3faaa3844c
|
||||
Create Date: 2025-03-12 14:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'add_waiting_approval'
|
||||
down_revision = 'af3faaa3844c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Füge die neue Spalte waiting_approval zur job-Tabelle hinzu
|
||||
with op.batch_alter_table('job', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('waiting_approval', sa.Integer(), server_default='0', nullable=False))
|
||||
|
||||
# SQLite-kompatible Migration für die print_job-Tabelle, falls diese existiert
|
||||
try:
|
||||
with op.batch_alter_table('print_job', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('waiting_approval', sa.Boolean(), server_default='0', nullable=False))
|
||||
except Exception as e:
|
||||
print(f"Migration für print_job-Tabelle übersprungen: {e}")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Entferne die waiting_approval-Spalte aus der job-Tabelle
|
||||
with op.batch_alter_table('job', schema=None) as batch_op:
|
||||
batch_op.drop_column('waiting_approval')
|
||||
|
||||
# SQLite-kompatible Migration für die print_job-Tabelle, falls diese existiert
|
||||
try:
|
||||
with op.batch_alter_table('print_job', schema=None) as batch_op:
|
||||
batch_op.drop_column('waiting_approval')
|
||||
except Exception as e:
|
||||
print(f"Downgrade für print_job-Tabelle übersprungen: {e}")
|
81
backend/migrations/versions/af3faaa3844c_.py
Normal file
81
backend/migrations/versions/af3faaa3844c_.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: af3faaa3844c
|
||||
Revises:
|
||||
Create Date: 2025-03-11 11:16:04.961964
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'af3faaa3844c'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('printer',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=False),
|
||||
sa.Column('status', sa.Integer(), nullable=True),
|
||||
sa.Column('ip_address', sa.String(length=15), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('printer', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_printer_name'), ['name'], unique=False)
|
||||
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('username', sa.String(length=64), nullable=True),
|
||||
sa.Column('password_hash', sa.String(length=128), nullable=True),
|
||||
sa.Column('display_name', sa.String(length=100), nullable=True),
|
||||
sa.Column('email', sa.String(length=120), nullable=True),
|
||||
sa.Column('role', sa.String(length=20), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
|
||||
|
||||
op.create_table('print_job',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('printer_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('user_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('start_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('duration_in_minutes', sa.Integer(), nullable=False),
|
||||
sa.Column('comments', sa.Text(), nullable=True),
|
||||
sa.Column('aborted', sa.Boolean(), nullable=True),
|
||||
sa.Column('abort_reason', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['printer_id'], ['printer.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('session',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('user_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('expires_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('session')
|
||||
op.drop_table('print_job')
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_user_username'))
|
||||
batch_op.drop_index(batch_op.f('ix_user_email'))
|
||||
|
||||
op.drop_table('user')
|
||||
with op.batch_alter_table('printer', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_printer_name'))
|
||||
|
||||
op.drop_table('printer')
|
||||
# ### end Alembic commands ###
|
7
backend/requirements.txt
Normal file
7
backend/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
flask==2.3.3
|
||||
flask-cors==4.0.0
|
||||
pyjwt==2.8.0
|
||||
python-dotenv==1.0.0
|
||||
werkzeug==2.3.7
|
||||
gunicorn==21.2.0
|
||||
PyP100==0.0.19
|
BIN
backend/static/fontawesome/webfonts/fa-brands-400.ttf
Normal file
BIN
backend/static/fontawesome/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
backend/static/fontawesome/webfonts/fa-brands-400.woff2
Normal file
BIN
backend/static/fontawesome/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
backend/static/fontawesome/webfonts/fa-regular-400.woff2
Normal file
BIN
backend/static/fontawesome/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
backend/static/fontawesome/webfonts/fa-solid-900.ttf
Normal file
BIN
backend/static/fontawesome/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
backend/static/fontawesome/webfonts/fa-solid-900.woff2
Normal file
BIN
backend/static/fontawesome/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
169
backend/templates/base.html
Normal file
169
backend/templates/base.html
Normal file
@@ -0,0 +1,169 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}MYP API Tester{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
<style>
|
||||
.sidebar {
|
||||
min-height: calc(100vh - 56px);
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.api-response {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.nav-link.active {
|
||||
background-color: #0d6efd;
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">MYP API Tester</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'home' %}active{% endif %}" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'printers' %}active{% endif %}" href="/admin/printers">Drucker</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'jobs' %}active{% endif %}" href="/admin/jobs">Druckaufträge</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'users' %}active{% endif %}" href="/admin/users">Benutzer</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'stats' %}active{% endif %}" href="/admin/stats">Statistiken</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto">
|
||||
{% if current_user %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
|
||||
{{ current_user.username }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="/logout">Abmelden</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/login">Anmelden</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid py-3">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
function formatJson(jsonString) {
|
||||
try {
|
||||
const obj = JSON.parse(jsonString);
|
||||
return JSON.stringify(obj, null, 2);
|
||||
} catch (e) {
|
||||
return jsonString;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Format all response areas
|
||||
document.querySelectorAll('.api-response').forEach(function(el) {
|
||||
if (el.textContent) {
|
||||
el.textContent = formatJson(el.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener to show response areas
|
||||
document.querySelectorAll('.api-form').forEach(function(form) {
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const url = this.getAttribute('data-url');
|
||||
const method = this.getAttribute('data-method') || 'GET';
|
||||
const responseArea = document.getElementById(this.getAttribute('data-response'));
|
||||
const formData = new FormData(this);
|
||||
const data = {};
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
if (value) {
|
||||
try {
|
||||
// Try to parse as JSON if it looks like JSON
|
||||
if (value.trim().startsWith('{') || value.trim().startsWith('[')) {
|
||||
data[key] = JSON.parse(value);
|
||||
} else {
|
||||
data[key] = value;
|
||||
}
|
||||
} catch (e) {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
try {
|
||||
responseArea.textContent = 'Sending request...';
|
||||
const response = await fetch(url, options);
|
||||
const responseText = await response.text();
|
||||
|
||||
try {
|
||||
const formatted = formatJson(responseText);
|
||||
responseArea.textContent = formatted;
|
||||
} catch (e) {
|
||||
responseArea.textContent = responseText;
|
||||
}
|
||||
|
||||
if (this.hasAttribute('data-reload') && response.ok) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
responseArea.textContent = 'Error: ' + err.message;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
304
backend/templates/dashboard.html
Normal file
304
backend/templates/dashboard.html
Normal file
@@ -0,0 +1,304 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - MYP API Tester{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Willkommen, {{ current_user.display_name }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Benutzerdetails:</p>
|
||||
<ul>
|
||||
<li><strong>ID:</strong> {{ current_user.id }}</li>
|
||||
<li><strong>Benutzername:</strong> {{ current_user.username }}</li>
|
||||
<li><strong>E-Mail:</strong> {{ current_user.email or "Nicht angegeben" }}</li>
|
||||
<li><strong>Rolle:</strong> {{ current_user.role }}</li>
|
||||
</ul>
|
||||
<div class="mt-3">
|
||||
<a href="/admin/printers" class="btn btn-primary me-2">Drucker verwalten</a>
|
||||
<a href="/admin/jobs" class="btn btn-success me-2">Druckaufträge verwalten</a>
|
||||
{% if current_user.role == 'admin' %}
|
||||
<a href="/admin/users" class="btn btn-info me-2">Benutzer verwalten</a>
|
||||
<a href="/admin/stats" class="btn btn-secondary">Statistiken</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Aktive Druckaufträge</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="api-form mb-3" data-url="/api/jobs" data-method="GET" data-response="jobsResponse">
|
||||
<button type="submit" class="btn btn-primary">Aktualisieren</button>
|
||||
</form>
|
||||
|
||||
<div id="activeJobsContainer">
|
||||
<div class="alert alert-info">Lade Druckaufträge...</div>
|
||||
</div>
|
||||
|
||||
<div class="d-none">
|
||||
<h6>API-Antwort:</h6>
|
||||
<pre class="api-response" id="jobsResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Verfügbare Drucker</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="api-form mb-3" data-url="/api/printers" data-method="GET" data-response="printersResponse">
|
||||
<button type="submit" class="btn btn-primary">Aktualisieren</button>
|
||||
</form>
|
||||
|
||||
<div id="availablePrintersContainer">
|
||||
<div class="alert alert-info">Lade Drucker...</div>
|
||||
</div>
|
||||
|
||||
<div class="d-none">
|
||||
<h6>API-Antwort:</h6>
|
||||
<pre class="api-response" id="printersResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job freischalten Modal -->
|
||||
<div class="modal fade" id="approveJobModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Druckauftrag freischalten</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Möchten Sie diesen Druckauftrag jetzt freischalten und starten?</p>
|
||||
<p><strong>Hinweis:</strong> Der Drucker muss verfügbar sein, damit der Auftrag gestartet werden kann.</p>
|
||||
<form id="approveJobForm" class="api-form" data-method="POST" data-response="approveJobResponse" data-reload="true">
|
||||
<input type="hidden" id="approveJobId" name="jobId">
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="approveJobResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" form="approveJobForm" class="btn btn-success">Freischalten</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Aufträge und Drucker laden
|
||||
document.querySelector('form[data-url="/api/jobs"]').dispatchEvent(new Event('submit'));
|
||||
document.querySelector('form[data-url="/api/printers"]').dispatchEvent(new Event('submit'));
|
||||
|
||||
// Tabellen aktualisieren, wenn Daten geladen werden
|
||||
const jobsResponse = document.getElementById('jobsResponse');
|
||||
const printersResponse = document.getElementById('printersResponse');
|
||||
|
||||
// Observer für Jobs
|
||||
const jobsObserver = new MutationObserver(function(mutations) {
|
||||
try {
|
||||
const jobs = JSON.parse(jobsResponse.textContent);
|
||||
updateActiveJobs(jobs);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Parsen der Auftrags-Daten:', e);
|
||||
}
|
||||
});
|
||||
|
||||
jobsObserver.observe(jobsResponse, { childList: true, characterData: true, subtree: true });
|
||||
|
||||
// Observer für Drucker
|
||||
const printersObserver = new MutationObserver(function(mutations) {
|
||||
try {
|
||||
const printers = JSON.parse(printersResponse.textContent);
|
||||
updateAvailablePrinters(printers);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Parsen der Drucker-Daten:', e);
|
||||
}
|
||||
});
|
||||
|
||||
printersObserver.observe(printersResponse, { childList: true, characterData: true, subtree: true });
|
||||
|
||||
// Approve-Modal vorbereiten
|
||||
document.getElementById('approveJobModal').addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const jobId = button.getAttribute('data-job-id');
|
||||
|
||||
document.getElementById('approveJobId').value = jobId;
|
||||
document.getElementById('approveJobForm').setAttribute('data-url', `/api/jobs/${jobId}/approve`);
|
||||
});
|
||||
|
||||
// Automatische Aktualisierung alle 60 Sekunden
|
||||
setInterval(() => {
|
||||
document.querySelector('form[data-url="/api/jobs"]').dispatchEvent(new Event('submit'));
|
||||
document.querySelector('form[data-url="/api/printers"]').dispatchEvent(new Event('submit'));
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
function updateActiveJobs(jobs) {
|
||||
const container = document.getElementById('activeJobsContainer');
|
||||
|
||||
// Filter für aktive und wartende Jobs
|
||||
const activeJobs = jobs.filter(job => !job.aborted && job.remainingMinutes > 0 && !job.waitingApproval);
|
||||
const waitingJobs = jobs.filter(job => !job.aborted && job.waitingApproval);
|
||||
|
||||
if (activeJobs.length === 0 && waitingJobs.length === 0) {
|
||||
container.innerHTML = '<div class="alert alert-info">Keine aktiven Druckaufträge vorhanden.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
|
||||
// Aktive Jobs anzeigen
|
||||
if (activeJobs.length > 0) {
|
||||
html += '<h6 class="mt-3">Laufende Aufträge</h6>';
|
||||
html += '<div class="list-group mb-3">';
|
||||
|
||||
activeJobs.forEach(job => {
|
||||
// Prozentsatz der abgelaufenen Zeit berechnen
|
||||
const totalDuration = job.durationInMinutes;
|
||||
const elapsed = totalDuration - job.remainingMinutes;
|
||||
const percentage = Math.round((elapsed / totalDuration) * 100);
|
||||
|
||||
html += `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<strong>Job ${job.id.substring(0, 8)}...</strong> (${job.durationInMinutes} Min)
|
||||
<div class="small text-muted">Verbleibend: ${job.remainingMinutes} Min</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge bg-warning">Aktiv</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height: 10px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar"
|
||||
style="width: ${percentage}%;"
|
||||
aria-valuenow="${percentage}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
${percentage}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Wartende Jobs anzeigen
|
||||
if (waitingJobs.length > 0) {
|
||||
html += '<h6 class="mt-3">Wartende Aufträge</h6>';
|
||||
html += '<div class="list-group">';
|
||||
|
||||
waitingJobs.forEach(job => {
|
||||
html += `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<strong>Job ${job.id.substring(0, 8)}...</strong> (${job.durationInMinutes} Min)
|
||||
<div class="small text-muted">Drucker: ${job.socketId.substring(0, 8)}...</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge bg-info">Wartet</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-sm btn-success"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#approveJobModal"
|
||||
data-job-id="${job.id}">
|
||||
Freischalten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function updateAvailablePrinters(printers) {
|
||||
const container = document.getElementById('availablePrintersContainer');
|
||||
|
||||
// Filter für verfügbare Drucker
|
||||
const availablePrinters = printers.filter(printer => printer.status === 0);
|
||||
|
||||
if (availablePrinters.length === 0) {
|
||||
container.innerHTML = '<div class="alert alert-warning">Keine verfügbaren Drucker gefunden.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="list-group">';
|
||||
|
||||
availablePrinters.forEach(printer => {
|
||||
html += `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>${printer.name}</strong>
|
||||
<div class="small text-muted">${printer.description}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge bg-success">Verfügbar</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<a href="/admin/jobs" class="btn btn-sm btn-primary">Auftrag erstellen</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Gesamtstatistik hinzufügen
|
||||
const busyPrinters = printers.filter(printer => printer.status === 1).length;
|
||||
const totalPrinters = printers.length;
|
||||
|
||||
if (totalPrinters > 0) {
|
||||
const statsHtml = `
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<small>Verfügbar: ${availablePrinters.length} / ${totalPrinters}</small>
|
||||
<small>Belegt: ${busyPrinters} / ${totalPrinters}</small>
|
||||
</div>
|
||||
<div class="progress mt-1" style="height: 5px;">
|
||||
<div class="progress-bar bg-success" style="width: ${(availablePrinters.length / totalPrinters) * 100}%"></div>
|
||||
<div class="progress-bar bg-warning" style="width: ${(busyPrinters / totalPrinters) * 100}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML += statsHtml;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
443
backend/templates/jobs.html
Normal file
443
backend/templates/jobs.html
Normal file
@@ -0,0 +1,443 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Druckaufträge - MYP API Tester{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Druckaufträge verwalten</h4>
|
||||
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newJobForm">
|
||||
Neuen Auftrag erstellen
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse" id="newJobForm">
|
||||
<div class="card-body border-bottom">
|
||||
<form class="api-form" data-url="/api/jobs" data-method="POST" data-response="createJobResponse" data-reload="true">
|
||||
<div class="mb-3">
|
||||
<label for="jobPrinterId" class="form-label">Drucker</label>
|
||||
<select class="form-control" id="jobPrinterId" name="printerId" required>
|
||||
<option value="">Drucker auswählen...</option>
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="jobDuration" class="form-label">Dauer (Minuten)</label>
|
||||
<input type="number" class="form-control" id="jobDuration" name="durationInMinutes" min="1" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="jobComments" class="form-label">Kommentare</label>
|
||||
<textarea class="form-control" id="jobComments" name="comments" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="allowQueuedJobs" name="allowQueuedJobs" value="true">
|
||||
<label class="form-check-label" for="allowQueuedJobs">
|
||||
Auftrag in Warteschlange erlauben (wenn Drucker belegt ist)
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Auftrag erstellen</button>
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="createJobResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="api-form mb-3" data-url="/api/jobs" data-method="GET" data-response="jobsResponse">
|
||||
<button type="submit" class="btn btn-primary">Aufträge aktualisieren</button>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Drucker</th>
|
||||
<th>Benutzer</th>
|
||||
<th>Start</th>
|
||||
<th>Dauer (Min)</th>
|
||||
<th>Verbleibend (Min)</th>
|
||||
<th>Status</th>
|
||||
<th>Kommentare</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jobsTableBody">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>API-Antwort:</h6>
|
||||
<pre class="api-response" id="jobsResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job abbrechen Modal -->
|
||||
<div class="modal fade" id="abortJobModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Auftrag abbrechen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Möchten Sie den Auftrag wirklich abbrechen?</p>
|
||||
<form id="abortJobForm" class="api-form" data-method="POST" data-response="abortJobResponse" data-reload="true">
|
||||
<input type="hidden" id="abortJobId" name="jobId">
|
||||
<div class="mb-3">
|
||||
<label for="abortReason" class="form-label">Abbruchgrund</label>
|
||||
<textarea class="form-control" id="abortReason" name="reason" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="abortJobResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" form="abortJobForm" class="btn btn-danger">Auftrag abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job beenden Modal -->
|
||||
<div class="modal fade" id="finishJobModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Auftrag beenden</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Möchten Sie den Auftrag als beendet markieren?</p>
|
||||
<form id="finishJobForm" class="api-form" data-method="POST" data-response="finishJobResponse" data-reload="true">
|
||||
<input type="hidden" id="finishJobId" name="jobId">
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="finishJobResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" form="finishJobForm" class="btn btn-success">Auftrag beenden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job verlängern Modal -->
|
||||
<div class="modal fade" id="extendJobModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Auftrag verlängern</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="extendJobForm" class="api-form" data-method="POST" data-response="extendJobResponse" data-reload="true">
|
||||
<input type="hidden" id="extendJobId" name="jobId">
|
||||
<div class="mb-3">
|
||||
<label for="extendHours" class="form-label">Stunden</label>
|
||||
<input type="number" class="form-control" id="extendHours" name="hours" min="0" value="0">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="extendMinutes" class="form-label">Minuten</label>
|
||||
<input type="number" class="form-control" id="extendMinutes" name="minutes" min="0" max="59" value="30">
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="extendJobResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" form="extendJobForm" class="btn btn-primary">Auftrag verlängern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Kommentare bearbeiten Modal -->
|
||||
<div class="modal fade" id="editCommentsModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Kommentare bearbeiten</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editCommentsForm" class="api-form" data-method="PUT" data-response="editCommentsResponse" data-reload="true">
|
||||
<input type="hidden" id="editCommentsJobId" name="jobId">
|
||||
<div class="mb-3">
|
||||
<label for="editJobComments" class="form-label">Kommentare</label>
|
||||
<textarea class="form-control" id="editJobComments" name="comments" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="editCommentsResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" form="editCommentsForm" class="btn btn-primary">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job freischalten Modal -->
|
||||
<div class="modal fade" id="approveJobModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Druckauftrag freischalten</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Möchten Sie diesen Druckauftrag jetzt freischalten und starten?</p>
|
||||
<p><strong>Hinweis:</strong> Der Drucker muss verfügbar sein, damit der Auftrag gestartet werden kann.</p>
|
||||
<form id="approveJobForm" class="api-form" data-method="POST" data-response="approveJobResponse" data-reload="true">
|
||||
<input type="hidden" id="approveJobId" name="jobId">
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="approveJobResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" form="approveJobForm" class="btn btn-success">Freischalten</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Drucker für Dropdown laden
|
||||
loadPrinters();
|
||||
|
||||
// Aufträge laden
|
||||
document.querySelector('form[data-url="/api/jobs"]').dispatchEvent(new Event('submit'));
|
||||
|
||||
// Tabelle aktualisieren, wenn Aufträge geladen werden
|
||||
const jobsResponse = document.getElementById('jobsResponse');
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
try {
|
||||
const jobs = JSON.parse(jobsResponse.textContent);
|
||||
updateJobsTable(jobs);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Parsen der Auftrags-Daten:', e);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(jobsResponse, { childList: true, characterData: true, subtree: true });
|
||||
|
||||
// Abort-Modal vorbereiten
|
||||
document.getElementById('abortJobModal').addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const jobId = button.getAttribute('data-job-id');
|
||||
|
||||
document.getElementById('abortJobId').value = jobId;
|
||||
document.getElementById('abortJobForm').setAttribute('data-url', `/api/jobs/${jobId}/abort`);
|
||||
});
|
||||
|
||||
// Finish-Modal vorbereiten
|
||||
document.getElementById('finishJobModal').addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const jobId = button.getAttribute('data-job-id');
|
||||
|
||||
document.getElementById('finishJobId').value = jobId;
|
||||
document.getElementById('finishJobForm').setAttribute('data-url', `/api/jobs/${jobId}/finish`);
|
||||
});
|
||||
|
||||
// Extend-Modal vorbereiten
|
||||
document.getElementById('extendJobModal').addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const jobId = button.getAttribute('data-job-id');
|
||||
|
||||
document.getElementById('extendJobId').value = jobId;
|
||||
document.getElementById('extendJobForm').setAttribute('data-url', `/api/jobs/${jobId}/extend`);
|
||||
});
|
||||
|
||||
// Edit-Comments-Modal vorbereiten
|
||||
document.getElementById('editCommentsModal').addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const jobId = button.getAttribute('data-job-id');
|
||||
const comments = button.getAttribute('data-job-comments');
|
||||
|
||||
document.getElementById('editCommentsJobId').value = jobId;
|
||||
document.getElementById('editCommentsForm').setAttribute('data-url', `/api/jobs/${jobId}/comments`);
|
||||
document.getElementById('editJobComments').value = comments || '';
|
||||
});
|
||||
|
||||
// Approve-Modal vorbereiten
|
||||
document.getElementById('approveJobModal').addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const jobId = button.getAttribute('data-job-id');
|
||||
|
||||
document.getElementById('approveJobId').value = jobId;
|
||||
document.getElementById('approveJobForm').setAttribute('data-url', `/api/jobs/${jobId}/approve`);
|
||||
});
|
||||
});
|
||||
|
||||
async function loadPrinters() {
|
||||
try {
|
||||
const response = await fetch('/api/printers');
|
||||
const printers = await response.json();
|
||||
|
||||
const selectElement = document.getElementById('jobPrinterId');
|
||||
selectElement.innerHTML = '<option value="">Drucker auswählen...</option>';
|
||||
|
||||
// Drucker anzeigen (alle, da man jetzt auch für belegte Drucker Jobs erstellen kann)
|
||||
printers.forEach(printer => {
|
||||
const option = document.createElement('option');
|
||||
option.value = printer.id;
|
||||
|
||||
// Status-Information zum Drucker hinzufügen
|
||||
const statusText = printer.status === 0 ? '(Verfügbar)' : '(Belegt)';
|
||||
option.textContent = `${printer.name} - ${printer.description} ${statusText}`;
|
||||
|
||||
// Belegte Drucker visuell unterscheiden
|
||||
if (printer.status !== 0) {
|
||||
option.classList.add('text-muted');
|
||||
}
|
||||
|
||||
selectElement.appendChild(option);
|
||||
});
|
||||
|
||||
// Hinweis auf die Checkbox für Warteschlange anzeigen oder verstecken
|
||||
const allowQueuedJobsCheckbox = document.getElementById('allowQueuedJobs');
|
||||
const queueCheckboxContainer = allowQueuedJobsCheckbox.closest('.form-check');
|
||||
|
||||
// Prüfen, ob es belegte Drucker gibt
|
||||
const hasBusyPrinters = printers.some(printer => printer.status !== 0);
|
||||
queueCheckboxContainer.style.display = hasBusyPrinters ? 'block' : 'none';
|
||||
|
||||
// Event-Listener für die Druckerauswahl hinzufügen
|
||||
selectElement.addEventListener('change', function() {
|
||||
const selectedPrinterId = this.value;
|
||||
const selectedPrinter = printers.find(p => p.id === selectedPrinterId);
|
||||
|
||||
if (selectedPrinter && selectedPrinter.status !== 0) {
|
||||
// Wenn ein belegter Drucker ausgewählt wird, Checkbox für Warteschlange anzeigen
|
||||
queueCheckboxContainer.style.display = 'block';
|
||||
allowQueuedJobsCheckbox.checked = true;
|
||||
} else if (selectedPrinter && selectedPrinter.status === 0) {
|
||||
// Wenn ein verfügbarer Drucker ausgewählt wird, Checkbox für Warteschlange verstecken
|
||||
allowQueuedJobsCheckbox.checked = false;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Drucker:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function updateJobsTable(jobs) {
|
||||
const tableBody = document.getElementById('jobsTableBody');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
jobs.forEach(job => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
const startDate = new Date(job.startAt);
|
||||
const formattedStart = startDate.toLocaleString();
|
||||
|
||||
const isActive = !job.aborted && job.remainingMinutes > 0 && !job.waitingApproval;
|
||||
const isWaiting = !job.aborted && job.waitingApproval;
|
||||
|
||||
let statusText = '';
|
||||
let statusClass = '';
|
||||
|
||||
if (job.aborted) {
|
||||
statusText = 'Abgebrochen';
|
||||
statusClass = 'text-danger';
|
||||
} else if (job.waitingApproval) {
|
||||
statusText = 'Wartet auf Freischaltung';
|
||||
statusClass = 'text-info';
|
||||
} else if (job.remainingMinutes <= 0) {
|
||||
statusText = 'Abgeschlossen';
|
||||
statusClass = 'text-success';
|
||||
} else {
|
||||
statusText = 'Aktiv';
|
||||
statusClass = 'text-warning';
|
||||
}
|
||||
|
||||
// Zeige die verbleibende Zeit an
|
||||
const remainingTime = job.waitingApproval ? '-' : job.remainingMinutes;
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${job.id}</td>
|
||||
<td>${job.printerId}</td>
|
||||
<td>${job.userId}</td>
|
||||
<td>${formattedStart}</td>
|
||||
<td>${job.durationInMinutes}</td>
|
||||
<td>${remainingTime}</td>
|
||||
<td><span class="${statusClass}">${statusText}</span></td>
|
||||
<td>${job.comments || '-'}</td>
|
||||
<td>
|
||||
${isActive ? `
|
||||
<button type="button" class="btn btn-sm btn-danger mb-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#abortJobModal"
|
||||
data-job-id="${job.id}">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success mb-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#finishJobModal"
|
||||
data-job-id="${job.id}">
|
||||
Beenden
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary mb-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#extendJobModal"
|
||||
data-job-id="${job.id}">
|
||||
Verlängern
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
${isWaiting ? `
|
||||
<button type="button" class="btn btn-sm btn-success mb-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#approveJobModal"
|
||||
data-job-id="${job.id}">
|
||||
Freischalten
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger mb-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#abortJobModal"
|
||||
data-job-id="${job.id}">
|
||||
Abbrechen
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
<button type="button" class="btn btn-sm btn-secondary mb-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#editCommentsModal"
|
||||
data-job-id="${job.id}"
|
||||
data-job-comments="${job.comments || ''}">
|
||||
Kommentare
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
37
backend/templates/login.html
Normal file
37
backend/templates/login.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Anmelden - MYP API Tester{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Anmelden</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="api-form" data-url="/auth/login" data-method="POST" data-response="loginResponse">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Passwort</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-3">
|
||||
<p>Noch kein Konto? <a href="/register">Registrieren</a></p>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h5>Antwort:</h5>
|
||||
<pre class="api-response" id="loginResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
280
backend/templates/printers.html
Normal file
280
backend/templates/printers.html
Normal file
@@ -0,0 +1,280 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Drucker - MYP API Tester{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Drucker verwalten</h4>
|
||||
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newPrinterForm">
|
||||
Neuen Drucker hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse" id="newPrinterForm">
|
||||
<div class="card-body border-bottom">
|
||||
<form class="api-form" data-url="/api/printers" data-method="POST" data-response="createPrinterResponse" data-reload="true">
|
||||
<div class="mb-3">
|
||||
<label for="printerName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="printerName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="printerDescription" class="form-label">Beschreibung</label>
|
||||
<textarea class="form-control" id="printerDescription" name="description" rows="3" required></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="printerStatus" class="form-label">Status</label>
|
||||
<select class="form-control" id="printerStatus" name="status">
|
||||
<option value="0">Verfügbar (0)</option>
|
||||
<option value="1">Besetzt (1)</option>
|
||||
<option value="2">Wartung (2)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="printerIpAddress" class="form-label">IP-Adresse (Tapo Steckdose)</label>
|
||||
<input type="text" class="form-control" id="printerIpAddress" name="ipAddress" placeholder="z.B. 192.168.1.100">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Drucker erstellen</button>
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="createPrinterResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="api-form mb-3" data-url="/api/printers" data-method="GET" data-response="printersResponse">
|
||||
<button type="submit" class="btn btn-primary">Drucker aktualisieren</button>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Status</th>
|
||||
<th>IP-Adresse</th>
|
||||
<th>Aktueller Job</th>
|
||||
<th>Wartende Jobs</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="printersTableBody">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>API-Antwort:</h6>
|
||||
<pre class="api-response" id="printersResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drucker bearbeiten Modal -->
|
||||
<div class="modal fade" id="editPrinterModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Drucker bearbeiten</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editPrinterForm" class="api-form" data-method="PUT" data-response="editPrinterResponse" data-reload="true">
|
||||
<input type="hidden" id="editPrinterId" name="printerId">
|
||||
<div class="mb-3">
|
||||
<label for="editPrinterName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="editPrinterName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editPrinterDescription" class="form-label">Beschreibung</label>
|
||||
<textarea class="form-control" id="editPrinterDescription" name="description" rows="3" required></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editPrinterStatus" class="form-label">Status</label>
|
||||
<select class="form-control" id="editPrinterStatus" name="status">
|
||||
<option value="0">Verfügbar (0)</option>
|
||||
<option value="1">Besetzt (1)</option>
|
||||
<option value="2">Wartung (2)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editPrinterIpAddress" class="form-label">IP-Adresse (Tapo Steckdose)</label>
|
||||
<input type="text" class="form-control" id="editPrinterIpAddress" name="ipAddress" placeholder="z.B. 192.168.1.100">
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="editPrinterResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" form="editPrinterForm" class="btn btn-primary">Änderungen speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drucker löschen Modal -->
|
||||
<div class="modal fade" id="deletePrinterModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Drucker löschen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Möchten Sie den Drucker <span id="deletePrinterName"></span> wirklich löschen?</p>
|
||||
<form id="deletePrinterForm" class="api-form" data-method="DELETE" data-response="deletePrinterResponse" data-reload="true">
|
||||
<input type="hidden" id="deletePrinterId" name="printerId">
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="deletePrinterResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" form="deletePrinterForm" class="btn btn-danger">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Drucker laden
|
||||
document.querySelector('form[data-url="/api/printers"]').dispatchEvent(new Event('submit'));
|
||||
|
||||
// Tabelle aktualisieren, wenn Drucker geladen werden
|
||||
const printersResponse = document.getElementById('printersResponse');
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
try {
|
||||
const printers = JSON.parse(printersResponse.textContent);
|
||||
updatePrintersTable(printers);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Parsen der Drucker-Daten:', e);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(printersResponse, { childList: true, characterData: true, subtree: true });
|
||||
|
||||
// Edit-Modal vorbereiten
|
||||
document.getElementById('editPrinterModal').addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const printerId = button.getAttribute('data-printer-id');
|
||||
const printerName = button.getAttribute('data-printer-name');
|
||||
const printerDescription = button.getAttribute('data-printer-description');
|
||||
const printerStatus = button.getAttribute('data-printer-status');
|
||||
const printerIpAddress = button.getAttribute('data-printer-ip');
|
||||
|
||||
document.getElementById('editPrinterId').value = printerId;
|
||||
document.getElementById('editPrinterForm').setAttribute('data-url', `/api/printers/${printerId}`);
|
||||
document.getElementById('editPrinterName').value = printerName;
|
||||
document.getElementById('editPrinterDescription').value = printerDescription;
|
||||
document.getElementById('editPrinterStatus').value = printerStatus;
|
||||
document.getElementById('editPrinterIpAddress').value = printerIpAddress || '';
|
||||
});
|
||||
|
||||
// Delete-Modal vorbereiten
|
||||
document.getElementById('deletePrinterModal').addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const printerId = button.getAttribute('data-printer-id');
|
||||
const printerName = button.getAttribute('data-printer-name');
|
||||
|
||||
document.getElementById('deletePrinterId').value = printerId;
|
||||
document.getElementById('deletePrinterForm').setAttribute('data-url', `/api/printers/${printerId}`);
|
||||
document.getElementById('deletePrinterName').textContent = printerName;
|
||||
});
|
||||
});
|
||||
|
||||
function updatePrintersTable(printers) {
|
||||
const tableBody = document.getElementById('printersTableBody');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
printers.forEach(printer => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
const statusText = {
|
||||
0: 'Verfügbar',
|
||||
1: 'Besetzt',
|
||||
2: 'Wartung'
|
||||
}[printer.status] || 'Unbekannt';
|
||||
|
||||
const statusClass = {
|
||||
0: 'text-success',
|
||||
1: 'text-warning',
|
||||
2: 'text-danger'
|
||||
}[printer.status] || '';
|
||||
|
||||
// Informationen zum aktuellen Job
|
||||
let currentJobInfo = '-';
|
||||
if (printer.latestJob && printer.status === 1) {
|
||||
// Verbleibende Zeit berechnen
|
||||
const remainingTime = printer.latestJob.remainingMinutes || 0;
|
||||
currentJobInfo = `
|
||||
<div class="small">
|
||||
<strong>ID:</strong> ${printer.latestJob.id.substring(0, 8)}...<br>
|
||||
<strong>Dauer:</strong> ${printer.latestJob.durationInMinutes} Min<br>
|
||||
<strong>Verbleibend:</strong> ${remainingTime} Min
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Wartende Jobs anzeigen
|
||||
let waitingJobsInfo = '-';
|
||||
if (printer.waitingJobs && printer.waitingJobs.length > 0) {
|
||||
const waitingJobsCount = printer.waitingJobs.length;
|
||||
waitingJobsInfo = `
|
||||
<div class="small">
|
||||
<strong>${waitingJobsCount} Job${waitingJobsCount !== 1 ? 's' : ''} in Warteschlange</strong><br>
|
||||
${printer.waitingJobs.map((job, index) =>
|
||||
`<span>${index + 1}. Job ${job.id.substring(0, 8)}... (${job.durationInMinutes} Min)</span>`
|
||||
).join('<br>')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${printer.id}</td>
|
||||
<td>${printer.name}</td>
|
||||
<td>${printer.description}</td>
|
||||
<td><span class="${statusClass}">${statusText} (${printer.status})</span></td>
|
||||
<td>${printer.ipAddress || '-'}</td>
|
||||
<td>${currentJobInfo}</td>
|
||||
<td>${waitingJobsInfo}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#editPrinterModal"
|
||||
data-printer-id="${printer.id}"
|
||||
data-printer-name="${printer.name}"
|
||||
data-printer-description="${printer.description}"
|
||||
data-printer-status="${printer.status}"
|
||||
data-printer-ip="${printer.ipAddress || ''}">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deletePrinterModal"
|
||||
data-printer-id="${printer.id}"
|
||||
data-printer-name="${printer.name}">
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
45
backend/templates/register.html
Normal file
45
backend/templates/register.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Registrieren - MYP API Tester{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Registrieren</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="api-form" data-url="/auth/register" data-method="POST" data-response="registerResponse">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Passwort</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="displayName" class="form-label">Anzeigename</label>
|
||||
<input type="text" class="form-control" id="displayName" name="displayName">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">E-Mail</label>
|
||||
<input type="email" class="form-control" id="email" name="email">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Registrieren</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-3">
|
||||
<p>Bereits registriert? <a href="/login">Anmelden</a></p>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h5>Antwort:</h5>
|
||||
<pre class="api-response" id="registerResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
395
backend/templates/stats.html
Normal file
395
backend/templates/stats.html
Normal file
@@ -0,0 +1,395 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Statistiken - MYP API Tester{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Systemstatistiken</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="api-form mb-3" data-url="/api/stats" data-method="GET" data-response="statsResponse">
|
||||
<button type="submit" class="btn btn-primary">Statistiken aktualisieren</button>
|
||||
</form>
|
||||
|
||||
<div class="row" id="statsContainer">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
</div>
|
||||
|
||||
<!-- Problem-Drucker-Bereich -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<h5 class="mb-0">Drucker mit Verbindungsproblemen</h5>
|
||||
</div>
|
||||
<div class="card-body" id="problemPrintersContainer">
|
||||
<div class="alert alert-info">Keine Verbindungsprobleme festgestellt.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uptime-Grafik -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0">Steckdosen-Verfügbarkeit</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="api-form mb-3" data-url="/api/uptime" data-method="GET" data-response="uptimeResponse">
|
||||
<button type="submit" class="btn btn-primary">Uptime-Daten laden</button>
|
||||
</form>
|
||||
<canvas id="uptimeChart" width="100%" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API-Antworten -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<h6>Stats API-Antwort:</h6>
|
||||
<pre class="api-response" id="statsResponse"></pre>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Uptime API-Antwort:</h6>
|
||||
<pre class="api-response" id="uptimeResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<!-- Chart.js für Diagramme -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
let uptimeChart;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Statistiken laden
|
||||
document.querySelector('form[data-url="/api/stats"]').dispatchEvent(new Event('submit'));
|
||||
document.querySelector('form[data-url="/api/uptime"]').dispatchEvent(new Event('submit'));
|
||||
|
||||
// Statistiken aktualisieren, wenn API-Antwort geladen wird
|
||||
const statsResponse = document.getElementById('statsResponse');
|
||||
const statsObserver = new MutationObserver(function(mutations) {
|
||||
try {
|
||||
const stats = JSON.parse(statsResponse.textContent);
|
||||
updateStatsDisplay(stats);
|
||||
updateProblemPrinters(stats);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Parsen der Statistik-Daten:', e);
|
||||
}
|
||||
});
|
||||
|
||||
statsObserver.observe(statsResponse, { childList: true, characterData: true, subtree: true });
|
||||
|
||||
// Uptime-Daten aktualisieren, wenn API-Antwort geladen wird
|
||||
const uptimeResponse = document.getElementById('uptimeResponse');
|
||||
const uptimeObserver = new MutationObserver(function(mutations) {
|
||||
try {
|
||||
const uptime = JSON.parse(uptimeResponse.textContent);
|
||||
updateUptimeChart(uptime);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Parsen der Uptime-Daten:', e);
|
||||
}
|
||||
});
|
||||
|
||||
uptimeObserver.observe(uptimeResponse, { childList: true, characterData: true, subtree: true });
|
||||
|
||||
// Periodische Aktualisierung
|
||||
setInterval(function() {
|
||||
document.querySelector('form[data-url="/api/stats"]').dispatchEvent(new Event('submit'));
|
||||
document.querySelector('form[data-url="/api/uptime"]').dispatchEvent(new Event('submit'));
|
||||
}, 60000); // Alle 60 Sekunden aktualisieren
|
||||
});
|
||||
|
||||
function updateStatsDisplay(stats) {
|
||||
const container = document.getElementById('statsContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Drucker-Statistiken
|
||||
const printerStats = document.createElement('div');
|
||||
printerStats.className = 'col-md-4 mb-3';
|
||||
printerStats.innerHTML = `
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">Drucker</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Gesamt:</span>
|
||||
<span>${stats.printers.total}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Verfügbar:</span>
|
||||
<span>${stats.printers.available}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Auslastung:</span>
|
||||
<span>${Math.round(stats.printers.utilization_rate * 100)}%</span>
|
||||
</div>
|
||||
<div class="progress mt-3 mb-3">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
style="width: ${Math.round(stats.printers.utilization_rate * 100)}%">
|
||||
${Math.round(stats.printers.utilization_rate * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Online:</span>
|
||||
<span>${stats.printers.online}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Offline:</span>
|
||||
<span>${stats.printers.offline}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Verbindungsrate:</span>
|
||||
<span>${Math.round(stats.printers.connectivity_rate * 100)}%</span>
|
||||
</div>
|
||||
<div class="progress mt-3">
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: ${Math.round(stats.printers.connectivity_rate * 100)}%">
|
||||
${Math.round(stats.printers.connectivity_rate * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Job-Statistiken
|
||||
const jobStats = document.createElement('div');
|
||||
jobStats.className = 'col-md-4 mb-3';
|
||||
jobStats.innerHTML = `
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">Druckaufträge</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Gesamt:</span>
|
||||
<span>${stats.jobs.total}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Aktiv:</span>
|
||||
<span>${stats.jobs.active}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Abgeschlossen:</span>
|
||||
<span>${stats.jobs.completed}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Durchschnittliche Dauer:</span>
|
||||
<span>${stats.jobs.avg_duration} Minuten</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Benutzer- und Uptime-Statistiken
|
||||
const userStats = document.createElement('div');
|
||||
userStats.className = 'col-md-4 mb-3';
|
||||
userStats.innerHTML = `
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0">System</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Benutzer:</span>
|
||||
<span>${stats.users.total}</span>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Verbindungsausfälle (7 Tage):</span>
|
||||
<span>${stats.uptime.outages_last_7_days}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Aktuelle Probleme:</span>
|
||||
<span>${stats.uptime.problem_printers.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(printerStats);
|
||||
container.appendChild(jobStats);
|
||||
container.appendChild(userStats);
|
||||
}
|
||||
|
||||
function updateProblemPrinters(stats) {
|
||||
const container = document.getElementById('problemPrintersContainer');
|
||||
const problemPrinters = stats.uptime.problem_printers;
|
||||
|
||||
if (problemPrinters.length === 0) {
|
||||
container.innerHTML = '<div class="alert alert-info">Keine Verbindungsprobleme festgestellt.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="table-responsive"><table class="table table-striped">';
|
||||
html += '<thead><tr><th>Drucker</th><th>Status</th><th>Offline seit</th><th>Dauer</th></tr></thead>';
|
||||
html += '<tbody>';
|
||||
|
||||
problemPrinters.forEach(printer => {
|
||||
let offlineSince = 'Unbekannt';
|
||||
let duration = 'Unbekannt';
|
||||
|
||||
if (printer.last_seen) {
|
||||
try {
|
||||
const lastSeen = new Date(printer.last_seen);
|
||||
const now = new Date();
|
||||
const diffSeconds = Math.floor((now - lastSeen) / 1000);
|
||||
const hours = Math.floor(diffSeconds / 3600);
|
||||
const minutes = Math.floor((diffSeconds % 3600) / 60);
|
||||
|
||||
offlineSince = lastSeen.toLocaleString();
|
||||
duration = `${hours}h ${minutes}m`;
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Berechnen der Offline-Zeit:', e);
|
||||
}
|
||||
}
|
||||
|
||||
html += `<tr>
|
||||
<td>${printer.name}</td>
|
||||
<td><span class="badge bg-danger">Offline</span></td>
|
||||
<td>${offlineSince}</td>
|
||||
<td>${duration}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function updateUptimeChart(uptimeData) {
|
||||
// Wenn keine Daten vorhanden sind, nichts tun
|
||||
if (!uptimeData || !uptimeData.sockets || uptimeData.sockets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Daten für das Diagramm vorbereiten
|
||||
const socketNames = [];
|
||||
const datasets = [];
|
||||
const colors = {
|
||||
online: 'rgba(40, 167, 69, 0.7)',
|
||||
offline: 'rgba(220, 53, 69, 0.7)',
|
||||
unknown: 'rgba(108, 117, 125, 0.7)'
|
||||
};
|
||||
|
||||
// Zeitraum für das Diagramm (letzten 7 Tage)
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - 7);
|
||||
|
||||
// Für jede Steckdose
|
||||
uptimeData.sockets.forEach(socket => {
|
||||
socketNames.push(socket.name);
|
||||
|
||||
// Sortiere Ereignisse nach Zeitstempel
|
||||
if (socket.events) {
|
||||
socket.events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||
|
||||
// Erstelle einen Datensatz für diese Steckdose
|
||||
const data = [];
|
||||
|
||||
// Füge Ereignisse zum Datensatz hinzu
|
||||
socket.events.forEach(event => {
|
||||
data.push({
|
||||
x: new Date(event.timestamp),
|
||||
y: event.status === 'online' ? 1 : 0,
|
||||
status: event.status,
|
||||
duration: event.duration_seconds ?
|
||||
formatDuration(event.duration_seconds) : null
|
||||
});
|
||||
});
|
||||
|
||||
// Füge aktuellen Status hinzu
|
||||
if (socket.current_status) {
|
||||
data.push({
|
||||
x: new Date(),
|
||||
y: socket.current_status.connection_status === 'online' ? 1 : 0,
|
||||
status: socket.current_status.connection_status,
|
||||
duration: null
|
||||
});
|
||||
}
|
||||
|
||||
datasets.push({
|
||||
label: socket.name,
|
||||
data: data,
|
||||
stepped: true,
|
||||
borderColor: colors[socket.current_status?.connection_status || 'unknown'],
|
||||
backgroundColor: colors[socket.current_status?.connection_status || 'unknown'],
|
||||
fill: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Chart.js Konfiguration
|
||||
const ctx = document.getElementById('uptimeChart').getContext('2d');
|
||||
|
||||
// Wenn Chart bereits existiert, zerstöre ihn
|
||||
if (uptimeChart) {
|
||||
uptimeChart.destroy();
|
||||
}
|
||||
|
||||
// Erstelle neuen Chart
|
||||
uptimeChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: datasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const point = context.raw;
|
||||
let label = context.dataset.label || '';
|
||||
label += ': ' + (point.status === 'online' ? 'Online' : 'Offline');
|
||||
if (point.duration) {
|
||||
label += ' (Dauer: ' + point.duration + ')';
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'day'
|
||||
},
|
||||
min: startDate,
|
||||
max: endDate
|
||||
},
|
||||
y: {
|
||||
min: -0.1,
|
||||
max: 1.1,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value === 0 ? 'Offline' : value === 1 ? 'Online' : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
238
backend/templates/users.html
Normal file
238
backend/templates/users.html
Normal file
@@ -0,0 +1,238 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Benutzer - MYP API Tester{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Benutzer verwalten</h4>
|
||||
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newUserForm">
|
||||
Neuen Benutzer hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse" id="newUserForm">
|
||||
<div class="card-body border-bottom">
|
||||
<form class="api-form" data-url="/auth/register" data-method="POST" data-response="createUserResponse" data-reload="true">
|
||||
<div class="mb-3">
|
||||
<label for="userName" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="userName" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="userPassword" class="form-label">Passwort</label>
|
||||
<input type="password" class="form-control" id="userPassword" name="password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="userDisplayName" class="form-label">Anzeigename</label>
|
||||
<input type="text" class="form-control" id="userDisplayName" name="displayName">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="userEmail" class="form-label">E-Mail</label>
|
||||
<input type="email" class="form-control" id="userEmail" name="email">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Benutzer erstellen</button>
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="createUserResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="api-form mb-3" data-url="/api/users" data-method="GET" data-response="usersResponse">
|
||||
<button type="submit" class="btn btn-primary">Benutzer aktualisieren</button>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Benutzername</th>
|
||||
<th>Anzeigename</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Rolle</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>API-Antwort:</h6>
|
||||
<pre class="api-response" id="usersResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Benutzer bearbeiten Modal -->
|
||||
<div class="modal fade" id="editUserModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Benutzer bearbeiten</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editUserForm" class="api-form" data-method="PUT" data-response="editUserResponse" data-reload="true">
|
||||
<input type="hidden" id="editUserId" name="userId">
|
||||
<div class="mb-3">
|
||||
<label for="editUserName" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="editUserName" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editUserDisplayName" class="form-label">Anzeigename</label>
|
||||
<input type="text" class="form-control" id="editUserDisplayName" name="displayName">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editUserEmail" class="form-label">E-Mail</label>
|
||||
<input type="email" class="form-control" id="editUserEmail" name="email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editUserRole" class="form-label">Rolle</label>
|
||||
<select class="form-control" id="editUserRole" name="role">
|
||||
<option value="user">Benutzer</option>
|
||||
<option value="admin">Administrator</option>
|
||||
<option value="guest">Gast</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="editUserResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" form="editUserForm" class="btn btn-primary">Änderungen speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Benutzer löschen Modal -->
|
||||
<div class="modal fade" id="deleteUserModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Benutzer löschen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Möchten Sie den Benutzer <span id="deleteUserName"></span> wirklich löschen?</p>
|
||||
<form id="deleteUserForm" class="api-form" data-method="DELETE" data-response="deleteUserResponse" data-reload="true">
|
||||
<input type="hidden" id="deleteUserId" name="userId">
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<h6>Antwort:</h6>
|
||||
<pre class="api-response" id="deleteUserResponse"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" form="deleteUserForm" class="btn btn-danger">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Benutzer laden
|
||||
document.querySelector('form[data-url="/api/users"]').dispatchEvent(new Event('submit'));
|
||||
|
||||
// Tabelle aktualisieren, wenn Benutzer geladen werden
|
||||
const usersResponse = document.getElementById('usersResponse');
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
try {
|
||||
const users = JSON.parse(usersResponse.textContent);
|
||||
updateUsersTable(users);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Parsen der Benutzer-Daten:', e);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(usersResponse, { childList: true, characterData: true, subtree: true });
|
||||
|
||||
// Edit-Modal vorbereiten
|
||||
document.getElementById('editUserModal').addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const userId = button.getAttribute('data-user-id');
|
||||
const userName = button.getAttribute('data-user-name');
|
||||
const userDisplayName = button.getAttribute('data-user-displayname');
|
||||
const userEmail = button.getAttribute('data-user-email');
|
||||
const userRole = button.getAttribute('data-user-role');
|
||||
|
||||
document.getElementById('editUserId').value = userId;
|
||||
document.getElementById('editUserForm').setAttribute('data-url', `/api/users/${userId}`);
|
||||
document.getElementById('editUserName').value = userName;
|
||||
document.getElementById('editUserDisplayName').value = userDisplayName || '';
|
||||
document.getElementById('editUserEmail').value = userEmail || '';
|
||||
document.getElementById('editUserRole').value = userRole;
|
||||
});
|
||||
|
||||
// Delete-Modal vorbereiten
|
||||
document.getElementById('deleteUserModal').addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const userId = button.getAttribute('data-user-id');
|
||||
const userName = button.getAttribute('data-user-name');
|
||||
|
||||
document.getElementById('deleteUserId').value = userId;
|
||||
document.getElementById('deleteUserForm').setAttribute('data-url', `/api/users/${userId}`);
|
||||
document.getElementById('deleteUserName').textContent = userName;
|
||||
});
|
||||
});
|
||||
|
||||
function updateUsersTable(users) {
|
||||
const tableBody = document.getElementById('usersTableBody');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
users.forEach(user => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
const roleClass = {
|
||||
'admin': 'text-danger',
|
||||
'user': 'text-primary',
|
||||
'guest': 'text-secondary'
|
||||
}[user.role] || '';
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${user.id}</td>
|
||||
<td>${user.username}</td>
|
||||
<td>${user.displayName || user.username}</td>
|
||||
<td>${user.email || '-'}</td>
|
||||
<td><span class="${roleClass}">${user.role}</span></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#editUserModal"
|
||||
data-user-id="${user.id}"
|
||||
data-user-name="${user.username}"
|
||||
data-user-displayname="${user.displayName || ''}"
|
||||
data-user-email="${user.email || ''}"
|
||||
data-user-role="${user.role}">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteUserModal"
|
||||
data-user-id="${user.id}"
|
||||
data-user-name="${user.username}">
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
1
docs/.gitkeep
Normal file
1
docs/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
53
docs/Aktueller Stand.md
Normal file
53
docs/Aktueller Stand.md
Normal file
@@ -0,0 +1,53 @@
|
||||
- HTTP Broadcast funktioniert nicht -> können IP nicht finden
|
||||
- Backend im Unternehmen, Frontend auf GitHub funktioniert nicht -> Frontend hat HTTPS und Backend nur HTTP, self-signed certificate würde Fehler werfen
|
||||
|
||||
---
|
||||
|
||||
[Knowledge Base - [PG Netzwerk] DHCP/DNS-Services: Anfragen und Änderungen von IP-Adressen/DNS (mercedes-benz.com)](https://servicenow.i.mercedes-benz.com/esc?id=kb_article&table=kb_knowledge&sysparm_article=KB0426678)
|
||||
|
||||
Statische IP beantragen
|
||||
Alias Zuordnung (druckerapp.xyz.corpintra.net)
|
||||
|
||||
!!! SSL Zertifikat???
|
||||
-> [Server-Zertifikat - Employee Center (mercedes-benz.com)](https://servicenow.i.mercedes-benz.com/esc?id=sc_cat_item&table=sc_cat_item&sys_id=7ef47a1c1b0d5450c4f43113dd4bcbf5)
|
||||
8€ (wahrscheinlich einmalig)
|
||||
benötigt Anwenderservice
|
||||
[Application Service (Erstellen) - Employee Center (mercedes-benz.com)](https://servicenow.i.mercedes-benz.com/esc?id=sc_cat_item&table=sc_cat_item&sys_id=ab095def1bf68810c4f43113dd4bcb06)
|
||||
|
||||
|
||||
-> [ServiceNow-Berechtigungen - Employee Center (mercedes-benz.com)](https://servicenow.i.mercedes-benz.com/esc?id=sc_cat_item&table=sc_cat_item&sys_id=8d645a30db3dc4501754ccd40596192c)
|
||||
|
||||
Sollten Sie für Ihre Applikation / Ihr Endgerät eine statisch zugewiesene IP Adresse (S-DHCP), einen manuellen IP-Pool (M-DHCP) oder eine entsprechende neue oder angepasste DNS-Zuordnung benötigen, nutzen Sie bitte die nachfolgenden SNOW-Templates (Hinweis zur Nutzung der Ticket-Templates benötigen Sie die **Service Now - Rechte eines "Agent Fulfilers"**. Die Beantragung für diese Rechte ist über das SNOW Employee Service Portal im Social Intranet möglich: [ServiceNow Permissions - Employee Service Center (mercedes-benz.com))](https://servicenow.i.mercedes-benz.com/esc?id=sc_cat_item&table=sc_cat_item&sys_id=8d645a30db3dc4501754ccd40596192c).
|
||||
-> Martin diesbezüglich Fragen, ob er Sie für sich bestellt?
|
||||
|
||||
---
|
||||
|
||||
- Martin Rechte SNOW
|
||||
- Martin Termin Volker Otto
|
||||
|
||||
[Examples · Diagrams (mingrammer.com)](https://diagrams.mingrammer.com/docs/getting-started/examples)
|
||||
|
||||
Hier ist eine Antwort, die ich mit Microsoft Copilot erhalten habe, der weltweit ersten KI-gestützten Antwort-Engine. Wählen Sie diese Option aus, um die vollständige Antwort anzuzeigen, oder probieren Sie sie selbst aus. https://sl.bing.net/iv24ZLcukjk
|
||||
|
||||
- use devcontainer !!!
|
||||
|
||||
https://x.com/tremorlabs/status/1779873516533547064?s=46
|
||||
|
||||
use mix of shadcn ui and tremor.so blocks
|
||||
|
||||
|
||||
Bestellliste:
|
||||
- 1x Switch
|
||||
- 1x wlan access point
|
||||
- 2x Raspberry Pi (4B / 5)
|
||||
(- Xx LAN-Kabel)
|
||||
- Adapter für 3D-Drucker oder Schaltbare Steckdosen
|
||||
|
||||
-> Datensicherung?!?!?!
|
||||
make it a user thing
|
||||
download db copy
|
||||
import/export
|
||||
OR Zugang zu CORP O über Anwendungsservice??
|
||||
|
||||
|
||||
Move TRILLUM to GITHUB WIKI
|
24
docs/Dokumentation_IHK.md
Normal file
24
docs/Dokumentation_IHK.md
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
Notizen:
|
||||
- Wollten zuerst OpenSUSE, haben uns dagegen entschieden, weil NixOS einfacher zu konfigurieren ist und besser geeignet für diesen Einsatzzweck
|
||||
- Mussten eine IP-Adresse und eine Domain organisieren von unserer IT-Abteilung
|
||||
- haben ein Netzwerkplan gemacht
|
||||
- haben uns die akutellen Prozesse und Konventionen bei der Organisation einer internen Domain angeguckt
|
||||
- haben uns für Raspberrys "entschieden", stand aber mehr oder weniger schon fest weil diese einfach perfekt für den Einsatzzweck sind
|
||||
- Da Till digitale Vernetzung hat macht er Backend, weil die Schnittstelle der Vernetzung zum cyberphysischen System dort lag
|
||||
- für die Dokumentation: Daten (Datums) müssen stimmen!
|
||||
|
||||
python schnittstelle funktionierte nicht
|
||||
nach etlichem rumprobieren festgestellt: geht nicht so einfach
|
||||
wireshark mitschnitt gemacht → auffällig: immer die selben responses bei verschlüsselter verbindung
|
||||
ohne erfolg beim simulieren einzelner anfragen
|
||||
dann: geistesblitz: anfragensequenz muss es sein!
|
||||
hat funktioniert → es hat klick gemacht!! .
|
||||
verbindung verschlüsselt und mit temporärem cookie
|
||||
→ proprietäre Verschlüsselung
|
||||
wie wird die verbindung ausgehandelt?
|
||||
|
||||
------
|
||||
|
||||
11.09 : Teile bestellt im internen Technikshop
|
||||
12.09 : DNS Alias festlegen / beantragen
|
1
docs/Infrastruktur.tldr
Normal file
1
docs/Infrastruktur.tldr
Normal file
File diff suppressed because one or more lines are too long
360
docs/LICENSE.md
Normal file
360
docs/LICENSE.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# Mercedes-Benz Inner Source License 1.0 ("ISL")
|
||||
|
||||
Copyright © 2022 Mercedes-Benz Group AG
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-MB-ISL-1.0
|
||||
|
||||
## 0. Preamble
|
||||
|
||||
0.0 This Mercedes-Benz Inner Source License 1.0 succeeds the
|
||||
Daimler Inner Source License 1.0 as a later version and is similar in spirit.
|
||||
|
||||
0.1 Mercedes-Benz Group AG ("Mercedes-Benz") provides a platform for collaborative
|
||||
development ("SCR") and use of source code and associated data (e.g. parameters,
|
||||
documentation) for use by and for purposes of companies of the Mercedes-Benz
|
||||
Group (§§ 15 et seq. Aktiengesetz, German Stock Companies Act; all such
|
||||
companies together "Mercedes-Benz Companies"). This also includes the use by
|
||||
third parties only as far as these are commissioned directly by a
|
||||
Mercedes-Benz Company with performances for purposes of Mercedes-Benz Companies
|
||||
("Commissioned Third Parties").
|
||||
|
||||
0.2 The SCR and cooperation model is named "Mercedes-Benz
|
||||
Inner Source Platform". Inner Source
|
||||
follows the pattern of Open Source software development, is however restricted to
|
||||
use by and for purposes of Mercedes-Benz Companies due to technical, security,
|
||||
relevance and majority reasons.
|
||||
|
||||
0.3 Source code uploads to repositories ("Projects") on the SCR come
|
||||
with the expectation that other parties in this cooperation model will
|
||||
improve such Projects through comments, suggestions, source code to
|
||||
provide benefits of the collaboration for all involved parties.
|
||||
|
||||
## 1. Scope of ISL
|
||||
|
||||
1.1 The ISL is an agreement by a Mercedes-Benz Company or a Commissioned Third
|
||||
Party, which contributes to or uses content on the SCR ("Participant"),
|
||||
with Mercedes-Benz or another Mercedes-Benz Company also for the direct benefit of
|
||||
other Mercedes-Benz Companies and Commissioned Third Parties. The direct
|
||||
benefit of Commissioned Third Parties is limited to grant of rights
|
||||
according to Section 4.4.
|
||||
|
||||
1.2 The ISL applies to Projects uploaded to or available on the SCR,
|
||||
especially source code and data, that contain this `LICENSE.md` file
|
||||
incorporating the unmodified ISL or that expressly refer to the ISL e.g.
|
||||
by stating "Licensed under Mercedes-Benz Inner Source License 1.0" (all
|
||||
together "Content").
|
||||
|
||||
1.3 The ISL applies to any and all handling and use of Content on the
|
||||
SCR, including copying, integration, making and/or making available
|
||||
modifications as well as any possible use of Content in foreseen or
|
||||
unforeseen manner by any means.
|
||||
|
||||
## 2. Consent to and Prevalence of ISL
|
||||
|
||||
2.1 The ISL is accepted by a Mercedes-Benz Company by accessing or making use
|
||||
of Content on the SCR through its employees or vicarious agents.
|
||||
|
||||
2.2 The ISL is accepted by a Commissioned Third Party by accessing or
|
||||
making use of Content on the SCR through its employees or vicarious
|
||||
agents. Subsidiarily, a user acting for a Commissioned Third Party
|
||||
accepts the ISL by such action for himself/herself and on behalf of such
|
||||
Commissioned Third Party.
|
||||
|
||||
2.3 Subsidiarily to Sections 2.1 and 2.2, the regulations in this ISL
|
||||
for rights to Content in Sections 3 and 4 are accepted by and apply
|
||||
analogously to any party, which contributes Content to the SCR, in
|
||||
respect of the offer of concession of rights according to Section 3.1
|
||||
and the scope of rights granted according to Section 4. Such party is
|
||||
not able to accept an offer in respect of Content (see Section 3.2).
|
||||
|
||||
2.4 The ISL is prevalent for any Content on the SCR. Any exception from
|
||||
the ISL is only valid if and applicable as far as (i) the ISL itself
|
||||
expressly provides for other stipulations or such is expressly agreed
|
||||
upon in written form for each individual case and (ii) in any case the
|
||||
Content to which an exception applies is to be transparently marked as
|
||||
underlying different stipulations.
|
||||
|
||||
2.5 The ISL itself can only be terminated extraordinarily for good cause
|
||||
by notice in written form by one affected party to the other affected
|
||||
parties in respect of specific affected Content. Any obligations which
|
||||
arose before or are caused by such termination remain unaffected. The
|
||||
stipulations on termination of rights according Section 6 of this ISL remain unaffected.
|
||||
|
||||
## 3. Offer of and Consent to Concession of Rights to Content available on SCR
|
||||
|
||||
3.1 The Mercedes-Benz Company or Commissioned Third Party which makes Content
|
||||
available on the SCR thereby offers to any other Mercedes-Benz Company and
|
||||
Commissioned Third Party the rights to Content described in Section 4.
|
||||
This applies irrespective of the means of making available Content on
|
||||
the SCR (e.g. upload, pull request, etc.). Contributions, e.g. through
|
||||
pull request, to Content and/or Projects under ISL shall state,
|
||||
"Licensed under Mercedes-Benz Inner Source License 1.0 and any later version
|
||||
similar in spirit". Such offer is valid as long as such Content or a
|
||||
modification of such Content is available on the SCR.
|
||||
|
||||
3.2 Any Mercedes-Benz Company can accept such offer in respect of Content
|
||||
available on the SCR by accessing or using such Content in any way by
|
||||
itself or through Commissioned Third Parties as agreed between the
|
||||
Mercedes-Benz Company and the Commissioned Third Party in the course of
|
||||
commissioning. A Commissioned Third Party can accept such offer only
|
||||
within the scope of its duties commissioned by a Mercedes-Benz Company and
|
||||
subject to Section 4.4 by accessing or using such Content. Prerequisite
|
||||
for an acceptance is a valid agreement of the ISL. Any restriction or
|
||||
reservation for acceptance of the ISL makes acceptance of the offer null
|
||||
and void.
|
||||
|
||||
## 4. Rights granted to Content
|
||||
|
||||
4.1 Upon acceptance of an offer to Content,
|
||||
the rights to such Content are granted directly by the Participant to
|
||||
the accepting entity non-exclusively.
|
||||
|
||||
4.2 The rights to Content encompass any and all possible handling and
|
||||
use of such Content for any purposes of Mercedes-Benz Companies in any
|
||||
possible foreseen and unforeseen manner by any means and in any form.
|
||||
This especially encompasses any reproduction, translation, adaptation,
|
||||
arrangement and other modifications as well as the reproduction of the
|
||||
results thereof, any form of distribution of Content or modified Content
|
||||
including rental. This also encompasses any use without attribution to
|
||||
any authors of Content as far as legally possible. The rights do not
|
||||
entitle to remove any copyright information or legal notice unless
|
||||
otherwise expressly entitled under this ISL and especially Section 5.5.
|
||||
Any communication of Content to the public is subject to the additional
|
||||
provisions of Section 5.
|
||||
|
||||
4.3 The rights to Content are granted to a Mercedes-Benz Company for the
|
||||
duration of its membership to the Mercedes-Benz Companies.
|
||||
|
||||
4.4 The rights to Content granted to Commissioned Third Parties are
|
||||
limited to the necessary use of Content for the benefit of performances
|
||||
for purposes of Mercedes-Benz Companies as far as and as long as such
|
||||
Commissioned Third Party is commissioned with such performances. For the
|
||||
avoidance of doubt, rights to Content granted to Commissioned Third
|
||||
Parties do also terminate with the end of the membership of its
|
||||
commissioning Mercedes-Benz Company to the Mercedes-Benz Companies.
|
||||
|
||||
4.5 Any grant of rights to Content beyond the scope of this Section 4 by
|
||||
a holder of rights by separate means remains
|
||||
unaffected. As far as a Commissioned Third Party has granted exclusive
|
||||
rights to specific content to a Mercedes-Benz Company, this Mercedes-Benz Company
|
||||
consents to the grant of rights according to this Section 4 by ordering
|
||||
or allowing the Commissioned Third Party to make such content available
|
||||
on the SCR. Any grant of rights according to this ISL does not restrict
|
||||
rights of a Mercedes-Benz Company to Content except as expressly stated in
|
||||
this Section 4.
|
||||
|
||||
4.6 Any rights granted to specific Content according to this Section 4
|
||||
remain unaffected if the availability of the respective Content on the
|
||||
SCR ends.
|
||||
|
||||
4.7 Any license terms and conditions pertaining to Free and Open Source
|
||||
Software (FOSS) as parts of Content or parts of Content from third
|
||||
parties shall prevail over this Section 4. Any obligations of a
|
||||
Participant in connection with FOSS remain unaffected, especially any
|
||||
obligations of a Commissioned Third Parties in the context of its
|
||||
commission.
|
||||
|
||||
4.8 Use of licensed Content under a particular version of the ISL, may
|
||||
continue under the terms of that version of the ISL notwithstanding the
|
||||
application of a later ISL version to a repository. Any party
|
||||
contributing to a repository accepts and grants rights to Content
|
||||
including such later application of a later ISL version in accordance
|
||||
with Section 3.1.
|
||||
|
||||
4.9 Any Participant who makes Content available on a repository in the
|
||||
SCR entitles the employee responsible for a Project ("Repository Owner")
|
||||
of such repository to decide on external use of such Content according
|
||||
to Section 5 in his/her own discretion as far as entitled by its respective
|
||||
Mercedes-Benz Company.
|
||||
|
||||
## 5. External Use of Content
|
||||
|
||||
5.1 Any use of Content beyond the internal purposes of Mercedes-Benz Companies
|
||||
requires the prior express consent of the Repository Owner as far as
|
||||
entitled by its respective Mercedes-Benz Company in each individual case. Use
|
||||
beyond internal purposes of Mercedes-Benz Companies in particular applies to
|
||||
communication of Content to the public, including making available
|
||||
Content or modified Content to the public ("External Use"), whether on a
|
||||
restricted basis or by way of open source software. Consent to External
|
||||
Use is deemed to be given with upload of Content to the SCR, subject to
|
||||
the following provisions. This prerequisite of consent must not be
|
||||
circumvented in any way (e.g. by forking a repository).
|
||||
|
||||
5.2 The rights to Content granted to Commissioned Third Parties are
|
||||
limited according to this ISL and do not
|
||||
include the rights to External Use.
|
||||
|
||||
5.3 The External Use in form of publication under an open source license
|
||||
is limited to the Repository Owner, as far as entitled by the employing
|
||||
Mercedes-Benz Company.
|
||||
|
||||
5.4 For any Content intended for External Use, all current standards,
|
||||
especially for source code, of Mercedes-Benz Companies apply irrespective of
|
||||
the ISL. The Repository Owner has to decide on any External Use
|
||||
including its respective scope in each individual case, taking into
|
||||
account the foreseen benefit for Mercedes-Benz Companies of External
|
||||
Use as well as the inevitable loss of confidentiality of externally
|
||||
disclosed Content. The Repository Owner has to withdraw
|
||||
consent to External Use if its prerequisites lapse. Any Repository Owner
|
||||
shall adequately document any decision in respect of External Use of
|
||||
Content in the repository including the decision and date of decision.
|
||||
|
||||
5.5 As long as the necessary consent of the Repository Owner is not
|
||||
withdrawn, the consent includes the External Use of any later
|
||||
contributions to Content of such repository, irrespective of the means
|
||||
of a contribution (e.g. upload, pull request, etc.). Any rights already
|
||||
granted for External Use of specific Content remain unaffected by a
|
||||
later withdrawal of necessary consent to External Use.
|
||||
|
||||
5.6 Any External Use of Content requires prior relicensing of the
|
||||
Content, e.g. using a Mercedes-Benz proprietary license. For the avoidance of
|
||||
doubt, this ISL shall not be used in externally disclosed Content.
|
||||
|
||||
5.7 Any Participant has to instruct and entitle its Repository Owners
|
||||
according to this Section 5.
|
||||
|
||||
## 6. Termination of Rights granted to Content
|
||||
|
||||
6.1 Any rights to any Content granted to a recipient according to
|
||||
Section 4 terminate automatically and completely if the recipient of
|
||||
such rights
|
||||
|
||||
(i) declares a termination of the ISL, except within the scope and
|
||||
under the prerequisites of Section 2.5,
|
||||
|
||||
(ii) violates the scope of rights granted by this ISL by acts or
|
||||
omissions by itself or through third parties, in case of a Mercedes-Benz
|
||||
Company only after a reasonable cure period expressly set has lapsed
|
||||
without effect, or
|
||||
|
||||
(iii) is a legal entity whose membership to the Mercedes-Benz Companies
|
||||
ends. In case of termination of membership of a Mercedes-Benz Company to the
|
||||
Mercedes-Benz Companies "Former Member", the Former Member requires a
|
||||
separate agreement with the Content providing Mercedes-Benz Companies for
|
||||
the further use of Content.
|
||||
|
||||
For the avoidance of doubt, any rights granted to Content provided by
|
||||
a Former Member do not terminate by this Section 6.
|
||||
|
||||
6.2 Any rights to specific Content granted to a Commissioned Third Party
|
||||
terminate completely and automatically if the commissioning with the
|
||||
performances for Mercedes-Benz Companies, which require the use of the
|
||||
affected Content, ends for whatsoever reason.
|
||||
|
||||
6.3 If the ISL is terminated according to Section 2.5, any and all
|
||||
rights to affected Content granted by the terminating party to the
|
||||
affected parties terminate completely and automatically.
|
||||
|
||||
6.4 If rights to Content are terminated according to Section 6.1(ii) the
|
||||
recipient may request a license to Content from the offering Mercedes-Benz
|
||||
Company at a later point in time. It is solely up to the discretion of
|
||||
the offering Mercedes-Benz Company to grant rights to such party under the ISL
|
||||
again.
|
||||
|
||||
## 7. Prerequisites for Use of Content available on SCR
|
||||
|
||||
7.1 It lies completely in the sole responsibility of any recipient of
|
||||
any Content to assess and verify if and to which extent such Content is
|
||||
suitable and technically qualified to be used for a particular purpose
|
||||
and a particular application intended by such recipient. Any contractual
|
||||
obligations and duties of Commissioned Third Parties and commissioned
|
||||
Mercedes-Benz Companies (a Mercedes-Benz Company that is commissioned by another
|
||||
Mercedes-Benz Company), especially based on any contract with a Mercedes-Benz
|
||||
Company, remain unaffected and are not restricted.
|
||||
|
||||
7.2 The user is solely and completely responsible for any use of Content
|
||||
by itself and on its behalf.
|
||||
|
||||
7.3 The user is solely and completely responsible for the fulfillment of
|
||||
any obligations in connection with Content, especially arising out of
|
||||
Free and Open Source licenses or licenses for content from third
|
||||
parties.
|
||||
|
||||
## 8. Remuneration, Warranty and Liability
|
||||
|
||||
8.1 Content is made available and rights to Content are granted without
|
||||
remuneration but with respect to Section 0.3.
|
||||
|
||||
8.2 Any Content is made available on the SCR "as is" without warranty.
|
||||
There is no warranty provided by any Participant beyond mandatory
|
||||
statutory obligations and subject to Section 8.4.
|
||||
|
||||
8.3 No Participant assumes any liability exceeding mandatory statutory
|
||||
obligations (intentional damage; damage to life and/or body; statutory
|
||||
product liability). This especially applies to any obligations of care
|
||||
or indemnification in connection with Content.
|
||||
|
||||
8.4 Any warranty and liability of Commissioned Third Parties and
|
||||
commissioned Mercedes-Benz Companies, especially based on any contract with a
|
||||
Mercedes-Benz Company, remain unaffected and are not restricted by Sections
|
||||
8.2 and 8.3.
|
||||
|
||||
## 9. Confidentiality of Content
|
||||
|
||||
9.1 As any Content is made available for purposes of Mercedes-Benz Companies
|
||||
only, any user of the SCR is obliged to take adequate precautions
|
||||
against unintended or unauthorized access or disclosure of Content to
|
||||
third parties.
|
||||
|
||||
9.2 Any Content not expressly marked or declared as "public", according
|
||||
to Mercedes-Benz Information Classification, (by and within Mercedes-Benz Companies)
|
||||
is to be treated as confidential and business secret of Mercedes-Benz
|
||||
Companies by any Mercedes-Benz Company and any Commissioned Third Party. Any
|
||||
such party has to take reasonable precautions, at least as for its own
|
||||
business secrets, to keep such Content secret and protect it against
|
||||
unauthorized disclosure to third parties, especially if a third party is
|
||||
not bound to adequate secrecy. Mercedes-Benz Companies may be entitled to use
|
||||
such Content for External Use in adherence with Section 5. For the
|
||||
avoidance of doubt, the SCR function "public" is by no means to be
|
||||
misunderstood to be such a declaration or marking of "public".
|
||||
|
||||
## 10. Final Provisions
|
||||
|
||||
10.1 Any party, which accesses, uses or downloads Content, is solely
|
||||
responsible for adherence to all applicable regulations and requirements
|
||||
including but not limited to the regulations and requirements in
|
||||
connection with export control.
|
||||
|
||||
10.2 Any later or new version of the ISL must be approved by the FOSS
|
||||
Center of Competence or any comparable successor thereof.
|
||||
|
||||
10.3 Except where otherwise expressly stated, this ISL does not
|
||||
encompass grant of rights to trade marks, patents or similar
|
||||
intellectual property rights.
|
||||
|
||||
10.4 This ISL constitutes the complete agreement on its subject matter.
|
||||
Any modifications shall be agreed upon in written form. If any
|
||||
stipulation of this ISL should be held to be invalid or unenforceable,
|
||||
this does not affect the remaining stipulations. The affected parties
|
||||
will substitute an invalid or unenforceable stipulation with a provision
|
||||
that comes as close as possible to the intent and effect of the affected
|
||||
stipulation.
|
||||
|
||||
10.5 Irrespective the regulations on dispute resolution in intra-group
|
||||
agreements, the following applies: In case of conflict over
|
||||
applicability and/or interpretation of the ISL between more than one
|
||||
Mercedes-Benz Company, the FOSS Center of Competence, or any comparable
|
||||
successor thereof, shall be addressed by one or the parties jointly to
|
||||
resolve the conflict through an ultimate decision. In the event that
|
||||
such ultimate decision cannot be made, the parties adhere to the
|
||||
regulations on dispute resolution in intra-group agreements if
|
||||
concluded.
|
||||
|
||||
10.6 The ISL is governed by applicable German Law for domestic business
|
||||
under exclusion of the UN-purchase rules (CISG). Sole legal venue for
|
||||
any conflict arising in connection with the ISL is the appropriate civil
|
||||
courts in Stuttgart (Mitte), Germany; mandatory statutory jurisdiction
|
||||
remains unaffected.
|
||||
|
||||
10.7 Any and all Participants on the SCR are obliged to desist from all
|
||||
practices which may lead to penal liability due to fraud or
|
||||
embezzlement, insolvency crimes, crimes in violation of competition,
|
||||
guaranteeing advantages, bribery, acceptance of bribes or other
|
||||
corruption crimes on the part of persons employed by the respective
|
||||
Participant or other third parties. In the event of violation of the
|
||||
above, Mercedes-Benz or the right granting Mercedes-Benz Company has the right to
|
||||
immediately withdraw from or terminate all legal transactions existing
|
||||
with the respective Participant and the right to cancel all
|
||||
negotiations. The above notwithstanding, any Participant is obliged to
|
||||
adhere to all laws and regulations applicable to both itself and the
|
||||
commercial relationship with Mercedes-Benz Companies.
|
60
docs/MYP.dbml
Normal file
60
docs/MYP.dbml
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
Table "Printer" {
|
||||
"id" int [unique, pk]
|
||||
"name" text
|
||||
"description" text
|
||||
"status" int [note: '0: OPERATIONAL\n1: OUT_OF_ORDER']
|
||||
"created_at" timestamp
|
||||
"updated_at" timestamp
|
||||
}
|
||||
|
||||
Table "PrintJob" {
|
||||
"id" int [unique, pk]
|
||||
"printer_id" int
|
||||
"user_id" int
|
||||
"start_at" timestamp
|
||||
"duration_in_minutes" int
|
||||
"comments" text
|
||||
"aborted" bool [default: false]
|
||||
"abort_reason" text [note: 'Error code displayed on printer']
|
||||
"created_at" timestamp
|
||||
"updated_at" timestamp
|
||||
}
|
||||
|
||||
|
||||
Table "Account" {
|
||||
"id" text [unique, pk]
|
||||
"userId" text
|
||||
"type" text
|
||||
"provider" text
|
||||
"providerAccountId" text
|
||||
"refresh_token" text
|
||||
"access_token" text
|
||||
"expires_at" int
|
||||
"token_type" text
|
||||
"scope" text
|
||||
"id_token" text
|
||||
"session_state" text
|
||||
}
|
||||
|
||||
Table "Session" {
|
||||
"id" text [unique, pk]
|
||||
"sessionToken" text [unique]
|
||||
"userId" text
|
||||
"expires" datetime
|
||||
}
|
||||
|
||||
Table "User" {
|
||||
"id" text [pk]
|
||||
"name" text
|
||||
"email" text [unique]
|
||||
"role" text [note: 'ADMIN,USER,GUEST']
|
||||
}
|
||||
|
||||
Ref:"PrintJob"."user_id" < "User"."id"
|
||||
|
||||
Ref:"Account"."userId" < "User"."id"
|
||||
|
||||
Ref:"Session"."userId" < "User"."id"
|
||||
|
||||
Ref:"PrintJob"."printer_id" < "Printer"."id"
|
BIN
docs/MYP.png
Normal file
BIN
docs/MYP.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
57
docs/MYP.sql
Normal file
57
docs/MYP.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
CREATE TABLE `Printer` (
|
||||
`id` int UNIQUE PRIMARY KEY,
|
||||
`name` text,
|
||||
`description` text,
|
||||
`status` int COMMENT '0: OPERATIONAL
|
||||
1: OUT_OF_ORDER',
|
||||
`created_at` timestamp,
|
||||
`updated_at` timestamp
|
||||
);
|
||||
|
||||
CREATE TABLE `PrintJob` (
|
||||
`id` int UNIQUE PRIMARY KEY,
|
||||
`printer_id` int,
|
||||
`user_id` int,
|
||||
`start_at` timestamp,
|
||||
`duration_in_minutes` int,
|
||||
`comments` text,
|
||||
`aborted` boolean DEFAULT false,
|
||||
`abort_reason` text COMMENT 'Error code displayed on printer'
|
||||
);
|
||||
|
||||
CREATE TABLE `Account` (
|
||||
`id` text UNIQUE PRIMARY KEY,
|
||||
`userId` text,
|
||||
`type` text,
|
||||
`provider` text,
|
||||
`providerAccountId` text,
|
||||
`refresh_token` text,
|
||||
`access_token` text,
|
||||
`expires_at` int,
|
||||
`token_type` text,
|
||||
`scope` text,
|
||||
`id_token` text,
|
||||
`session_state` text
|
||||
);
|
||||
|
||||
CREATE TABLE `Session` (
|
||||
`id` text UNIQUE PRIMARY KEY,
|
||||
`sessionToken` text UNIQUE,
|
||||
`userId` text,
|
||||
`expires` datetime
|
||||
);
|
||||
|
||||
CREATE TABLE `User` (
|
||||
`id` text PRIMARY KEY,
|
||||
`name` text,
|
||||
`email` text UNIQUE,
|
||||
`role` text COMMENT 'ADMIN,USER,GUEST'
|
||||
);
|
||||
|
||||
ALTER TABLE `User` ADD FOREIGN KEY (`id`) REFERENCES `PrintJob` (`user_id`);
|
||||
|
||||
ALTER TABLE `User` ADD FOREIGN KEY (`id`) REFERENCES `Account` (`userId`);
|
||||
|
||||
ALTER TABLE `User` ADD FOREIGN KEY (`id`) REFERENCES `Session` (`userId`);
|
||||
|
||||
ALTER TABLE `Printer` ADD FOREIGN KEY (`id`) REFERENCES `PrintJob` (`printer_id`);
|
22
frontend-aenderungen.md
Normal file
22
frontend-aenderungen.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Notwendige Frontend-Änderungen
|
||||
|
||||
1. Frontend-Authentifizierung anpassen:
|
||||
- GitHub OAuth durch lokale Authentifizierung ersetzen
|
||||
- Login-Komponenten für Benutzername/Passwort erstellen
|
||||
- Registrierungs-Formular implementieren
|
||||
- API-Routen für Login- und Registrierungsprozess anpassen
|
||||
|
||||
2. Datenbankschema:
|
||||
- Users-Tabelle anpassen um Passwort-Hash zu unterstützen
|
||||
- GitHub-ID entfernen oder optional machen
|
||||
|
||||
3. Auth-System:
|
||||
- Lucia.js: Anpassung von OAuth auf Formular-basierte Authentifizierung
|
||||
- Session-Management beibehalten
|
||||
|
||||
4. API-Endpunktanpassungen:
|
||||
- Neue Login und Register-Endpunkte erstellen
|
||||
- Route für initialen Admin-Setup
|
||||
|
||||
Die Änderungen im Frontend sind umfangreicher, da das aktuelle System stark auf GitHub OAuth ausgerichtet ist und komplett umgestellt werden muss.
|
||||
|
27
packages/reservation-platform/.dockerignore
Normal file
27
packages/reservation-platform/.dockerignore
Normal file
@@ -0,0 +1,27 @@
|
||||
# Build and utility assets
|
||||
docker/
|
||||
scripts/
|
||||
|
||||
# Ignore node_modules as they will be installed in the container
|
||||
node_modules
|
||||
|
||||
# Ignore build artifacts
|
||||
.next
|
||||
|
||||
# Ignore runtime data
|
||||
db/
|
||||
|
||||
# Ignore local configuration files
|
||||
.env
|
||||
.env.example
|
||||
|
||||
# Ignore version control files
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Ignore IDE/editor specific files
|
||||
*.log
|
||||
*.tmp
|
||||
*.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
3
packages/reservation-platform/.env.example
Normal file
3
packages/reservation-platform/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
# OAuth Configuration
|
||||
OAUTH_CLIENT_ID=client_id
|
||||
OAUTH_CLIENT_SECRET=client_secret
|
43
packages/reservation-platform/.gitignore
vendored
Normal file
43
packages/reservation-platform/.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# db folder
|
||||
db/
|
||||
|
||||
# Env file
|
||||
.env
|
||||
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
34
packages/reservation-platform/Dockerfile
Normal file
34
packages/reservation-platform/Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM node:20-bookworm-slim
|
||||
|
||||
# Create application directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
# Set environment variables
|
||||
ENV PORT=3000
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy package.json and pnpm-lock.yaml
|
||||
COPY package.json /usr/src/app
|
||||
COPY pnpm-lock.yaml /usr/src/app
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable pnpm
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Initialize Database, if it not already exists
|
||||
RUN pnpm run db
|
||||
|
||||
# Build the application
|
||||
RUN pnpm run build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD ["/bin/sh", "-c", "if [ ! -f ./db/sqlite.db ]; then pnpm db; fi && pnpm start"]
|
32
packages/reservation-platform/README.md
Normal file
32
packages/reservation-platform/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# MYP - Manage Your Printer
|
||||
|
||||
MYP (Manage Your Printer) ist eine Webanwendung zur Reservierung von 3D-Druckern.
|
||||
Sie wurde im Rahmen des Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
- Netzwerk auf Raspberry Pi ist eingerichtet
|
||||
- Docker ist installiert
|
||||
|
||||
### Schritte
|
||||
|
||||
1. Docker-Container bauen (docker/build.sh)
|
||||
2. Docker-Container speichern (docker/save.sh caddy:2.8 myp-rp:latest)
|
||||
3. Docker-Container auf Raspberry Pi bereitstellen (docker/deploy.sh)
|
||||
|
||||
## Entwicklerinformationen
|
||||
|
||||
### Raspberry Pi Einstellungen
|
||||
|
||||
Auf dem Raspberry Pi wurde Raspbian Lite installiert.
|
||||
Unter /srv/* sind die Projektdateien zu finden.
|
||||
|
||||
### Anmeldedaten
|
||||
|
||||
```
|
||||
Benutzer: myp
|
||||
Passwort: (persönlich bekannt)
|
||||
```
|
||||
|
19
packages/reservation-platform/biome.json
Normal file
19
packages/reservation-platform/biome.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"lineWidth": 120
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"noUnusedImports": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
packages/reservation-platform/components.json
Normal file
17
packages/reservation-platform/components.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/utils/styles"
|
||||
}
|
||||
}
|
31
packages/reservation-platform/docker/build.sh
Normal file
31
packages/reservation-platform/docker/build.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Define image name
|
||||
MYP_RP_IMAGE_NAME="myp-rp"
|
||||
|
||||
# Function to build Docker image
|
||||
build_image() {
|
||||
local image_name=$1
|
||||
local dockerfile=$2
|
||||
local platform=$3
|
||||
|
||||
echo "Building $image_name Docker image for $platform..."
|
||||
|
||||
docker buildx build --platform $platform -t ${image_name}:latest -f $dockerfile --load .
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "$image_name Docker image built successfully"
|
||||
else
|
||||
echo "Error occurred while building $image_name Docker image"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create and use a builder instance (if not already created)
|
||||
BUILDER_NAME="myp-rp-arm64-builder"
|
||||
docker buildx create --name $BUILDER_NAME --use || docker buildx use $BUILDER_NAME
|
||||
|
||||
# Build myp-rp image
|
||||
build_image "$MYP_RP_IMAGE_NAME" "$PWD/Dockerfile" "linux/arm64"
|
||||
|
||||
# Remove the builder instance
|
||||
docker buildx rm $BUILDER_NAME
|
8
packages/reservation-platform/docker/caddy/Caddyfile
Normal file
8
packages/reservation-platform/docker/caddy/Caddyfile
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
debug
|
||||
}
|
||||
|
||||
m040tbaraspi001.de040.corpintra.net, m040tbaraspi001.de040.corpinter.net {
|
||||
reverse_proxy myp-rp:3000
|
||||
tls internal
|
||||
}
|
19
packages/reservation-platform/docker/compose.yml
Normal file
19
packages/reservation-platform/docker/compose.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
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
|
||||
env_file: "/srv/myp-env/github.env"
|
||||
volumes:
|
||||
- /srv/MYP-DB:/usr/src/app/db
|
||||
restart: unless-stopped
|
36
packages/reservation-platform/docker/deploy.sh
Normal file
36
packages/reservation-platform/docker/deploy.sh
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Directory containing the Docker images
|
||||
IMAGE_DIR="docker/images"
|
||||
|
||||
# Load all Docker images from the tar.xz files in the IMAGE_DIR
|
||||
echo "Loading Docker images from $IMAGE_DIR..."
|
||||
|
||||
for image_file in "$IMAGE_DIR"/*.tar.xz; do
|
||||
if [ -f "$image_file" ]; then
|
||||
echo "Loading Docker image from $image_file..."
|
||||
docker load -i "$image_file"
|
||||
|
||||
# Check if the image loading was successful
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error occurred while loading Docker image from $image_file"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No Docker image tar.xz files found in $IMAGE_DIR."
|
||||
fi
|
||||
done
|
||||
|
||||
# Execute docker compose
|
||||
echo "Running docker compose..."
|
||||
docker compose -f "docker/compose.yml" up -d
|
||||
|
||||
# Check if the operation was successful
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Docker compose executed successfully"
|
||||
else
|
||||
echo "Error occurred while executing docker compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deployment completed successfully"
|
2
packages/reservation-platform/docker/images/.gitattributes
vendored
Normal file
2
packages/reservation-platform/docker/images/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
caddy_2.8.tar.xz filter=lfs diff=lfs merge=lfs -text
|
||||
myp-rp_latest.tar.xz filter=lfs diff=lfs merge=lfs -text
|
BIN
packages/reservation-platform/docker/images/caddy_2.8.tar.xz
(Stored with Git LFS)
Normal file
BIN
packages/reservation-platform/docker/images/caddy_2.8.tar.xz
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
packages/reservation-platform/docker/images/myp-rp_latest.tar.xz
(Stored with Git LFS)
Normal file
BIN
packages/reservation-platform/docker/images/myp-rp_latest.tar.xz
(Stored with Git LFS)
Normal file
Binary file not shown.
68
packages/reservation-platform/docker/save.sh
Normal file
68
packages/reservation-platform/docker/save.sh
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get image name as argument
|
||||
IMAGE_NAME=$1
|
||||
PLATFORM="linux/arm64"
|
||||
|
||||
# Define paths
|
||||
IMAGE_DIR="docker/images"
|
||||
IMAGE_FILE="${IMAGE_DIR}/${IMAGE_NAME//[:\/]/_}.tar"
|
||||
COMPRESSED_FILE="${IMAGE_FILE}.xz"
|
||||
|
||||
# Function to pull the image
|
||||
pull_image() {
|
||||
local image=$1
|
||||
if [[ $image == arm64v8/* ]]; then
|
||||
echo "Pulling image $image without platform specification..."
|
||||
docker pull $image
|
||||
else
|
||||
echo "Pulling image $image for platform $PLATFORM..."
|
||||
docker pull --platform $PLATFORM $image
|
||||
fi
|
||||
return $?
|
||||
}
|
||||
|
||||
# Pull the image if it is not available locally
|
||||
if ! docker image inspect ${IMAGE_NAME} &>/dev/null; then
|
||||
if pull_image ${IMAGE_NAME}; then
|
||||
echo "Image $IMAGE_NAME pulled successfully."
|
||||
else
|
||||
echo "Error occurred while pulling $IMAGE_NAME for platform $PLATFORM"
|
||||
echo "Trying to pull $IMAGE_NAME without platform specification..."
|
||||
|
||||
# Attempt to pull again without platform
|
||||
if pull_image ${IMAGE_NAME}; then
|
||||
echo "Image $IMAGE_NAME pulled successfully without platform."
|
||||
else
|
||||
echo "Error occurred while pulling $IMAGE_NAME without platform."
|
||||
echo "Trying to pull arm64v8/${IMAGE_NAME} instead..."
|
||||
|
||||
# Construct new image name
|
||||
NEW_IMAGE_NAME="arm64v8/${IMAGE_NAME}"
|
||||
if pull_image ${NEW_IMAGE_NAME}; then
|
||||
echo "Image $NEW_IMAGE_NAME pulled successfully."
|
||||
IMAGE_NAME=${NEW_IMAGE_NAME} # Update IMAGE_NAME to use the new one
|
||||
else
|
||||
echo "Error occurred while pulling $NEW_IMAGE_NAME"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Image $IMAGE_NAME found locally. Skipping pull."
|
||||
fi
|
||||
|
||||
# Save the Docker image
|
||||
echo "Saving $IMAGE_NAME Docker image..."
|
||||
docker save ${IMAGE_NAME} > $IMAGE_FILE
|
||||
|
||||
# Compress the Docker image (overwriting if file exists)
|
||||
echo "Compressing $IMAGE_FILE..."
|
||||
xz -z --force $IMAGE_FILE
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "$IMAGE_NAME Docker image saved and compressed successfully as $COMPRESSED_FILE"
|
||||
else
|
||||
echo "Error occurred while compressing $IMAGE_NAME Docker image"
|
||||
exit 1
|
||||
fi
|
116
packages/reservation-platform/docs/Admin-Dashboard.md
Normal file
116
packages/reservation-platform/docs/Admin-Dashboard.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# **Detaillierte Dokumentation des Admin-Dashboards**
|
||||
|
||||
In diesem Abschnitt werde ich die Funktionen und Nutzung des Admin-Dashboards genauer beschreiben, einschließlich der verschiedenen Module, Diagramme und deren Zweck.
|
||||
|
||||
---
|
||||
|
||||
## **1. Überblick über das Admin-Dashboard**
|
||||
|
||||
Das Admin-Dashboard ist der zentrale Verwaltungsbereich für Administratoren. Es bietet Funktionen wie die Verwaltung von Druckern, Benutzern und Druckaufträgen sowie detaillierte Statistiken und Analysen.
|
||||
|
||||
### **1.1. Navigation**
|
||||
Das Dashboard enthält ein Sidebar-Menü mit den folgenden Hauptbereichen:
|
||||
1. **Dashboard:** Übersicht der wichtigsten Statistiken.
|
||||
2. **Benutzer:** Verwaltung von Benutzerkonten.
|
||||
3. **Drucker:** Hinzufügen, Bearbeiten und Verwalten von Druckern.
|
||||
4. **Druckaufträge:** Einsicht in alle Druckaufträge und deren Status.
|
||||
5. **Einstellungen:** Konfiguration der Anwendung.
|
||||
6. **Über MYP:** Informationen über das Projekt und den Entwickler.
|
||||
|
||||
Die Sidebar wird in der Datei `src/app/admin/admin-sidebar.tsx` definiert und dynamisch basierend auf der aktuellen Seite hervorgehoben.
|
||||
|
||||
---
|
||||
|
||||
## **2. Funktionen des Admin-Dashboards**
|
||||
|
||||
### **2.1. Benutzerverwaltung**
|
||||
- **Datei:** `src/app/admin/users/page.tsx`
|
||||
- **Beschreibung:** Ermöglicht das Anzeigen, Bearbeiten und Löschen von Benutzerkonten.
|
||||
- **Funktionen:**
|
||||
- Anzeige einer Liste aller registrierten Benutzer.
|
||||
- Bearbeiten von Benutzerrollen (z. B. „admin“ oder „user“).
|
||||
- Deaktivieren oder Löschen von Benutzerkonten.
|
||||
|
||||
---
|
||||
|
||||
### **2.2. Druckerverwaltung**
|
||||
- **Datei:** `src/app/admin/printers/page.tsx`
|
||||
- **Beschreibung:** Verwaltung der Drucker, einschließlich Hinzufügen, Bearbeiten und Deaktivieren.
|
||||
- **Funktionen:**
|
||||
- Statusanzeige der Drucker (aktiv/inaktiv).
|
||||
- Hinzufügen neuer Drucker mit Namen und Beschreibung.
|
||||
- Löschen oder Bearbeiten bestehender Drucker.
|
||||
|
||||
---
|
||||
|
||||
### **2.3. Druckaufträge**
|
||||
- **Datei:** `src/app/admin/jobs/page.tsx`
|
||||
- **Beschreibung:** Übersicht aller Druckaufträge, einschließlich Details wie Startzeit, Dauer und Status.
|
||||
- **Funktionen:**
|
||||
- Filtern nach Benutzern, Druckern oder Status (abgeschlossen, abgebrochen).
|
||||
- Anzeigen von Abbruchgründen und Fehlermeldungen.
|
||||
- Sortieren nach Zeit oder Benutzer.
|
||||
|
||||
---
|
||||
|
||||
### **2.4. Einstellungen**
|
||||
- **Datei:** `src/app/admin/settings/page.tsx`
|
||||
- **Beschreibung:** Konfigurationsseite für die Anwendung.
|
||||
- **Funktionen:**
|
||||
- Ändern von globalen Einstellungen wie Standardzeiten oder Fehlerrichtlinien.
|
||||
- Download von Daten (z. B. Export der Druckhistorie).
|
||||
|
||||
---
|
||||
|
||||
## **3. Statistiken und Diagramme**
|
||||
|
||||
Das Admin-Dashboard enthält interaktive Diagramme, die wichtige Statistiken visualisieren. Hier einige der zentralen Diagramme:
|
||||
|
||||
### **3.1. Abbruchgründe**
|
||||
- **Datei:** `src/app/admin/charts/printer-error-chart.tsx`
|
||||
- **Beschreibung:** Zeigt die Häufigkeit der Abbruchgründe für Druckaufträge in einem Balkendiagramm.
|
||||
- **Nutzen:** Identifiziert häufige Probleme wie Materialmangel oder Düsenverstopfungen.
|
||||
|
||||
---
|
||||
|
||||
### **3.2. Fehlerraten**
|
||||
- **Datei:** `src/app/admin/charts/printer-error-rate.tsx`
|
||||
- **Beschreibung:** Zeigt die prozentuale Fehlerrate für jeden Drucker in einem Balkendiagramm.
|
||||
- **Nutzen:** Ermöglicht die Überwachung und Identifizierung von problematischen Druckern.
|
||||
|
||||
---
|
||||
|
||||
### **3.3. Druckvolumen**
|
||||
- **Datei:** `src/app/admin/charts/printer-volume.tsx`
|
||||
- **Beschreibung:** Zeigt das Druckvolumen für heute, diese Woche und diesen Monat.
|
||||
- **Nutzen:** Vergleich des Druckeroutputs über verschiedene Zeiträume.
|
||||
|
||||
---
|
||||
|
||||
### **3.4. Prognostizierte Nutzung**
|
||||
- **Datei:** `src/app/admin/charts/printer-forecast.tsx`
|
||||
- **Beschreibung:** Ein Bereichsdiagramm zeigt die erwartete Druckernutzung pro Wochentag.
|
||||
- **Nutzen:** Hilft bei der Planung von Wartungsarbeiten oder Ressourcenzuweisungen.
|
||||
|
||||
---
|
||||
|
||||
### **3.5. Druckerauslastung**
|
||||
- **Datei:** `src/app/admin/charts/printer-utilization.tsx`
|
||||
- **Beschreibung:** Zeigt die aktuelle Nutzung eines Druckers in Prozent in einem Kreisdiagramm.
|
||||
- **Nutzen:** Überwacht die Auslastung und identifiziert ungenutzte Ressourcen.
|
||||
|
||||
---
|
||||
|
||||
## **4. Rollenbasierte Zugriffssteuerung**
|
||||
|
||||
Das Admin-Dashboard ist nur für Benutzer mit der Rolle „admin“ zugänglich. Nicht berechtigte Benutzer werden auf die Startseite umgeleitet. Die Zugriffssteuerung erfolgt durch folgende Logik:
|
||||
- **Datei:** `src/app/admin/layout.tsx`
|
||||
- **Funktion:** `validateRequest` prüft die Rolle des aktuellen Benutzers.
|
||||
- **Umleitung:** Falls die Rolle unzureichend ist, wird der Benutzer automatisch umgeleitet:
|
||||
```typescript
|
||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||
redirect("/");
|
||||
}
|
||||
```
|
||||
|
||||
Nächster Schritt: [=> API-Endpunkte und deren Nutzung](./API.md)
|
79
packages/reservation-platform/docs/Architektur.md
Normal file
79
packages/reservation-platform/docs/Architektur.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# **Technische Architektur und Codeaufbau**
|
||||
|
||||
In diesem Abschnitt erläutere ich die Architektur und Struktur des MYP-Projekts sowie die Funktionalitäten der zentralen Komponenten.
|
||||
|
||||
---
|
||||
|
||||
## **1. Technische Architektur**
|
||||
|
||||
### **1.1. Architekturübersicht**
|
||||
MYP basiert auf einer modernen Webanwendungsarchitektur:
|
||||
- **Frontend:** Entwickelt mit React und Next.js. Stellt die Benutzeroberfläche bereit.
|
||||
- **Backend:** Nutzt Node.js und Drizzle ORM für die Datenbankinteraktion und Geschäftslogik.
|
||||
- **Datenbank:** SQLite zur Speicherung von Nutzerdaten, Druckaufträgen und Druckerkonfigurationen.
|
||||
- **Containerisierung:** Docker wird verwendet, um die Anwendung in isolierten Containern bereitzustellen.
|
||||
- **Webserver:** Caddy dient als Reverse Proxy mit HTTPS-Unterstützung.
|
||||
|
||||
### **1.2. Modulübersicht**
|
||||
- **Datenfluss:** Die Anwendung ist stark datengetrieben. API-Routen werden genutzt, um Daten zwischen Frontend und Backend auszutauschen.
|
||||
- **Rollenbasierter Zugriff:** Über ein Berechtigungssystem können Administratoren und Benutzer unterschiedliche Funktionen nutzen.
|
||||
|
||||
---
|
||||
|
||||
## **2. Codeaufbau**
|
||||
|
||||
### **2.1. Ordnerstruktur**
|
||||
Die Datei `repomix-output.txt` zeigt eine strukturierte Übersicht des Projekts. Nachfolgend einige wichtige Verzeichnisse:
|
||||
|
||||
| **Verzeichnis** | **Inhalt** |
|
||||
|--------------------------|---------------------------------------------------------------------------|
|
||||
| `src/app` | Next.js-Seiten und Komponenten für Benutzer und Admins. |
|
||||
| `src/components` | Wiederverwendbare UI-Komponenten wie Karten, Diagramme, Buttons etc. |
|
||||
| `src/server` | Backend-Logik, Authentifizierung und Datenbankinteraktionen. |
|
||||
| `src/utils` | Hilfsfunktionen für Analysen, Validierungen und Datenbankzugriffe. |
|
||||
| `drizzle` | Datenbank-Migrationsdateien und Metadaten. |
|
||||
| `docker` | Docker-Konfigurations- und Bereitstellungsskripte. |
|
||||
|
||||
---
|
||||
|
||||
### **2.2. Hauptdateien**
|
||||
#### **Frontend**
|
||||
- **`src/app/page.tsx`:** Startseite der Anwendung.
|
||||
- **`src/app/admin/`:** Admin-spezifische Seiten, z. B. Druckerverwaltung oder Fehlerstatistiken.
|
||||
- **`src/components/ui/`:** UI-Komponenten wie Dialoge, Formulare und Tabellen.
|
||||
|
||||
#### **Backend**
|
||||
- **`src/server/auth/`:** Authentifizierung und Benutzerrollenmanagement.
|
||||
- **`src/server/actions/`:** Funktionen zur Interaktion mit Druckaufträgen und Druckern.
|
||||
- **`src/utils/`:** Analyse und Verarbeitung von Druckdaten (z. B. Fehlerquoten und Auslastung).
|
||||
|
||||
#### **Datenbank**
|
||||
- **`drizzle/0000_overjoyed_strong_guy.sql`:** SQLite-Datenbankschema mit Tabellen für Drucker, Benutzer und Druckaufträge.
|
||||
- **`drizzle.meta/`:** Metadaten zur Datenbankmigration.
|
||||
|
||||
---
|
||||
|
||||
### **2.3. Datenbankschema**
|
||||
Das Schema enthält vier Haupttabellen:
|
||||
1. **`user`:** Speichert Benutzerinformationen, einschließlich Rollen und E-Mail-Adressen.
|
||||
2. **`printer`:** Beschreibt die Drucker, ihren Status und ihre Eigenschaften.
|
||||
3. **`printJob`:** Zeichnet Druckaufträge auf, einschließlich Startzeit, Dauer und Abbruchgrund.
|
||||
4. **`session`:** Verwaltert Benutzer-Sitzungen und Ablaufzeiten.
|
||||
|
||||
---
|
||||
|
||||
## **3. Wichtige Funktionen**
|
||||
|
||||
### **3.1. Authentifizierung**
|
||||
Das System nutzt OAuth zur Anmeldung. Benutzerrollen werden in der Tabelle `user` gespeichert und im Backend überprüft.
|
||||
|
||||
### **3.2. Statistiken**
|
||||
- **Fehlerrate:** Berechnet die Häufigkeit von Abbrüchen für jeden Drucker.
|
||||
- **Auslastung:** Prozentuale Nutzung der Drucker, basierend auf geplanten und abgeschlossenen Druckaufträgen.
|
||||
- **Prognosen:** Verwenden historische Daten, um zukünftige Drucknutzungen vorherzusagen.
|
||||
|
||||
### **3.3. API-Endpunkte**
|
||||
- **`src/app/api/printers/`:** Zugriff auf Druckerkonfigurationsdaten.
|
||||
- **`src/app/api/job/[jobId]/`:** Verwaltung einzelner Druckaufträge.
|
||||
|
||||
Nächster Schritt: [=> Datenbank und Analytik-Funktionen](./Datenbank.md)
|
150
packages/reservation-platform/docs/Bereitstellungsdetails .md
Normal file
150
packages/reservation-platform/docs/Bereitstellungsdetails .md
Normal file
@@ -0,0 +1,150 @@
|
||||
# **Bereitstellungsdetails und Best Practices**
|
||||
|
||||
In diesem Abschnitt erläutere ich, wie das MYP-Projekt auf einem Server bereitgestellt wird, sowie empfohlene Praktiken zur Verwaltung und Optimierung des Systems.
|
||||
|
||||
---
|
||||
|
||||
## **1. Bereitstellungsschritte**
|
||||
|
||||
### **1.1. Voraussetzungen**
|
||||
- **Server:** Raspberry Pi mit installiertem Raspbian Lite.
|
||||
- **Docker:** Docker und Docker Compose müssen vorab installiert sein.
|
||||
- **Netzwerk:** Der Server muss über eine statische IP-Adresse oder einen DNS-Namen erreichbar sein.
|
||||
|
||||
### **1.2. Vorbereitung**
|
||||
#### **1.2.1. Docker-Images erstellen und speichern**
|
||||
Führen Sie die folgenden Schritte auf dem Entwicklungssystem aus:
|
||||
1. **Images erstellen:**
|
||||
```bash
|
||||
bash docker/build.sh
|
||||
```
|
||||
2. **Images exportieren und komprimieren:**
|
||||
```bash
|
||||
bash docker/save.sh <image-name>
|
||||
```
|
||||
Dies speichert die Docker-Images im Verzeichnis `docker/images/`.
|
||||
|
||||
#### **1.2.2. Übertragung auf den Server**
|
||||
Kopieren Sie die erzeugten `.tar.xz`-Dateien auf den Raspberry Pi:
|
||||
```bash
|
||||
scp docker/images/*.tar.xz <username>@<server-ip>:/path/to/destination/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **1.3. Images auf dem Server laden**
|
||||
Loggen Sie sich auf dem Server ein und laden Sie die Docker-Images:
|
||||
```bash
|
||||
docker load -i /path/to/destination/<image-name>.tar.xz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **1.4. Starten der Anwendung**
|
||||
Führen Sie das Bereitstellungsskript aus:
|
||||
```bash
|
||||
bash docker/deploy.sh
|
||||
```
|
||||
Dieses Skript:
|
||||
- Startet die Docker-Container mithilfe von `docker compose`.
|
||||
- Verbindet den Reverse Proxy (Caddy) mit der Anwendung.
|
||||
|
||||
Die Anwendung sollte unter `http://<server-ip>` oder der konfigurierten Domain erreichbar sein.
|
||||
|
||||
---
|
||||
|
||||
## **2. Best Practices**
|
||||
|
||||
### **2.1. Sicherheit**
|
||||
1. **Umgebungsvariablen schützen:**
|
||||
- Stellen Sie sicher, dass die Datei `.env` nicht versehentlich in ein öffentliches Repository hochgeladen wird.
|
||||
- Verwenden Sie geeignete Zugriffsrechte:
|
||||
```bash
|
||||
chmod 600 .env
|
||||
```
|
||||
2. **HTTPS aktivieren:**
|
||||
- Der Caddy-Webserver unterstützt automatisch HTTPS. Stellen Sie sicher, dass eine gültige Domain konfiguriert ist.
|
||||
|
||||
3. **Zugriffsrechte beschränken:**
|
||||
- Verwenden Sie Benutzerrollen („admin“, „guest“), um den Zugriff auf kritische Funktionen zu steuern.
|
||||
|
||||
---
|
||||
|
||||
### **2.2. Performance**
|
||||
1. **Docker-Container optimieren:**
|
||||
- Reduzieren Sie die Größe der Docker-Images, indem Sie unnötige Dateien in `.dockerignore` ausschließen.
|
||||
|
||||
2. **Datenbankwartung:**
|
||||
- Führen Sie regelmäßige Backups der SQLite-Datenbank durch:
|
||||
```bash
|
||||
cp db/sqlite.db /path/to/backup/location/
|
||||
```
|
||||
- Optimieren Sie die Datenbank regelmäßig:
|
||||
```sql
|
||||
VACUUM;
|
||||
```
|
||||
|
||||
3. **Skalierung:**
|
||||
- Bei hoher Last kann die Anwendung mit Kubernetes oder einer Cloud-Lösung (z. B. AWS oder Azure) skaliert werden.
|
||||
|
||||
---
|
||||
|
||||
### **2.3. Fehlerbehebung**
|
||||
1. **Logs überprüfen:**
|
||||
- Docker-Logs können wichtige Debug-Informationen liefern:
|
||||
```bash
|
||||
docker logs <container-name>
|
||||
```
|
||||
|
||||
2. **Health Checks:**
|
||||
- Integrieren Sie Health Checks in die Docker Compose-Datei, um sicherzustellen, dass die Dienste korrekt laufen.
|
||||
|
||||
3. **Fehlerhafte Drucker deaktivieren:**
|
||||
- Deaktivieren Sie Drucker mit einer hohen Fehlerrate über das Admin-Dashboard, um die Benutzererfahrung zu verbessern.
|
||||
|
||||
---
|
||||
|
||||
### **2.4. Updates**
|
||||
1. **Neue Funktionen hinzufügen:**
|
||||
- Aktualisieren Sie die Anwendung und erstellen Sie neue Docker-Images:
|
||||
```bash
|
||||
git pull origin main
|
||||
bash docker/build.sh
|
||||
```
|
||||
- Stellen Sie die aktualisierten Images bereit:
|
||||
```bash
|
||||
bash docker/deploy.sh
|
||||
```
|
||||
|
||||
2. **Datenbankmigrationen:**
|
||||
- Führen Sie neue Migrationsskripte mit folgendem Befehl aus:
|
||||
```bash
|
||||
pnpm run db:migrate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **3. Backup und Wiederherstellung**
|
||||
|
||||
### **3.1. Backups erstellen**
|
||||
Sichern Sie wichtige Dateien und Datenbanken regelmäßig:
|
||||
- **SQLite-Datenbank:**
|
||||
```bash
|
||||
cp db/sqlite.db /backup/location/sqlite-$(date +%F).db
|
||||
```
|
||||
- **Docker-Images:**
|
||||
```bash
|
||||
docker save myp-rp:latest | gzip > /backup/location/myp-rp-$(date +%F).tar.gz
|
||||
```
|
||||
|
||||
### **3.2. Wiederherstellung**
|
||||
- **Datenbank wiederherstellen:**
|
||||
```bash
|
||||
cp /backup/location/sqlite-<date>.db db/sqlite.db
|
||||
```
|
||||
- **Docker-Images importieren:**
|
||||
```bash
|
||||
docker load < /backup/location/myp-rp-<date>.tar.gz
|
||||
```
|
||||
|
||||
Nächster Schritt: [=> Admin-Dashboard](./Admin-Dashboard.md)
|
153
packages/reservation-platform/docs/Datenbank.md
Normal file
153
packages/reservation-platform/docs/Datenbank.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# **Datenbank und Analytik-Funktionen**
|
||||
|
||||
Dieser Abschnitt konzentriert sich auf die Struktur der Datenbank sowie die Analyse- und Prognosefunktionen, die im Projekt verwendet werden.
|
||||
|
||||
---
|
||||
|
||||
## **1. Datenbankstruktur**
|
||||
|
||||
Das Datenbankschema wurde mit **Drizzle ORM** definiert und basiert auf SQLite. Die wichtigsten Tabellen und ihre Zwecke sind:
|
||||
|
||||
### **1.1. Tabellenübersicht**
|
||||
|
||||
#### **`user`**
|
||||
- Speichert Benutzerinformationen.
|
||||
- Enthält Rollen wie „admin“ oder „guest“ zur Verwaltung von Berechtigungen.
|
||||
|
||||
| **Feld** | **Typ** | **Beschreibung** |
|
||||
|-------------------|------------|-------------------------------------------|
|
||||
| `id` | `text` | Eindeutige ID des Benutzers. |
|
||||
| `github_id` | `integer` | ID des Benutzers aus dem OAuth-Dienst. |
|
||||
| `name` | `text` | Benutzername. |
|
||||
| `displayName` | `text` | Angezeigter Name. |
|
||||
| `email` | `text` | E-Mail-Adresse. |
|
||||
| `role` | `text` | Benutzerrolle, Standardwert: „guest“. |
|
||||
|
||||
---
|
||||
|
||||
#### **`printer`**
|
||||
- Beschreibt verfügbare Drucker und deren Status.
|
||||
|
||||
| **Feld** | **Typ** | **Beschreibung** |
|
||||
|-------------------|------------|-------------------------------------------|
|
||||
| `id` | `text` | Eindeutige Drucker-ID. |
|
||||
| `name` | `text` | Name des Druckers. |
|
||||
| `description` | `text` | Beschreibung oder Spezifikationen. |
|
||||
| `status` | `integer` | Betriebsstatus (0 = inaktiv, 1 = aktiv). |
|
||||
|
||||
---
|
||||
|
||||
#### **`printJob`**
|
||||
- Speichert Informationen zu Druckaufträgen.
|
||||
|
||||
| **Feld** | **Typ** | **Beschreibung** |
|
||||
|-----------------------|---------------|-------------------------------------------------------|
|
||||
| `id` | `text` | Eindeutige Auftrags-ID. |
|
||||
| `printerId` | `text` | Verweis auf die ID des Druckers. |
|
||||
| `userId` | `text` | Verweis auf die ID des Benutzers. |
|
||||
| `startAt` | `integer` | Startzeit des Druckauftrags (Unix-Timestamp). |
|
||||
| `durationInMinutes` | `integer` | Dauer des Druckauftrags in Minuten. |
|
||||
| `comments` | `text` | Zusätzliche Kommentare. |
|
||||
| `aborted` | `integer` | 1 = Abgebrochen, 0 = Erfolgreich abgeschlossen. |
|
||||
| `abortReason` | `text` | Grund für den Abbruch (falls zutreffend). |
|
||||
|
||||
---
|
||||
|
||||
#### **`session`**
|
||||
- Verwaltert Benutzer-Sitzungen und Ablaufzeiten.
|
||||
|
||||
| **Feld** | **Typ** | **Beschreibung** |
|
||||
|-------------------|------------|-------------------------------------------|
|
||||
| `id` | `text` | Eindeutige Sitzungs-ID. |
|
||||
| `user_id` | `text` | Verweis auf die ID des Benutzers. |
|
||||
| `expires_at` | `integer` | Zeitpunkt, wann die Sitzung abläuft. |
|
||||
|
||||
---
|
||||
|
||||
### **1.2. Relationen**
|
||||
- `printer` → `printJob`: Druckaufträge sind an spezifische Drucker gebunden.
|
||||
- `user` → `printJob`: Druckaufträge werden Benutzern zugewiesen.
|
||||
- `user` → `session`: Sitzungen verknüpfen Benutzer mit Login-Details.
|
||||
|
||||
---
|
||||
|
||||
## **2. Analytik-Funktionen**
|
||||
|
||||
Das Projekt bietet verschiedene Analytik- und Prognosetools, um die Druckernutzung und Fehler zu überwachen.
|
||||
|
||||
### **2.1. Fehlerratenanalyse**
|
||||
- Funktion: `calculatePrinterErrorRate` (in `src/utils/analytics/error-rate.ts`).
|
||||
- Berechnet die prozentuale Fehlerrate für jeden Drucker basierend auf abgebrochenen Aufträgen.
|
||||
|
||||
Beispielausgabe:
|
||||
```json
|
||||
[
|
||||
{ "name": "Drucker 1", "errorRate": 5.2 },
|
||||
{ "name": "Drucker 2", "errorRate": 3.7 }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **2.2. Abbruchgründe**
|
||||
- Funktion: `calculateAbortReasonsCount` (in `src/utils/analytics/errors.ts`).
|
||||
- Zählt die Häufigkeit der Abbruchgründe aus der Tabelle `printJob`.
|
||||
|
||||
Beispielausgabe:
|
||||
```json
|
||||
[
|
||||
{ "abortReason": "Materialmangel", "count": 10 },
|
||||
{ "abortReason": "Düsenverstopfung", "count": 7 }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **2.3. Nutzung und Prognosen**
|
||||
#### Nutzung:
|
||||
- Funktion: `calculatePrinterUtilization` (in `src/utils/analytics/utilization.ts`).
|
||||
- Berechnet die Nutzung der Drucker in Prozent.
|
||||
|
||||
Beispielausgabe:
|
||||
```json
|
||||
{ "printerId": "1", "utilizationPercentage": 85 }
|
||||
```
|
||||
|
||||
#### Prognosen:
|
||||
- Funktion: `forecastPrinterUsage` (in `src/utils/analytics/forecast.ts`).
|
||||
- Nutzt historische Daten, um die erwartete Druckernutzung für kommende Tage/Wochen zu schätzen.
|
||||
|
||||
Beispielausgabe:
|
||||
```json
|
||||
[
|
||||
{ "day": 1, "usageMinutes": 300 },
|
||||
{ "day": 2, "usageMinutes": 200 }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **2.4. Druckvolumen**
|
||||
- Funktion: `calculatePrintVolumes` (in `src/utils/analytics/volume.ts`).
|
||||
- Vergleicht die Anzahl der abgeschlossenen Druckaufträge für heute, diese Woche und diesen Monat.
|
||||
|
||||
Beispielausgabe:
|
||||
```json
|
||||
{
|
||||
"today": 15,
|
||||
"thisWeek": 90,
|
||||
"thisMonth": 300
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **3. Datenbankinitialisierung**
|
||||
Die Datenbank wird über Skripte in der `package.json` initialisiert:
|
||||
```bash
|
||||
pnpm run db:clean # Datenbank und Migrationsordner löschen
|
||||
pnpm run db:generate # Neues Schema generieren
|
||||
pnpm run db:migrate # Migrationsskripte ausführen
|
||||
```
|
||||
|
||||
Nächster Schritt: [=> Bereitstellungsdetails und Best Practices](./Bereitstellungsdetails.md)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user