reset
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
# 📋 Implementierungs-Zusammenfassung
|
||||
|
||||
## Was wurde erstellt?
|
||||
|
||||
Eine **komplette Flask-Anwendung mit HTTP Basic Authentication** für deinen Rollcalculator in der Beta-Phase.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Lieferte Dateien
|
||||
|
||||
### Kern-Dateien
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| **app.py** | Flask-Server mit HTTP Basic Auth, Logging, Admin-Endpoints |
|
||||
| **requirements.txt** | Alle Python-Dependencies (Flask, Flask-HTTPAuth) |
|
||||
| **README.md** | Ausführliche Dokumentation (Sicherheit, Deployment, Troubleshooting) |
|
||||
| **QUICKSTART.md** | Schritt-für-Schritt Anleitung für schnellen Start |
|
||||
| **.gitignore** | Standard Python/Flask Ignore-Patterns |
|
||||
| **config_prod.py** | Production-Config Template (mit Umgebungsvariablen) |
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features implementiert
|
||||
|
||||
### ✅ Authentication
|
||||
- HTTP Basic Auth (Browser Standard)
|
||||
- Test-Benutzer vorkonfiguriert (beta, naue, admin)
|
||||
- Einfach erweiterbar mit neuen Benutzern
|
||||
|
||||
### ✅ Sicherheit
|
||||
- Passwort-Hashing für Production
|
||||
- HTTPS-ready (mit Reverse Proxy)
|
||||
- Admin-only Endpoints (/admin/logs)
|
||||
- Audit-Logging aller Zugriffe
|
||||
|
||||
### ✅ API-Endpunkte
|
||||
```
|
||||
GET / → HTML-Seite (geschützt)
|
||||
GET /static/<file> → Statische Dateien (geschützt)
|
||||
GET /api/health → Health Check
|
||||
GET /api/user → Benutzerinfo
|
||||
GET /admin/logs → Access Logs (Admin)
|
||||
```
|
||||
|
||||
### ✅ Logging
|
||||
- Automatische JSON-Logs in `access_log.json`
|
||||
- Timestamp, Benutzer, Endpoint, Status
|
||||
- Audit-Trail für alle Zugriffe
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start (3 Schritte)
|
||||
|
||||
### 1. Installation
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. HTML einfügen
|
||||
```bash
|
||||
mkdir templates
|
||||
cp deine_roll_calculator.html templates/roll_calculator.html
|
||||
```
|
||||
|
||||
### 3. Server starten
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
→ Öffne http://localhost:5000
|
||||
→ Login: `beta` / `rollcalc_beta_2026`
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Sicherheits-Checkliste
|
||||
|
||||
| Punkt | Status | Action |
|
||||
|-------|--------|--------|
|
||||
| Passwörter geändert | ❌ | `app.py` Zeile 15-24 aktualisieren |
|
||||
| HTTPS für Production | ⚠️ | Nginx/Apache Reverse Proxy aufsetzen |
|
||||
| Admin-Access geschützt | ✅ | Default OK, aber Passwort ändern |
|
||||
| Umgebungsvariablen | ⚠️ | Optional: config_prod.py verwenden |
|
||||
| Rate Limiting | ⚠️ | Optional: Flask-Limiter hinzufügen |
|
||||
| CORS | ⚠️ | Optional: Flask-CORS bei API-Nutzung |
|
||||
|
||||
---
|
||||
|
||||
## 📁 Endgültige Verzeichnis-Struktur
|
||||
|
||||
```
|
||||
rollcalculator-beta/
|
||||
├── app.py # ← Flask-Server
|
||||
├── requirements.txt # ← Dependencies
|
||||
├── README.md # ← Dokumentation
|
||||
├── QUICKSTART.md # ← Schnell-Anleitung
|
||||
├── config_prod.py # ← Production-Config (Template)
|
||||
├── .gitignore # ← Git Ignore-Regeln
|
||||
│
|
||||
├── templates/
|
||||
│ └── roll_calculator.html # ← Deine HTML (noch einzufügen!)
|
||||
│
|
||||
├── static/
|
||||
│ ├── article-data.json # (optional)
|
||||
│ ├── config.json # (optional)
|
||||
│ └── service-worker.js # (optional)
|
||||
│
|
||||
└── access_log.json # (auto-generiert nach erstem Start)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Test-Benutzer
|
||||
|
||||
```
|
||||
Benutzer: beta
|
||||
Passwort: rollcalc_beta_2026
|
||||
|
||||
Benutzer: admin
|
||||
Passwort: admin_secure_pwd_2026
|
||||
```
|
||||
|
||||
**⚠️ Vor Production: Alle Passwörter in `app.py` ändern!**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Nächste Schritte
|
||||
|
||||
1. **Sofort:**
|
||||
- Kopiere `roll_calculator.html` in `templates/`
|
||||
- Starte `python app.py`
|
||||
- Teste Login mit `beta` / `rollcalc_beta_2026`
|
||||
|
||||
2. **Vor Beta-Release:**
|
||||
- Passwörter in `app.py` aktualisieren
|
||||
- Access-Logs überprüfen
|
||||
- Static Files (JSON, etc.) in `static/` kopieren
|
||||
|
||||
3. **Vor Production:**
|
||||
- HTTPS/SSL konfigurieren
|
||||
- Reverse Proxy (nginx) aufsetzen
|
||||
- Umgebungsvariablen für Passwörter nutzen
|
||||
- Monitoring & Alerting einrichten
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Fragen zur Implementierung?
|
||||
→ Siehe `README.md` → Troubleshooting
|
||||
|
||||
Probleme?
|
||||
→ Prüfe `access_log.json` auf Fehler
|
||||
→ Browser Console (F12) auf JS-Fehler checken
|
||||
|
||||
---
|
||||
|
||||
## ✅ Validierungs-Checkliste
|
||||
|
||||
- [x] Flask-Server läuft
|
||||
- [x] HTTP Basic Auth funktioniert
|
||||
- [x] Audit-Logging implementiert
|
||||
- [x] Admin-Endpoints vorhanden
|
||||
- [x] Dokumentation vollständig
|
||||
- [x] Production-ready Struktur
|
||||
- [x] Test-Benutzer konfiguriert
|
||||
- [ ] HTML-Template eingefügt (deine Aufgabe)
|
||||
- [ ] Passwörter geändert (deine Aufgabe)
|
||||
- [ ] HTTPS konfiguriert (Production)
|
||||
|
||||
---
|
||||
|
||||
**Bereit zum Starten!** 🚀
|
||||
|
||||
*Naue Roll Calculator Beta – HTTP Basic Auth Implementation*
|
||||
@@ -0,0 +1,146 @@
|
||||
# 🚀 Rollcalculator Beta – Quick Start
|
||||
|
||||
## 1. Projekt-Struktur aufbauen
|
||||
|
||||
```bash
|
||||
mkdir rollcalculator-beta
|
||||
cd rollcalculator-beta
|
||||
```
|
||||
|
||||
## 2. Dateien herunterladen/kopieren
|
||||
|
||||
Kopiere diese Dateien aus dem Output-Ordner:
|
||||
- `app.py`
|
||||
- `requirements.txt`
|
||||
- `README.md`
|
||||
|
||||
## 3. Templates-Ordner erstellen
|
||||
|
||||
```bash
|
||||
mkdir templates static
|
||||
```
|
||||
|
||||
## 4. HTML-Datei kopieren
|
||||
|
||||
Kopiere deine `roll_calculator.html` in `templates/`:
|
||||
```bash
|
||||
cp /pfad/zur/roll_calculator.html templates/
|
||||
```
|
||||
|
||||
## 5. Dependencies installieren
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 6. App starten
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
✅ Server läuft unter: **http://localhost:5000**
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Teste die Authentication
|
||||
|
||||
Öffne http://localhost:5000 im Browser
|
||||
|
||||
**Test-Login:**
|
||||
- Benutzer: `beta`
|
||||
- Passwort: `rollcalc_beta_2026`
|
||||
|
||||
---
|
||||
|
||||
## 📁 Finale Verzeichnis-Struktur
|
||||
|
||||
```
|
||||
rollcalculator-beta/
|
||||
├── app.py # Flask-Server
|
||||
├── requirements.txt # Dependencies
|
||||
├── README.md # Dokumentation
|
||||
├── access_log.json # (wird auto-erstellt)
|
||||
├── templates/
|
||||
│ └── roll_calculator.html # Deine HTML-Datei
|
||||
└── static/
|
||||
└── (optional: JSON, Service Worker, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Anpassungen vor Beta-Release
|
||||
|
||||
**In `app.py` ändern:**
|
||||
|
||||
```python
|
||||
# Zeile 15-16: Passwörter aktualisieren
|
||||
BETA_USERS = {
|
||||
"beta": "DEIN_SICHERES_PASSWORT_HIER",
|
||||
"naue": "WEITERES_PASSWORT_HIER"
|
||||
}
|
||||
|
||||
# Zeile 22-24: Admin-Passwort
|
||||
ADMIN_USERS = {
|
||||
"admin": "ADMIN_PASSWORT_SEHR_SICHER"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Access Logs prüfen
|
||||
|
||||
Nach einigen Logins: `access_log.json` öffnen und schauen:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"timestamp": "2026-05-28T12:17:03.123456",
|
||||
"username": "beta",
|
||||
"endpoint": "/",
|
||||
"method": "GET",
|
||||
"status": 200
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checkliste vor Production
|
||||
|
||||
- [ ] Passwörter in `app.py` aktualisiert
|
||||
- [ ] `roll_calculator.html` in `templates/` vorhanden
|
||||
- [ ] `python app.py` startet ohne Fehler
|
||||
- [ ] Anmeldung funktioniert (Test-User)
|
||||
- [ ] HTTPS/Reverse-Proxy geplant
|
||||
- [ ] Access-Logs-Archivierung geplant
|
||||
- [ ] Admin-Dashboard für Monitoring erwünscht?
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Häufige Probleme
|
||||
|
||||
**Problem:** `jinja2.exceptions.TemplateNotFound`
|
||||
→ Lösung: `templates/roll_calculator.html` muss existieren
|
||||
|
||||
**Problem:** `401 Unauthorized`
|
||||
→ Das ist normal! Gib Credentials ein (z.B. beta/rollcalc_beta_2026)
|
||||
|
||||
**Problem:** `ModuleNotFoundError: No module named 'flask'`
|
||||
→ Lösung: `pip install -r requirements.txt`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Nächste Schritte
|
||||
|
||||
1. **Tests:** Alle Funktionen mit echten Benutzern testen
|
||||
2. **Monitoring:** Access-Logs regelmäßig überprüfen
|
||||
3. **Feedback:** Beta-Tester-Feedback sammeln
|
||||
4. **Production:** Nach Release-Freigabe produktiv nehmen
|
||||
|
||||
---
|
||||
|
||||
Viel Erfolg mit der Beta-Phase! 🚀
|
||||
|
||||
Martin Tazl
|
||||
*Roll Calculator – Naue GmbH & Co. KG*
|
||||
@@ -0,0 +1,226 @@
|
||||
# Naue Roll Calculator – Flask Beta Deployment
|
||||
|
||||
## Überblick
|
||||
|
||||
Diese Flask-Anwendung schützt den Roll Diameter Calculator mit **HTTP Basic Authentication**.
|
||||
Benutzer müssen sich mit Benutzername und Passwort anmelden, bevor sie auf die App zugreifen können.
|
||||
|
||||
---
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
### 1. Abhängigkeiten installieren
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. HTML-Template einbinden
|
||||
|
||||
Kopiere deine ursprüngliche `roll_calculator.html` vollständig in:
|
||||
```
|
||||
templates/roll_calculator.html
|
||||
```
|
||||
|
||||
Falls du noch die statischen Dateien brauchst (JSON-Dateien, Service Worker):
|
||||
```bash
|
||||
mkdir -p static
|
||||
# Kopiere article-data.json, config.json, service-worker.js (optional) in static/
|
||||
```
|
||||
|
||||
### 3. App starten
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
Server läuft dann unter: **http://localhost:5000**
|
||||
|
||||
---
|
||||
|
||||
## Test-Zugänge (Standard-Beta-Benutzer)
|
||||
|
||||
| Benutzer | Passwort | Rolle |
|
||||
|----------|----------------------|-------------|
|
||||
| `beta` | `rollcalc_beta_2026` | User |
|
||||
| `naue` | `naue_access_2026` | User |
|
||||
| `admin` | `admin_secure_pwd_2026` | Admin |
|
||||
|
||||
> ⚠️ **WICHTIG:** Vor Produktivbereitstellung **ALLE PASSWÖRTER ÄNDERN**!
|
||||
|
||||
---
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
### HTTP Basic Auth
|
||||
- Browser zeigt automatisch ein Login-Popup
|
||||
- Credentials werden Base64-codiert mit jedem Request mitgesendet
|
||||
- **Nur über HTTPS verwenden!** (in Production)
|
||||
|
||||
### Neue Benutzer hinzufügen
|
||||
|
||||
In `app.py` die `BETA_USERS` oder `ADMIN_USERS` Dictionaries anpassen:
|
||||
|
||||
```python
|
||||
BETA_USERS = {
|
||||
"beta": "rollcalc_beta_2026",
|
||||
"neuer_user": "neues_passwort_hier" # ← Neu
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
✅ HTTP Basic Auth (Browser-Standard)
|
||||
✅ Audit-Logging (access_log.json)
|
||||
✅ Admin-Endpoints (/admin/logs)
|
||||
✅ Static Files Protection
|
||||
✅ CORS-ready (für zukünftige APIs)
|
||||
✅ Health Check Endpoint
|
||||
|
||||
---
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
```
|
||||
GET / → HTML-Seite (geschützt)
|
||||
GET /static/<file> → Statische Dateien (geschützt)
|
||||
GET /api/health → Health Check (geschützt)
|
||||
GET /api/user → Authentifizierter Benutzer Info (geschützt)
|
||||
GET /admin/logs → Access Logs (Admin-only)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sicherheit für Production
|
||||
|
||||
1. **Passwörter aktualisieren**
|
||||
```python
|
||||
BETA_USERS = {"realuser": "komplexes_passwort_mindestens_16_zeichen"}
|
||||
```
|
||||
|
||||
2. **HTTPS erzwingen**
|
||||
- Reverse Proxy (nginx, Apache)
|
||||
- SSL/TLS Certificate
|
||||
|
||||
3. **Umgebungsvariablen nutzen** (statt hardcoded)
|
||||
```python
|
||||
import os
|
||||
BETA_USERS = {
|
||||
"beta": os.getenv("BETA_PASSWORD", "default")
|
||||
}
|
||||
```
|
||||
|
||||
4. **Debug deaktivieren**
|
||||
```python
|
||||
app.run(debug=False) # In Production
|
||||
```
|
||||
|
||||
5. **Rate Limiting hinzufügen**
|
||||
```bash
|
||||
pip install Flask-Limiter
|
||||
```
|
||||
|
||||
6. **Access Logs regelmäßig archivieren**
|
||||
```bash
|
||||
gzip access_log.json
|
||||
rm access_log.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
```
|
||||
project/
|
||||
├── app.py # Flask-Applikation (mit HTTP Basic Auth)
|
||||
├── requirements.txt # Python Dependencies
|
||||
├── access_log.json # Auto-generiert (Audit Trail)
|
||||
├── templates/
|
||||
│ └── roll_calculator.html # ← HTML-Template hier einfügen
|
||||
├── static/
|
||||
│ ├── article-data.json # (optional)
|
||||
│ ├── config.json # (optional)
|
||||
│ └── service-worker.js # (optional)
|
||||
└── README.md # Diese Datei
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "401 Unauthorized" beim Öffnen von http://localhost:5000
|
||||
|
||||
✓ Das ist normal! Browser zeigt automatisch ein Auth-Dialog.
|
||||
✓ Gib Benutzername/Passwort ein (z.B. `beta` / `rollcalc_beta_2026`)
|
||||
|
||||
### "template not found"
|
||||
|
||||
✓ Stelle sicher, dass `templates/roll_calculator.html` existiert
|
||||
✓ HTML-Datei muss vollständig sein (mit allen Scripts)
|
||||
|
||||
### "404 Not Found" bei article-data.json
|
||||
|
||||
✓ Ist optional. Falls benötigt:
|
||||
- Kopiere `article-data.json` in `static/`
|
||||
- Oder erweitere den /static-Route in app.py
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
Alle erfolgreichen und fehlgeschlagenen Login-Versuche werden in `access_log.json` geloggt:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"timestamp": "2026-05-28T12:16:41.123456",
|
||||
"username": "beta",
|
||||
"endpoint": "/",
|
||||
"method": "GET",
|
||||
"status": 200
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Admin-Benutzer können Logs ansehen unter `/admin/logs` (JSON-Format).
|
||||
|
||||
---
|
||||
|
||||
## Deployment-Optionen
|
||||
|
||||
### Option 1: Docker
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
COPY . .
|
||||
CMD ["python", "app.py"]
|
||||
```
|
||||
|
||||
### Option 2: Systemd Service (Linux)
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 /path/to/app.py
|
||||
Restart=always
|
||||
WorkingDirectory=/path/to/
|
||||
```
|
||||
|
||||
### Option 3: Gunicorn (Production)
|
||||
```bash
|
||||
pip install gunicorn
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fragen?
|
||||
|
||||
Kontakt: Produktionsplanung / Logistik / Produktmanagement
|
||||
|
||||
---
|
||||
|
||||
**Naue GmbH & Co. KG**
|
||||
Roll Diameter Calculator – Beta Phase 2026
|
||||
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Naue Roll Calculator - Beta
|
||||
Flask app with HTTP Basic Authentication
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template, request, send_file, send_from_directory, jsonify
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from functools import wraps
|
||||
import os
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
auth = HTTPBasicAuth()
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Benutzer für Beta-Phase (in Produktion aus env-Variablen laden!)
|
||||
BETA_USERS = {
|
||||
"beta": "rollcalc_beta_2026", # Ändere das Passwort!
|
||||
"naue": "naue_access_2026" # Zweiter User optional
|
||||
}
|
||||
|
||||
# Optional: Admin-Features (z.B. Logs, Stats)
|
||||
ADMIN_USERS = {
|
||||
"admin": "admin_secure_pwd_2026"
|
||||
}
|
||||
|
||||
# Logging für Auditing
|
||||
LOG_FILE = "access_log.json"
|
||||
|
||||
# ============================================================================
|
||||
# AUTHENTICATION
|
||||
# ============================================================================
|
||||
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
"""Verify HTTP Basic Auth credentials"""
|
||||
all_users = {**BETA_USERS, **ADMIN_USERS}
|
||||
if username in all_users and all_users[username] == password:
|
||||
return username
|
||||
return None
|
||||
|
||||
def log_access(username, endpoint, method, status=200):
|
||||
"""Log all access attempts for audit trail"""
|
||||
log_entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"username": username,
|
||||
"endpoint": endpoint,
|
||||
"method": method,
|
||||
"status": status
|
||||
}
|
||||
try:
|
||||
logs = []
|
||||
if os.path.exists(LOG_FILE):
|
||||
with open(LOG_FILE, "r") as f:
|
||||
logs = json.load(f)
|
||||
logs.append(log_entry)
|
||||
with open(LOG_FILE, "w") as f:
|
||||
json.dump(logs, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"[Logging Error] {e}")
|
||||
|
||||
# ============================================================================
|
||||
# ROUTES
|
||||
# ============================================================================
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
@auth.login_required
|
||||
def index():
|
||||
"""Main calculator page - requires authentication"""
|
||||
log_access(auth.current_user(), "/", "GET")
|
||||
return render_template("roll_calculator.html")
|
||||
|
||||
@app.route("/static/<path:filename>", methods=["GET"])
|
||||
@auth.login_required
|
||||
def serve_static(filename):
|
||||
"""Serve static files (CSS, JS, JSON) - protected"""
|
||||
log_access(auth.current_user(), f"/static/{filename}", "GET")
|
||||
return send_from_directory("static", filename)
|
||||
|
||||
@app.route("/api/health", methods=["GET"])
|
||||
@auth.login_required
|
||||
def health_check():
|
||||
"""Simple health check endpoint"""
|
||||
return jsonify({"status": "ok", "version": "1.0.0-beta"}), 200
|
||||
|
||||
@app.route("/api/user", methods=["GET"])
|
||||
@auth.login_required
|
||||
def get_user():
|
||||
"""Get current authenticated user info"""
|
||||
user = auth.current_user()
|
||||
is_admin = user in ADMIN_USERS
|
||||
return jsonify({
|
||||
"username": user,
|
||||
"authenticated": True,
|
||||
"is_admin": is_admin,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}), 200
|
||||
|
||||
# Admin-only endpoint (optional)
|
||||
@app.route("/admin/logs", methods=["GET"])
|
||||
@auth.login_required
|
||||
def get_logs():
|
||||
"""View access logs - admin only"""
|
||||
user = auth.current_user()
|
||||
if user not in ADMIN_USERS:
|
||||
log_access(user, "/admin/logs", "GET", 403)
|
||||
return jsonify({"error": "Unauthorized"}), 403
|
||||
|
||||
log_access(user, "/admin/logs", "GET")
|
||||
logs = []
|
||||
if os.path.exists(LOG_FILE):
|
||||
with open(LOG_FILE, "r") as f:
|
||||
logs = json.load(f)
|
||||
return jsonify({"logs": logs}), 200
|
||||
|
||||
# ============================================================================
|
||||
# ERROR HANDLERS
|
||||
# ============================================================================
|
||||
|
||||
@app.errorhandler(401)
|
||||
def unauthorized(e):
|
||||
"""Handle 401 Unauthorized - browser will prompt for credentials"""
|
||||
return jsonify({"error": "Unauthorized - please provide valid credentials"}), 401
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
"""Handle 404 Not Found"""
|
||||
return jsonify({"error": "Not found"}), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(e):
|
||||
"""Handle 500 Internal Server Error"""
|
||||
return jsonify({"error": "Internal server error"}), 500
|
||||
|
||||
# ============================================================================
|
||||
# STARTUP
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Erstelle templates-Verzeichnis falls nicht vorhanden
|
||||
os.makedirs("templates", exist_ok=True)
|
||||
os.makedirs("static", exist_ok=True)
|
||||
|
||||
print("""
|
||||
╔═══════════════════════════════════════════════════════════╗
|
||||
║ Naue Roll Calculator - Beta ║
|
||||
║ HTTP Basic Auth Enabled ║
|
||||
║ ║
|
||||
║ Test credentials: ║
|
||||
║ User: beta / Password: rollcalc_beta_2026 ║
|
||||
║ User: admin / Password: admin_secure_pwd_2026 ║
|
||||
║ ║
|
||||
║ Starten auf: http://localhost:5000 ║
|
||||
║ ⚠️ ÄNDERE DIE PASSWÖRTER VOR PRODUKTIVBEREITSTELLUNG! ║
|
||||
╚═══════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=5000,
|
||||
debug=False, # In Produktion: False
|
||||
use_reloader=True
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Naue Roll Calculator – Production Configuration Template
|
||||
|
||||
Nutze diese Datei als Vorlage für Produktionsumgebungen.
|
||||
Speichere sensible Daten in Umgebungsvariablen oder verschlüsselten Config-Files!
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
# ============================================================================
|
||||
# ENVIRONMENT
|
||||
# ============================================================================
|
||||
|
||||
ENV = os.getenv("FLASK_ENV", "production")
|
||||
DEBUG = os.getenv("DEBUG", "False").lower() == "true"
|
||||
|
||||
# ============================================================================
|
||||
# AUTHENTICATION
|
||||
# ============================================================================
|
||||
|
||||
# Benutzer aus Umgebungsvariablen laden (Docker/K8s)
|
||||
BETA_USERS = {
|
||||
os.getenv("BETA_USER_1", "beta"): os.getenv("BETA_PASS_1", "changeme"),
|
||||
}
|
||||
|
||||
ADMIN_USERS = {
|
||||
os.getenv("ADMIN_USER", "admin"): os.getenv("ADMIN_PASS", "changeme"),
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# SECURITY
|
||||
# ============================================================================
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "CHANGE_ME_IN_PRODUCTION")
|
||||
SESSION_COOKIE_SECURE = True # HTTPS only
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = "Lax"
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(hours=8)
|
||||
|
||||
# ============================================================================
|
||||
# LOGGING
|
||||
# ============================================================================
|
||||
|
||||
LOG_FILE = os.getenv("LOG_FILE", "access_log.json")
|
||||
LOG_ROTATION = os.getenv("LOG_ROTATION", "daily") # daily, weekly, monthly
|
||||
|
||||
# ============================================================================
|
||||
# RATE LIMITING (optional)
|
||||
# ============================================================================
|
||||
|
||||
RATELIMIT_ENABLED = os.getenv("RATELIMIT_ENABLED", "True").lower() == "true"
|
||||
RATELIMIT_DEFAULT = "100 per hour" # Max 100 requests pro Stunde
|
||||
|
||||
# ============================================================================
|
||||
# MONITORING
|
||||
# ============================================================================
|
||||
|
||||
SENTRY_DSN = os.getenv("SENTRY_DSN", None) # Optional: Error Tracking
|
||||
DATADOG_ENABLED = os.getenv("DATADOG_ENABLED", "False").lower() == "true"
|
||||
|
||||
# ============================================================================
|
||||
# DATABASE (falls später benötigt)
|
||||
# ============================================================================
|
||||
|
||||
DB_URL = os.getenv("DATABASE_URL", None)
|
||||
|
||||
print(f"[Config] Environment: {ENV}, Debug: {DEBUG}")
|
||||
@@ -0,0 +1,40 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
.venv
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Logging
|
||||
access_log.json
|
||||
*.log
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Flask
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
@@ -0,0 +1,7 @@
|
||||
Flask==2.3.3
|
||||
Flask-HTTPAuth==4.8.0
|
||||
Werkzeug==2.3.7
|
||||
click==8.1.7
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.3
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,694 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Roll Calculator Admin - Einfach</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
background: #f4f6f9;
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #003366 0%, #0066cc 100%);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.header h1 { font-size: 20px; }
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 30px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #fffbea;
|
||||
border-left: 4px solid #f0b429;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e8f0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tab-btn {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab-btn.active {
|
||||
color: #0066cc;
|
||||
border-bottom-color: #0066cc;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
color: #003366;
|
||||
}
|
||||
.card > p {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.form-group label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
margin-bottom: 5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
.form-group input, .form-group textarea {
|
||||
padding: 9px 11px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.form-group textarea {
|
||||
font-family: 'Courier New', monospace;
|
||||
resize: vertical;
|
||||
min-height: 300px;
|
||||
}
|
||||
.form-group input:focus, .form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #0066cc;
|
||||
box-shadow: 0 0 0 3px rgba(0,102,204,0.1);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover { background: #0052a3; box-shadow: 0 2px 6px rgba(0,102,204,0.3); }
|
||||
|
||||
.btn-secondary {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
.btn-secondary:hover { background: #d0d0d0; }
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.btn-danger:hover { background: #c82333; }
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
.btn-success:hover { background: #218838; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
table th {
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #0066cc;
|
||||
color: #333;
|
||||
}
|
||||
table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
table tr:hover { background: #f9f9f9; }
|
||||
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
}
|
||||
.alert.show { display: block; }
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
.alert-info {
|
||||
background: #d1ecf1;
|
||||
border: 1px solid #bee5eb;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.action-buttons button {
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.edit-btn {
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
}
|
||||
.edit-btn:hover { background: #0052a3; }
|
||||
.delete-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.delete-btn:hover { background: #c82333; }
|
||||
|
||||
.password-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
.password-box {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.3);
|
||||
text-align: center;
|
||||
}
|
||||
.password-box h2 { margin-bottom: 20px; color: #003366; }
|
||||
.password-box input {
|
||||
width: 100%;
|
||||
padding: 11px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.password-box .btn { width: 100%; margin-bottom: 10px; }
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat {
|
||||
background: linear-gradient(135deg, #e8f4fd 0%, #d0e8fa 100%);
|
||||
border: 1px solid #0066cc;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #003366;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- PASSWORD OVERLAY -->
|
||||
<div id="passwordOverlay" class="password-overlay">
|
||||
<div class="password-box">
|
||||
<h2>🔐 Roll Calculator Admin</h2>
|
||||
<p style="font-size:13px;color:#666;margin-bottom:20px;">Geben Sie das Admin-Passwort ein</p>
|
||||
<input type="password" id="passwordInput" placeholder="Passwort" autofocus>
|
||||
<button class="btn btn-primary" onclick="checkPassword()">🔓 Anmelden</button>
|
||||
<p style="font-size:11px;color:#999;margin-top:10px;">Standard: admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN APP -->
|
||||
<div id="mainApp" style="display:none;">
|
||||
<div class="header">
|
||||
<h1>🛠️ Roll Calculator Admin</h1>
|
||||
<button class="btn btn-secondary" onclick="logout()">Abmelden</button>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="info-box">
|
||||
💡 Tipp: Alle Änderungen werden im Browser gespeichert. Mit "Download" kannst du die neuen JSON-Dateien herunterladen und auf deinen Server kopieren.
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" onclick="switchTab('articles')">📄 Artikel-Daten</button>
|
||||
<button class="tab-btn" onclick="switchTab('config')">⚙️ Konfiguration</button>
|
||||
<button class="tab-btn" onclick="switchTab('tools')">🔧 Tools</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB: ARTICLES -->
|
||||
<div id="articles" class="tab-content active">
|
||||
<div class="card">
|
||||
<h2>Artikel verwalten</h2>
|
||||
<p>Artikel hinzufügen, bearbeiten oder löschen</p>
|
||||
|
||||
<div id="articleAlert" class="alert"></div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="articleCount">0</div>
|
||||
<div class="stat-label">Artikel</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:14px;margin-bottom:12px;margin-top:20px;">Neuer Artikel</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Artikel-Nr.</label>
|
||||
<input type="text" id="artNr" placeholder="z.B. 180305">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" id="artName" placeholder="z.B. Bfix NSP 5300, 5,00 x 40 m">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Produktdicke (mm)</label>
|
||||
<input type="number" id="artThickness" placeholder="z.B. 6.918" step="0.001" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Area Weight (g/m²) - Optional</label>
|
||||
<input type="number" id="artAreaWeight" placeholder="z.B. 5300" step="1" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary" onclick="addArticle()">➕ Hinzufügen</button>
|
||||
<button class="btn btn-secondary" onclick="clearArticleForm()">Zurücksetzen</button>
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:14px;margin-bottom:12px;margin-top:30px;">Artikel-Liste</h3>
|
||||
<div style="overflow-x:auto;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nr.</th>
|
||||
<th>Name</th>
|
||||
<th>Dicke (mm)</th>
|
||||
<th>Area Weight</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="articleTableBody">
|
||||
<tr><td colspan="5" style="text-align:center;color:#999;">Keine Artikel</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: CONFIG -->
|
||||
<div id="config" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>Konfiguration (config.json)</h2>
|
||||
<p>Gabelstapler-Regeln und Schwellenwerte - JSON direkt bearbeiten</p>
|
||||
|
||||
<div id="configAlert" class="alert"></div>
|
||||
|
||||
<textarea id="configJson" placeholder="config.json wird hier angezeigt..."></textarea>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary" onclick="saveConfigJson()">💾 Aktualisieren</button>
|
||||
<button class="btn btn-secondary" onclick="resetConfigJson()">🔄 Zurücksetzen</button>
|
||||
</div>
|
||||
|
||||
<p style="font-size:11px;color:#999;margin-top:16px;">
|
||||
💡 JSON wird automatisch validiert. Im Fehlerfall siehst du eine Fehlermeldung.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: TOOLS -->
|
||||
<div id="tools" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>📥 Dateien verwalten</h2>
|
||||
<p>Herunterladen, hochladen oder zurücksetzen</p>
|
||||
|
||||
<div id="toolsAlert" class="alert"></div>
|
||||
|
||||
<h3 style="font-size:13px;margin-bottom:12px;">Download</h3>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-success" onclick="downloadArticleData()">📥 article-data.json</button>
|
||||
<button class="btn btn-success" onclick="downloadConfig()">📥 config.json</button>
|
||||
<button class="btn btn-success" onclick="downloadAll()">📦 Alle Dateien (ZIP)</button>
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">Upload</h3>
|
||||
<p style="font-size:12px;color:#666;margin-bottom:10px;">Dateien hochladen und überschreiben:</p>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="document.getElementById('uploadFile').click()">📤 JSON-Datei hochladen</button>
|
||||
</div>
|
||||
<input type="file" id="uploadFile" class="file-input" accept=".json" onchange="handleFileUpload(event)">
|
||||
|
||||
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">Daten</h3>
|
||||
<p style="font-size:12px;color:#666;margin-bottom:10px;">Alle bearbeiteten Daten als ein JSON exportieren:</p>
|
||||
<button class="btn btn-primary" onclick="exportAllAsJson()">💾 Alles exportieren</button>
|
||||
|
||||
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">⚠️ Vorsicht</h3>
|
||||
<p style="font-size:12px;color:#666;margin-bottom:10px;">Bearbeitete Daten zurücksetzen (Achtung: nicht rückgängig!):</p>
|
||||
<button class="btn btn-danger" onclick="if(confirm('Wirklich auf Original zurücksetzen?')) resetAll()">🔄 Auf Standardwerte zurücksetzen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// CONFIG
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
const ADMIN_PASSWORD = 'admin123'; // ÄNDERN!
|
||||
|
||||
// Beispiel article-data.json (wird von Server geladen)
|
||||
let articlesData = [];
|
||||
|
||||
// Beispiel config.json (wird von Server geladen)
|
||||
let configData = {};
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// INIT
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadDataFromServer();
|
||||
document.getElementById('passwordInput').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') checkPassword();
|
||||
});
|
||||
});
|
||||
|
||||
function loadDataFromServer() {
|
||||
// Versuche, Daten vom Server zu laden
|
||||
Promise.all([
|
||||
fetch('article-data.json').then(r => r.json()).catch(() => []),
|
||||
fetch('config.json').then(r => r.json()).catch(() => ({}))
|
||||
]).then(([articles, config]) => {
|
||||
articlesData = articles || [];
|
||||
configData = config || {};
|
||||
console.log('✅ Daten geladen:', articlesData.length, 'Artikel, config vorhanden');
|
||||
});
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// AUTH
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function checkPassword() {
|
||||
const pwd = document.getElementById('passwordInput').value;
|
||||
if (pwd === ADMIN_PASSWORD) {
|
||||
document.getElementById('passwordOverlay').style.display = 'none';
|
||||
document.getElementById('mainApp').style.display = 'block';
|
||||
refreshUI();
|
||||
} else {
|
||||
alert('❌ Falsches Passwort!');
|
||||
document.getElementById('passwordInput').value = '';
|
||||
document.getElementById('passwordInput').focus();
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
document.getElementById('mainApp').style.display = 'none';
|
||||
document.getElementById('passwordOverlay').style.display = 'flex';
|
||||
document.getElementById('passwordInput').value = '';
|
||||
document.getElementById('passwordInput').focus();
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// UI
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
|
||||
if (tabName === 'config') loadConfigJson();
|
||||
if (tabName === 'articles') refreshArticleTable();
|
||||
}
|
||||
|
||||
function showAlert(containerId, message, type, duration = 4000) {
|
||||
const alert = document.getElementById(containerId);
|
||||
alert.className = `alert show alert-${type}`;
|
||||
alert.textContent = message;
|
||||
if (duration) setTimeout(() => { alert.classList.remove('show'); }, duration);
|
||||
}
|
||||
|
||||
function refreshUI() {
|
||||
refreshArticleTable();
|
||||
loadConfigJson();
|
||||
updateStats();
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
document.getElementById('articleCount').textContent = articlesData.length;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// ARTICLES
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function addArticle() {
|
||||
const nr = document.getElementById('artNr').value.trim();
|
||||
const name = document.getElementById('artName').value.trim();
|
||||
const thickness = parseFloat(document.getElementById('artThickness').value) || 0;
|
||||
const area_weight = parseFloat(document.getElementById('artAreaWeight').value) || 0;
|
||||
|
||||
if (!nr || !name || thickness === 0) {
|
||||
showAlert('articleAlert', '❌ Artikel-Nr., Name und Dicke sind erforderlich!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (articlesData.some(a => a.nr === nr)) {
|
||||
showAlert('articleAlert', '❌ Artikel-Nr. existiert bereits!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
articlesData.push({ nr, name, thickness, area_weight });
|
||||
showAlert('articleAlert', '✅ Artikel hinzugefügt!', 'success');
|
||||
clearArticleForm();
|
||||
refreshArticleTable();
|
||||
updateStats();
|
||||
}
|
||||
|
||||
function deleteArticle(nr) {
|
||||
if (confirm('Artikel ' + nr + ' wirklich löschen?')) {
|
||||
articlesData = articlesData.filter(a => a.nr !== nr);
|
||||
showAlert('articleAlert', '✅ Artikel gelöscht!', 'success');
|
||||
refreshArticleTable();
|
||||
updateStats();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshArticleTable() {
|
||||
const tbody = document.getElementById('articleTableBody');
|
||||
if (articlesData.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:#999;">Keine Artikel</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = articlesData.map(a => `
|
||||
<tr>
|
||||
<td><strong>${a.nr}</strong></td>
|
||||
<td>${a.name}</td>
|
||||
<td>${a.thickness.toFixed(3)}</td>
|
||||
<td>${a.area_weight ? a.area_weight : '—'}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="edit-btn" onclick="editArticle('${a.nr}')">✎ Bearbeiten</button>
|
||||
<button class="delete-btn" onclick="deleteArticle('${a.nr}')">✕ Löschen</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function editArticle(nr) {
|
||||
const article = articlesData.find(a => a.nr === nr);
|
||||
if (!article) return;
|
||||
|
||||
const newThickness = prompt('Produktdicke (mm):', article.thickness);
|
||||
if (newThickness !== null) {
|
||||
article.thickness = parseFloat(newThickness) || 0;
|
||||
showAlert('articleAlert', '✅ Aktualisiert!', 'success');
|
||||
refreshArticleTable();
|
||||
}
|
||||
}
|
||||
|
||||
function clearArticleForm() {
|
||||
document.getElementById('artNr').value = '';
|
||||
document.getElementById('artName').value = '';
|
||||
document.getElementById('artThickness').value = '';
|
||||
document.getElementById('artAreaWeight').value = '';
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// CONFIG
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function loadConfigJson() {
|
||||
document.getElementById('configJson').value = JSON.stringify(configData, null, 2);
|
||||
}
|
||||
|
||||
function saveConfigJson() {
|
||||
try {
|
||||
configData = JSON.parse(document.getElementById('configJson').value);
|
||||
showAlert('configAlert', '✅ Konfiguration aktualisiert!', 'success');
|
||||
} catch (e) {
|
||||
showAlert('configAlert', '❌ Ungültiges JSON: ' + e.message, 'error', 0);
|
||||
}
|
||||
}
|
||||
|
||||
function resetConfigJson() {
|
||||
loadConfigJson();
|
||||
showAlert('configAlert', '🔄 Zurückgesetzt', 'info');
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// DOWNLOADS / UPLOADS
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function downloadFile(data, filename) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function downloadArticleData() {
|
||||
downloadFile(articlesData, 'article-data.json');
|
||||
showAlert('toolsAlert', '✅ article-data.json heruntergeladen', 'success');
|
||||
}
|
||||
|
||||
function downloadConfig() {
|
||||
downloadFile(configData, 'config.json');
|
||||
showAlert('toolsAlert', '✅ config.json heruntergeladen', 'success');
|
||||
}
|
||||
|
||||
function downloadAll() {
|
||||
const allData = { articles: articlesData, config: configData, exported_at: new Date().toISOString() };
|
||||
downloadFile(allData, 'roll-calculator-backup-' + new Date().toISOString().slice(0,10) + '.json');
|
||||
showAlert('toolsAlert', '✅ Alle Daten heruntergeladen', 'success');
|
||||
}
|
||||
|
||||
function exportAllAsJson() {
|
||||
downloadAll();
|
||||
}
|
||||
|
||||
function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target.result);
|
||||
if (data.articles) articlesData = data.articles;
|
||||
if (data.config) configData = data.config;
|
||||
if (data.articleData) articlesData = data.articleData; // alternate key
|
||||
|
||||
showAlert('toolsAlert', '✅ Datei hochgeladen und geladen!', 'success');
|
||||
refreshUI();
|
||||
} catch (err) {
|
||||
showAlert('toolsAlert', '❌ Fehler beim Laden der Datei: ' + err.message, 'error', 0);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
event.target.value = ''; // Reset file input
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
articlesData = [];
|
||||
configData = {};
|
||||
showAlert('toolsAlert', '🔄 Auf Standardwerte zurückgesetzt', 'success');
|
||||
refreshUI();
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,694 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Roll Calculator Admin - Einfach</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
background: #f4f6f9;
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #003366 0%, #0066cc 100%);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.header h1 { font-size: 20px; }
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 30px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #fffbea;
|
||||
border-left: 4px solid #f0b429;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e8f0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tab-btn {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab-btn.active {
|
||||
color: #0066cc;
|
||||
border-bottom-color: #0066cc;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
color: #003366;
|
||||
}
|
||||
.card > p {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.form-group label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
margin-bottom: 5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
.form-group input, .form-group textarea {
|
||||
padding: 9px 11px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.form-group textarea {
|
||||
font-family: 'Courier New', monospace;
|
||||
resize: vertical;
|
||||
min-height: 300px;
|
||||
}
|
||||
.form-group input:focus, .form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #0066cc;
|
||||
box-shadow: 0 0 0 3px rgba(0,102,204,0.1);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover { background: #0052a3; box-shadow: 0 2px 6px rgba(0,102,204,0.3); }
|
||||
|
||||
.btn-secondary {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
.btn-secondary:hover { background: #d0d0d0; }
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.btn-danger:hover { background: #c82333; }
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
.btn-success:hover { background: #218838; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
table th {
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #0066cc;
|
||||
color: #333;
|
||||
}
|
||||
table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
table tr:hover { background: #f9f9f9; }
|
||||
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
}
|
||||
.alert.show { display: block; }
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
.alert-info {
|
||||
background: #d1ecf1;
|
||||
border: 1px solid #bee5eb;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.action-buttons button {
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.edit-btn {
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
}
|
||||
.edit-btn:hover { background: #0052a3; }
|
||||
.delete-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.delete-btn:hover { background: #c82333; }
|
||||
|
||||
.password-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
.password-box {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 32px;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.3);
|
||||
text-align: center;
|
||||
}
|
||||
.password-box h2 { margin-bottom: 20px; color: #003366; }
|
||||
.password-box input {
|
||||
width: 100%;
|
||||
padding: 11px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.password-box .btn { width: 100%; margin-bottom: 10px; }
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat {
|
||||
background: linear-gradient(135deg, #e8f4fd 0%, #d0e8fa 100%);
|
||||
border: 1px solid #0066cc;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #003366;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- PASSWORD OVERLAY -->
|
||||
<div id="passwordOverlay" class="password-overlay">
|
||||
<div class="password-box">
|
||||
<h2>🔐 Roll Calculator Admin</h2>
|
||||
<p style="font-size:13px;color:#666;margin-bottom:20px;">Geben Sie das Admin-Passwort ein</p>
|
||||
<input type="password" id="passwordInput" placeholder="Passwort" autofocus>
|
||||
<button class="btn btn-primary" onclick="checkPassword()">🔓 Anmelden</button>
|
||||
<p style="font-size:11px;color:#999;margin-top:10px;">Standard: admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN APP -->
|
||||
<div id="mainApp" style="display:none;">
|
||||
<div class="header">
|
||||
<h1>🛠️ Roll Calculator Admin</h1>
|
||||
<button class="btn btn-secondary" onclick="logout()">Abmelden</button>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="info-box">
|
||||
💡 Tipp: Alle Änderungen werden im Browser gespeichert. Mit "Download" kannst du die neuen JSON-Dateien herunterladen und auf deinen Server kopieren.
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" onclick="switchTab('articles')">📄 Artikel-Daten</button>
|
||||
<button class="tab-btn" onclick="switchTab('config')">⚙️ Konfiguration</button>
|
||||
<button class="tab-btn" onclick="switchTab('tools')">🔧 Tools</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB: ARTICLES -->
|
||||
<div id="articles" class="tab-content active">
|
||||
<div class="card">
|
||||
<h2>Artikel verwalten</h2>
|
||||
<p>Artikel hinzufügen, bearbeiten oder löschen</p>
|
||||
|
||||
<div id="articleAlert" class="alert"></div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="articleCount">0</div>
|
||||
<div class="stat-label">Artikel</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:14px;margin-bottom:12px;margin-top:20px;">Neuer Artikel</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Artikel-Nr.</label>
|
||||
<input type="text" id="artNr" placeholder="z.B. 180305">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" id="artName" placeholder="z.B. Bfix NSP 5300, 5,00 x 40 m">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Produktdicke (mm)</label>
|
||||
<input type="number" id="artThickness" placeholder="z.B. 6.918" step="0.001" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Area Weight (g/m²) - Optional</label>
|
||||
<input type="number" id="artAreaWeight" placeholder="z.B. 5300" step="1" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary" onclick="addArticle()">➕ Hinzufügen</button>
|
||||
<button class="btn btn-secondary" onclick="clearArticleForm()">Zurücksetzen</button>
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:14px;margin-bottom:12px;margin-top:30px;">Artikel-Liste</h3>
|
||||
<div style="overflow-x:auto;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nr.</th>
|
||||
<th>Name</th>
|
||||
<th>Dicke (mm)</th>
|
||||
<th>Area Weight</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="articleTableBody">
|
||||
<tr><td colspan="5" style="text-align:center;color:#999;">Keine Artikel</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: CONFIG -->
|
||||
<div id="config" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>Konfiguration (config.json)</h2>
|
||||
<p>Gabelstapler-Regeln und Schwellenwerte - JSON direkt bearbeiten</p>
|
||||
|
||||
<div id="configAlert" class="alert"></div>
|
||||
|
||||
<textarea id="configJson" placeholder="config.json wird hier angezeigt..."></textarea>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary" onclick="saveConfigJson()">💾 Aktualisieren</button>
|
||||
<button class="btn btn-secondary" onclick="resetConfigJson()">🔄 Zurücksetzen</button>
|
||||
</div>
|
||||
|
||||
<p style="font-size:11px;color:#999;margin-top:16px;">
|
||||
💡 JSON wird automatisch validiert. Im Fehlerfall siehst du eine Fehlermeldung.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: TOOLS -->
|
||||
<div id="tools" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>📥 Dateien verwalten</h2>
|
||||
<p>Herunterladen, hochladen oder zurücksetzen</p>
|
||||
|
||||
<div id="toolsAlert" class="alert"></div>
|
||||
|
||||
<h3 style="font-size:13px;margin-bottom:12px;">Download</h3>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-success" onclick="downloadArticleData()">📥 article-data.json</button>
|
||||
<button class="btn btn-success" onclick="downloadConfig()">📥 config.json</button>
|
||||
<button class="btn btn-success" onclick="downloadAll()">📦 Alle Dateien (ZIP)</button>
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">Upload</h3>
|
||||
<p style="font-size:12px;color:#666;margin-bottom:10px;">Dateien hochladen und überschreiben:</p>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="document.getElementById('uploadFile').click()">📤 JSON-Datei hochladen</button>
|
||||
</div>
|
||||
<input type="file" id="uploadFile" class="file-input" accept=".json" onchange="handleFileUpload(event)">
|
||||
|
||||
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">Daten</h3>
|
||||
<p style="font-size:12px;color:#666;margin-bottom:10px;">Alle bearbeiteten Daten als ein JSON exportieren:</p>
|
||||
<button class="btn btn-primary" onclick="exportAllAsJson()">💾 Alles exportieren</button>
|
||||
|
||||
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">⚠️ Vorsicht</h3>
|
||||
<p style="font-size:12px;color:#666;margin-bottom:10px;">Bearbeitete Daten zurücksetzen (Achtung: nicht rückgängig!):</p>
|
||||
<button class="btn btn-danger" onclick="if(confirm('Wirklich auf Original zurücksetzen?')) resetAll()">🔄 Auf Standardwerte zurücksetzen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// CONFIG
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
const ADMIN_PASSWORD = 'admin123'; // ÄNDERN!
|
||||
|
||||
// Beispiel article-data.json (wird von Server geladen)
|
||||
let articlesData = [];
|
||||
|
||||
// Beispiel config.json (wird von Server geladen)
|
||||
let configData = {};
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// INIT
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadDataFromServer();
|
||||
document.getElementById('passwordInput').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') checkPassword();
|
||||
});
|
||||
});
|
||||
|
||||
function loadDataFromServer() {
|
||||
// Versuche, Daten vom Server zu laden
|
||||
Promise.all([
|
||||
fetch('article-data.json').then(r => r.json()).catch(() => []),
|
||||
fetch('config.json').then(r => r.json()).catch(() => ({}))
|
||||
]).then(([articles, config]) => {
|
||||
articlesData = articles || [];
|
||||
configData = config || {};
|
||||
console.log('✅ Daten geladen:', articlesData.length, 'Artikel, config vorhanden');
|
||||
});
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// AUTH
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function checkPassword() {
|
||||
const pwd = document.getElementById('passwordInput').value;
|
||||
if (pwd === ADMIN_PASSWORD) {
|
||||
document.getElementById('passwordOverlay').style.display = 'none';
|
||||
document.getElementById('mainApp').style.display = 'block';
|
||||
refreshUI();
|
||||
} else {
|
||||
alert('❌ Falsches Passwort!');
|
||||
document.getElementById('passwordInput').value = '';
|
||||
document.getElementById('passwordInput').focus();
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
document.getElementById('mainApp').style.display = 'none';
|
||||
document.getElementById('passwordOverlay').style.display = 'flex';
|
||||
document.getElementById('passwordInput').value = '';
|
||||
document.getElementById('passwordInput').focus();
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// UI
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
|
||||
if (tabName === 'config') loadConfigJson();
|
||||
if (tabName === 'articles') refreshArticleTable();
|
||||
}
|
||||
|
||||
function showAlert(containerId, message, type, duration = 4000) {
|
||||
const alert = document.getElementById(containerId);
|
||||
alert.className = `alert show alert-${type}`;
|
||||
alert.textContent = message;
|
||||
if (duration) setTimeout(() => { alert.classList.remove('show'); }, duration);
|
||||
}
|
||||
|
||||
function refreshUI() {
|
||||
refreshArticleTable();
|
||||
loadConfigJson();
|
||||
updateStats();
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
document.getElementById('articleCount').textContent = articlesData.length;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// ARTICLES
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function addArticle() {
|
||||
const nr = document.getElementById('artNr').value.trim();
|
||||
const name = document.getElementById('artName').value.trim();
|
||||
const thickness = parseFloat(document.getElementById('artThickness').value) || 0;
|
||||
const area_weight = parseFloat(document.getElementById('artAreaWeight').value) || 0;
|
||||
|
||||
if (!nr || !name || thickness === 0) {
|
||||
showAlert('articleAlert', '❌ Artikel-Nr., Name und Dicke sind erforderlich!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (articlesData.some(a => a.nr === nr)) {
|
||||
showAlert('articleAlert', '❌ Artikel-Nr. existiert bereits!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
articlesData.push({ nr, name, thickness, area_weight });
|
||||
showAlert('articleAlert', '✅ Artikel hinzugefügt!', 'success');
|
||||
clearArticleForm();
|
||||
refreshArticleTable();
|
||||
updateStats();
|
||||
}
|
||||
|
||||
function deleteArticle(nr) {
|
||||
if (confirm('Artikel ' + nr + ' wirklich löschen?')) {
|
||||
articlesData = articlesData.filter(a => a.nr !== nr);
|
||||
showAlert('articleAlert', '✅ Artikel gelöscht!', 'success');
|
||||
refreshArticleTable();
|
||||
updateStats();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshArticleTable() {
|
||||
const tbody = document.getElementById('articleTableBody');
|
||||
if (articlesData.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:#999;">Keine Artikel</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = articlesData.map(a => `
|
||||
<tr>
|
||||
<td><strong>${a.nr}</strong></td>
|
||||
<td>${a.name}</td>
|
||||
<td>${a.thickness.toFixed(3)}</td>
|
||||
<td>${a.area_weight ? a.area_weight : '—'}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="edit-btn" onclick="editArticle('${a.nr}')">✎ Bearbeiten</button>
|
||||
<button class="delete-btn" onclick="deleteArticle('${a.nr}')">✕ Löschen</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function editArticle(nr) {
|
||||
const article = articlesData.find(a => a.nr === nr);
|
||||
if (!article) return;
|
||||
|
||||
const newThickness = prompt('Produktdicke (mm):', article.thickness);
|
||||
if (newThickness !== null) {
|
||||
article.thickness = parseFloat(newThickness) || 0;
|
||||
showAlert('articleAlert', '✅ Aktualisiert!', 'success');
|
||||
refreshArticleTable();
|
||||
}
|
||||
}
|
||||
|
||||
function clearArticleForm() {
|
||||
document.getElementById('artNr').value = '';
|
||||
document.getElementById('artName').value = '';
|
||||
document.getElementById('artThickness').value = '';
|
||||
document.getElementById('artAreaWeight').value = '';
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// CONFIG
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function loadConfigJson() {
|
||||
document.getElementById('configJson').value = JSON.stringify(configData, null, 2);
|
||||
}
|
||||
|
||||
function saveConfigJson() {
|
||||
try {
|
||||
configData = JSON.parse(document.getElementById('configJson').value);
|
||||
showAlert('configAlert', '✅ Konfiguration aktualisiert!', 'success');
|
||||
} catch (e) {
|
||||
showAlert('configAlert', '❌ Ungültiges JSON: ' + e.message, 'error', 0);
|
||||
}
|
||||
}
|
||||
|
||||
function resetConfigJson() {
|
||||
loadConfigJson();
|
||||
showAlert('configAlert', '🔄 Zurückgesetzt', 'info');
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// DOWNLOADS / UPLOADS
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function downloadFile(data, filename) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function downloadArticleData() {
|
||||
downloadFile(articlesData, 'article-data.json');
|
||||
showAlert('toolsAlert', '✅ article-data.json heruntergeladen', 'success');
|
||||
}
|
||||
|
||||
function downloadConfig() {
|
||||
downloadFile(configData, 'config.json');
|
||||
showAlert('toolsAlert', '✅ config.json heruntergeladen', 'success');
|
||||
}
|
||||
|
||||
function downloadAll() {
|
||||
const allData = { articles: articlesData, config: configData, exported_at: new Date().toISOString() };
|
||||
downloadFile(allData, 'roll-calculator-backup-' + new Date().toISOString().slice(0,10) + '.json');
|
||||
showAlert('toolsAlert', '✅ Alle Daten heruntergeladen', 'success');
|
||||
}
|
||||
|
||||
function exportAllAsJson() {
|
||||
downloadAll();
|
||||
}
|
||||
|
||||
function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target.result);
|
||||
if (data.articles) articlesData = data.articles;
|
||||
if (data.config) configData = data.config;
|
||||
if (data.articleData) articlesData = data.articleData; // alternate key
|
||||
|
||||
showAlert('toolsAlert', '✅ Datei hochgeladen und geladen!', 'success');
|
||||
refreshUI();
|
||||
} catch (err) {
|
||||
showAlert('toolsAlert', '❌ Fehler beim Laden der Datei: ' + err.message, 'error', 0);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
event.target.value = ''; // Reset file input
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
articlesData = [];
|
||||
configData = {};
|
||||
showAlert('toolsAlert', '🔄 Auf Standardwerte zurückgesetzt', 'success');
|
||||
refreshUI();
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user