2 Commits
Author SHA1 Message Date
admin 5c4b944d6a added login screen and admin console 2026-05-28 15:40:26 +02:00
admin e267b1fe02 reset 2026-05-28 14:55:09 +02:00
22 changed files with 10806 additions and 0 deletions
+479
View File
@@ -0,0 +1,479 @@
# 📊 Admin Dashboard Integration – 4 Optionen
## Übersicht
Hier sind die Best Practices für die Integration eines Admin-Panels in die Flask-App:
---
## Option 1: Embedded Admin-Route (Empfohlen für Beta)
**Am schnellsten & sichersten für Beta-Phase**
### Wie es funktioniert:
- Admin-HTML wird direkt als Template serviert
- Nur für Admin-Benutzer zugänglich (/admin)
- Integriertes Logging-Dashboard
- Keine separate App nötig
### Implementierung:
**In `app.py` (nach den Error Handlers hinzufügen):**
```python
@app.route("/admin", methods=["GET"])
@auth.login_required
def admin_dashboard():
"""Admin Dashboard - nur für Admins"""
user = auth.current_user()
if user not in ADMIN_USERS:
log_access(user, "/admin", "GET", 403)
return jsonify({"error": "Unauthorized"}), 403
log_access(user, "/admin", "GET")
return render_template("admin.html", username=user)
@app.route("/api/admin/logs", methods=["GET"])
@auth.login_required
def get_admin_logs():
"""Logs als JSON für Dashboard"""
user = auth.current_user()
if user not in ADMIN_USERS:
return jsonify({"error": "Unauthorized"}), 403
logs = []
if os.path.exists(LOG_FILE):
with open(LOG_FILE, "r") as f:
logs = json.load(f)
# Optional: Letzte 100 Logs
return jsonify({"logs": logs[-100:]}), 200
@app.route("/api/admin/stats", methods=["GET"])
@auth.login_required
def get_admin_stats():
"""Statistiken für Dashboard"""
user = auth.current_user()
if user not in ADMIN_USERS:
return jsonify({"error": "Unauthorized"}), 403
logs = []
if os.path.exists(LOG_FILE):
with open(LOG_FILE, "r") as f:
logs = json.load(f)
# Statistiken berechnen
total_requests = len(logs)
unique_users = len(set(log["username"] for log in logs))
failed_logins = len([l for l in logs if l["status"] == 401])
return jsonify({
"total_requests": total_requests,
"unique_users": unique_users,
"failed_logins": failed_logins,
"last_update": datetime.now().isoformat()
}), 200
```
**Neue Template-Datei: `templates/admin.html`**
```html
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Naue Admin Panel</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: #f4f6f9;
color: #333;
}
.admin-header {
background: linear-gradient(135deg, #003366 0%, #0066cc 100%);
color: white;
padding: 20px 32px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-header h1 { font-size: 24px; }
.admin-header .user-info {
font-size: 12px;
opacity: 0.9;
}
.admin-container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px 60px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
border-left: 4px solid #0066cc;
}
.stat-card .label {
font-size: 12px;
color: #666;
text-transform: uppercase;
margin-bottom: 8px;
}
.stat-card .value {
font-size: 32px;
font-weight: 700;
color: #0066cc;
}
.logs-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
}
.logs-section h2 {
font-size: 18px;
margin-bottom: 20px;
color: #003366;
}
.logs-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.logs-table thead {
background: #f0f4fa;
border-bottom: 2px solid #0066cc;
}
.logs-table th {
padding: 12px;
text-align: left;
font-weight: 600;
color: #333;
}
.logs-table td {
padding: 10px 12px;
border-bottom: 1px solid #e8ecf0;
}
.logs-table tbody tr:hover {
background: #f8f9fa;
}
.status-200 { color: #28a745; font-weight: 600; }
.status-401 { color: #dc3545; font-weight: 600; }
.status-403 { color: #f0b429; font-weight: 600; }
.btn-refresh {
background: #0066cc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-weight: 600;
margin-bottom: 20px;
}
.btn-refresh:hover { background: #0052a3; }
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.footer {
text-align: center;
font-size: 11px;
color: #aaa;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e8ecf0;
}
</style>
</head>
<body>
<div class="admin-header">
<div>
<h1>🔐 Admin Panel</h1>
<div class="user-info">Roll Calculator – Naue</div>
</div>
<div class="user-info">
Angemeldet als: <strong>{{ username }}</strong>
</div>
</div>
<div class="admin-container">
<!-- Statistics -->
<div class="stats-grid" id="statsGrid">
<div class="loading">Statistiken werden geladen...</div>
</div>
<!-- Logs Section -->
<div class="logs-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>📋 Access Logs</h2>
<button class="btn-refresh" onclick="refreshData()">🔄 Aktualisieren</button>
</div>
<table class="logs-table" id="logsTable">
<thead>
<tr>
<th>Zeitstempel</th>
<th>Benutzer</th>
<th>Endpoint</th>
<th>Methode</th>
<th>Status</th>
</tr>
</thead>
<tbody id="logsList">
<tr>
<td colspan="5" class="loading">Logs werden geladen...</td>
</tr>
</tbody>
</table>
</div>
<div class="footer">
Naue GmbH & Co. KG · Roll Calculator Admin Panel
</div>
</div>
<script>
async function refreshData() {
try {
// Fetch Statistics
const statsRes = await fetch('/api/admin/stats');
const stats = await statsRes.json();
// Fetch Logs
const logsRes = await fetch('/api/admin/logs');
const logsData = await logsRes.json();
// Display Statistics
const statsHtml = `
<div class="stat-card">
<div class="label">Gesamte Anfragen</div>
<div class="value">${stats.total_requests}</div>
</div>
<div class="stat-card">
<div class="label">Eindeutige Benutzer</div>
<div class="value">${stats.unique_users}</div>
</div>
<div class="stat-card">
<div class="label">Fehlgeschlagene Logins</div>
<div class="value" style="color: #dc3545;">${stats.failed_logins}</div>
</div>
`;
document.getElementById('statsGrid').innerHTML = statsHtml;
// Display Logs
const logsHtml = logsData.logs.reverse().map(log => {
const time = new Date(log.timestamp).toLocaleString('de-DE');
const statusClass = `status-${log.status}`;
return `
<tr>
<td>${time}</td>
<td>${log.username}</td>
<td>${log.endpoint}</td>
<td>${log.method}</td>
<td class="${statusClass}">${log.status}</td>
</tr>
`;
}).join('');
document.getElementById('logsList').innerHTML = logsHtml ||
'<tr><td colspan="5" style="text-align: center; padding: 20px;">Keine Logs verfügbar</td></tr>';
} catch (error) {
console.error('Fehler beim Laden der Daten:', error);
document.getElementById('logsList').innerHTML =
'<tr><td colspan="5" style="color: #dc3545;">Fehler beim Laden der Daten</td></tr>';
}
}
// Initial load
document.addEventListener('DOMContentLoaded', refreshData);
// Auto-refresh alle 30 Sekunden
setInterval(refreshData, 30000);
</script>
</body>
</html>
```
### Zugriff:
- URL: `http://localhost:5000/admin`
- Nur für Admin-Benutzer (403 Unauthorized für andere)
- Automatische Log-Anzeige
---
## Option 2: Separate Admin-App (für später)
**Wenn Admin-Features wachsen:**
Erstelle eine separate `admin_app.py`:
```python
from flask import Flask, render_template
from flask_httpauth import HTTPBasicAuth
admin = Flask(__name__, template_folder='admin_templates')
auth_admin = HTTPBasicAuth()
ADMIN_USERS = {"admin": "admin_secure_pwd_2026"}
@auth_admin.verify_password
def verify(username, password):
if username in ADMIN_USERS and ADMIN_USERS[username] == password:
return username
return None
@admin.route("/", methods=["GET"])
@auth_admin.login_required
def dashboard():
return render_template("admin.html")
if __name__ == "__main__":
admin.run(port=5001, debug=False) # Anderer Port
```
Dann starten mit: `python admin_app.py`
---
## Option 3: Reverse Proxy Setup (Production)
**Mit Nginx als Reverse Proxy:**
```nginx
server {
listen 80;
server_name rollcalculator.naue.de;
# Main App (Port 5000)
location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
}
# Admin (gleiche Auth, aber separates Template)
location /admin {
proxy_pass http://localhost:5000/admin;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
}
}
```
---
## Option 4: Vue.js / React Admin UI (für erweiterte Features)
Falls du später komplexere Admin-Features brauchst:
```javascript
// static/js/admin-dashboard.js
class AdminDashboard {
constructor() {
this.stats = null;
this.logs = [];
this.init();
}
async init() {
await this.loadStats();
await this.loadLogs();
this.setupAutoRefresh();
}
async loadStats() {
const res = await fetch('/api/admin/stats');
this.stats = await res.json();
this.renderStats();
}
async loadLogs() {
const res = await fetch('/api/admin/logs');
this.logs = (await res.json()).logs;
this.renderLogs();
}
renderStats() {
// Render logic...
}
renderLogs() {
// Render logic...
}
setupAutoRefresh() {
setInterval(() => this.loadStats(), 30000);
setInterval(() => this.loadLogs(), 30000);
}
}
// Starten
new AdminDashboard();
```
---
## 🎯 Empfehlungen
| Phase | Option | Vorteile | Nachteile |
|-------|--------|----------|-----------|
| **Beta** | Option 1 (Embedded) | ✅ Schnell, ✅ Sicher, ✅ Integriert | ⚠️ Einfaches UI |
| **Production Beta** | Option 1 + Option 3 | ✅ Mit HTTPS, ✅ Skalierbar | ⚠️ Nginx-Setup |
| **Später erweitern** | Option 4 (Vue/React) | ✅ Modern, ✅ Interactive | ⚠️ Komplexer |
| **Separate Team** | Option 2 (Separate App) | ✅ Unabhängig, ✅ Unterschiedliche Tech Stack | ⚠️ 2 Apps |
---
## ⚡ Quick Start: Option 1 implementieren
1. **Kopiere den Code oben in `app.py`** (die neuen Routes)
2. **Erstelle `templates/admin.html`** mit dem HTML oben
3. **Starte Flask neu**: `python app.py`
4. **Öffne**: `http://localhost:5000/admin`
5. **Login**: admin / admin_secure_pwd_2026
---
## Nächste Schritte
- [ ] admin.html als Template in templates/ speichern
- [ ] Routes in app.py hinzufügen
- [ ] Tests mit Admin-User durchführen
- [ ] Logs überprüfen
- [ ] Optional: Charts/Graphs hinzufügen (Chart.js)
@@ -0,0 +1,215 @@
# 🔧 Admin Panel Integration – Schritt-für-Schritt
## 1. Templates-Datei erstellen
Kopiere `admin.html` in dein `templates/` Verzeichnis:
```bash
cp admin.html templates/admin.html
```
## 2. Flask Routes hinzufügen
Öffne `app.py` und füge die neuen Routes hinzu:
**Nach den Error Handlers (ca. Zeile 80), vor `if __name__ == "__main__"`, hinzufügen:**
```python
# ============================================================================
# ADMIN ROUTES
# ============================================================================
@app.route("/admin", methods=["GET"])
@auth.login_required
def admin_dashboard():
"""Admin Dashboard - nur für Admin-Benutzer"""
user = auth.current_user()
if user not in ADMIN_USERS:
log_access(user, "/admin", "GET", 403)
return jsonify({"error": "Unauthorized - Admin access required"}), 403
log_access(user, "/admin", "GET")
return render_template("admin.html")
@app.route("/api/admin/stats", methods=["GET"])
@auth.login_required
def get_admin_stats():
"""Statistiken für Admin Dashboard"""
user = auth.current_user()
if user not in ADMIN_USERS:
log_access(user, "/api/admin/stats", "GET", 403)
return jsonify({"error": "Unauthorized"}), 403
log_access(user, "/api/admin/stats", "GET")
logs = []
if os.path.exists(LOG_FILE):
try:
with open(LOG_FILE, "r") as f:
logs = json.load(f)
except json.JSONDecodeError:
logs = []
total_requests = len(logs)
unique_users = len(set(log.get("username", "unknown") for log in logs))
failed_logins = len([l for l in logs if l.get("status") == 401])
return jsonify({
"total_requests": total_requests,
"unique_users": unique_users,
"failed_logins": failed_logins,
"timestamp": datetime.now().isoformat()
}), 200
@app.route("/api/admin/logs", methods=["GET"])
@auth.login_required
def get_admin_logs():
"""Access Logs für Admin Dashboard"""
user = auth.current_user()
if user not in ADMIN_USERS:
log_access(user, "/api/admin/logs", "GET", 403)
return jsonify({"error": "Unauthorized"}), 403
log_access(user, "/api/admin/logs", "GET")
logs = []
if os.path.exists(LOG_FILE):
try:
with open(LOG_FILE, "r") as f:
logs = json.load(f)
except json.JSONDecodeError:
logs = []
logs_sorted = sorted(logs, key=lambda x: x.get("timestamp", ""), reverse=True)
return jsonify({"logs": logs_sorted[-500:]}), 200
```
## 3. Server testen
```bash
python app.py
```
## 4. Zugriff
- URL: **http://localhost:5000/admin**
- Login: **admin** / **admin_secure_pwd_2026**
---
## Features im Admin Panel
### 📊 Dashboard-Statistiken
- ✅ Gesamte API-Anfragen
- ✅ Eindeutige Benutzer
- ✅ Fehlgeschlagene Logins (401 Unauthorized)
### 📋 Access Logs
- ✅ Echtzeit-Logs mit Timestamp
- ✅ Benutzer, Endpoint, HTTP-Method, Status
- ✅ Farbcodierung nach HTTP-Status (200, 401, 403)
### 🔍 Filter & Suche
- ✅ Nach Benutzer filtern
- ✅ Nach Endpoint filtern
- ✅ Live-Filter während Eingabe
### ⬇️ Export
- ✅ Download als JSON
- ✅ Optionale CSV-Export
### 🔄 Auto-Refresh
- ✅ Automatische Aktualisierung alle 30 Sekunden
- ✅ Manueller Refresh-Button
---
## Sicherheit
### ✅ Implementiert:
- HTTP Basic Auth
- Admin-only Access (403 Unauthorized für Non-Admins)
- Audit-Logging aller Admin-Aktionen
- XSS-Protection (HTML-escaped)
### ⚠️ Für Production:
- HTTPS erforderlich (Basic Auth unsicher über HTTP!)
- Admin-Passwort ändern
- Logs regelmäßig archivieren
---
## Erweiterungen (Optional)
### Charts hinzufügen (Chart.js)
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<canvas id="requestsChart"></canvas>
<script>
const ctx = document.getElementById('requestsChart').getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
datasets: [{
label: 'Requests',
data: [12, 19, 3, 5, 2, 3, 7]
}]
}
});
</script>
```
### Weitere Admin-Features
- User-Management (neue Benutzer hinzufügen)
- Konfiguration Editor
- System-Monitor (CPU, RAM)
- Backup-Manager
- Audit-Trail Export
---
## Troubleshooting
### "403 Unauthorized" beim Öffnen von /admin
✓ Du musst als Admin angemeldet sein
✓ Login mit: `admin` / `admin_secure_pwd_2026`
### "No JSON object could be decoded" (in access_log.json)
✓ Lösche `access_log.json` und starte neu
✓ Die Datei wird automatisch neu erstellt
### Logs werden nicht angezeigt
✓ Prüfe, ob `access_log.json` im Projektverzeichnis existiert
✓ Öffne Browser Console (F12) auf Fehler
---
## API-Referenz
| Endpoint | Method | Auth | Beschreibung |
|----------|--------|------|-------------|
| `/admin` | GET | Admin | Admin-Dashboard |
| `/api/admin/stats` | GET | Admin | Statistiken (JSON) |
| `/api/admin/logs` | GET | Admin | Access-Logs (JSON) |
| `/api/admin/export` | GET | Admin | Export als JSON/CSV |
| `/api/admin/clear-logs` | POST | Admin | Alle Logs löschen |
---
## Nächste Schritte
- [ ] `admin.html` in `templates/` speichern
- [ ] Routes in `app.py` hinzufügen
- [ ] Server neustarten
- [ ] http://localhost:5000/admin testen
- [ ] Mit Admin-Credentials anmelden
- [ ] Logs prüfen
---
**Fertig!** 🎉 Dein Admin-Panel ist jetzt integriert!
@@ -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*
+146
View File
@@ -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*
+226
View File
@@ -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
+568
View File
@@ -0,0 +1,568 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#0066cc">
<title>Admin Panel – Roll Calculator</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: #f4f6f9;
color: #333;
min-height: 100vh;
}
/* ---- HEADER ---- */
.admin-header {
background: linear-gradient(135deg, #003366 0%, #0066cc 100%);
color: white;
padding: 20px 32px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-header h1 {
font-size: 24px;
font-weight: 700;
letter-spacing: 0.5px;
}
.admin-header .subtitle {
font-size: 12px;
opacity: 0.85;
}
.admin-header .user-info {
font-size: 12px;
opacity: 0.9;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.admin-header .user-info strong {
opacity: 1;
font-weight: 600;
}
/* ---- CONTAINER ---- */
.admin-container {
max-width: 1300px;
margin: 30px auto;
padding: 0 20px 60px;
}
/* ---- STATS GRID ---- */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
border-left: 4px solid #0066cc;
transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.stat-card.warning {
border-left-color: #f0b429;
}
.stat-card.danger {
border-left-color: #dc3545;
}
.stat-card .label {
font-size: 12px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.4px;
margin-bottom: 8px;
font-weight: 600;
}
.stat-card .value {
font-size: 36px;
font-weight: 700;
color: #0066cc;
line-height: 1;
}
.stat-card.warning .value {
color: #f0b429;
}
.stat-card.danger .value {
color: #dc3545;
}
.stat-card .sublabel {
font-size: 11px;
color: #999;
margin-top: 8px;
}
/* ---- LOGS SECTION ---- */
.logs-section {
background: white;
border-radius: 10px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 12px;
}
.logs-header h2 {
font-size: 18px;
color: #003366;
font-weight: 700;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
background: #0066cc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
transition: background 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.btn:hover {
background: #0052a3;
}
.btn.secondary {
background: #e0e0e0;
color: #333;
}
.btn.secondary:hover {
background: #d0d0d0;
}
.btn-refresh {
animation: spin 1s linear infinite;
}
.btn-refresh.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* ---- TABLE ---- */
.logs-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.logs-table thead {
background: #f0f4fa;
border-bottom: 2px solid #0066cc;
}
.logs-table th {
padding: 12px;
text-align: left;
font-weight: 600;
color: #333;
}
.logs-table td {
padding: 10px 12px;
border-bottom: 1px solid #e8ecf0;
}
.logs-table tbody tr {
transition: background 0.15s;
}
.logs-table tbody tr:hover {
background: #f8f9fa;
}
.status-200 {
color: #28a745;
font-weight: 600;
background: #f0f9f6;
padding: 2px 6px;
border-radius: 3px;
display: inline-block;
}
.status-401 {
color: #dc3545;
font-weight: 600;
background: #fff0f0;
padding: 2px 6px;
border-radius: 3px;
display: inline-block;
}
.status-403 {
color: #f0b429;
font-weight: 600;
background: #fffbea;
padding: 2px 6px;
border-radius: 3px;
display: inline-block;
}
/* ---- LOADING / EMPTY ---- */
.loading {
text-align: center;
padding: 40px;
color: #999;
}
.empty {
text-align: center;
padding: 40px;
color: #999;
font-style: italic;
}
.error {
background: #fff0f0;
color: #dc3545;
padding: 12px;
border-radius: 5px;
border-left: 4px solid #dc3545;
margin: 12px 0;
}
/* ---- FILTERS ---- */
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-input {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 13px;
}
.filter-input:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 2px rgba(0,102,204,0.1);
}
/* ---- FOOTER ---- */
.admin-footer {
text-align: center;
font-size: 11px;
color: #aaa;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e8ecf0;
}
/* ---- RESPONSIVE ---- */
@media (max-width: 768px) {
.admin-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.admin-header .user-info {
align-items: flex-start;
}
.stats-grid {
grid-template-columns: 1fr;
}
.logs-header {
flex-direction: column;
align-items: flex-start;
}
.controls {
width: 100%;
}
.btn {
flex: 1;
justify-content: center;
}
.logs-table {
font-size: 12px;
}
.logs-table th, .logs-table td {
padding: 8px;
}
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="admin-header">
<div>
<h1>🔐 Admin Panel</h1>
<div class="subtitle">Roll Calculator – Naue GmbH & Co. KG</div>
</div>
<div class="user-info">
<span>Angemeldet als:</span>
<strong id="authUser">admin</strong>
</div>
</div>
<!-- MAIN CONTAINER -->
<div class="admin-container">
<!-- STATISTICS -->
<div class="stats-grid" id="statsGrid">
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: #999;">
⏳ Statistiken werden geladen...
</div>
</div>
<!-- LOGS SECTION -->
<div class="logs-section">
<!-- HEADER -->
<div class="logs-header">
<h2>📋 Access Logs</h2>
<div class="controls">
<button class="btn" id="refreshBtn" onclick="refreshData()">
<span id="refreshIcon">🔄</span> Aktualisieren
</button>
<button class="btn secondary" onclick="downloadLogs()">
⬇️ Download
</button>
</div>
</div>
<!-- FILTERS -->
<div class="filters">
<input
type="text"
class="filter-input"
placeholder="Nach Benutzer filtern..."
id="filterUser"
onkeyup="filterLogs()"
>
<input
type="text"
class="filter-input"
placeholder="Nach Endpoint filtern..."
id="filterEndpoint"
onkeyup="filterLogs()"
>
</div>
<!-- TABLE -->
<div id="errorContainer"></div>
<table class="logs-table">
<thead>
<tr>
<th>Zeitstempel</th>
<th>Benutzer</th>
<th>Endpoint</th>
<th>Methode</th>
<th style="width: 80px;">Status</th>
</tr>
</thead>
<tbody id="logsList">
<tr>
<td colspan="5" class="loading">Logs werden geladen...</td>
</tr>
</tbody>
</table>
</div>
<!-- FOOTER -->
<div class="admin-footer">
ℹ️ Logs werden automatisch alle 30 Sekunden aktualisiert |
Letztes Update: <span id="lastUpdate">-</span>
</div>
</div>
<!-- SCRIPTS -->
<script>
let allLogs = [];
// Get authenticated user from API
async function getAuthUser() {
try {
const res = await fetch('/api/user');
const data = await res.json();
document.getElementById('authUser').textContent = data.username || 'admin';
} catch (e) {
console.warn('Konnte Benutzerinformation nicht laden');
}
}
// Refresh Data (Statistiken + Logs)
async function refreshData() {
const btn = document.getElementById('refreshBtn');
const icon = document.getElementById('refreshIcon');
try {
icon.style.animation = 'spin 1s linear infinite';
// Load Statistics
const statsRes = await fetch('/api/admin/stats');
if (!statsRes.ok) throw new Error('Statistiken konnten nicht geladen werden');
const stats = await statsRes.json();
// Load Logs
const logsRes = await fetch('/api/admin/logs');
if (!logsRes.ok) throw new Error('Logs konnten nicht geladen werden');
const logsData = await logsRes.json();
allLogs = logsData.logs || [];
// Render Statistics
renderStats(stats);
// Render Logs
renderLogs(allLogs);
// Update timestamp
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('de-DE');
// Clear error
document.getElementById('errorContainer').innerHTML = '';
} catch (error) {
console.error('Fehler beim Laden der Daten:', error);
document.getElementById('errorContainer').innerHTML =
'<div class="error">❌ ' + error.message + '</div>';
} finally {
icon.style.animation = 'none';
}
}
// Render Statistics
function renderStats(stats) {
const html = `
<div class="stat-card">
<div class="label">📊 Gesamte Anfragen</div>
<div class="value">${stats.total_requests || 0}</div>
</div>
<div class="stat-card">
<div class="label">👥 Eindeutige Benutzer</div>
<div class="value">${stats.unique_users || 0}</div>
</div>
<div class="stat-card danger">
<div class="label">⛔ Fehlgeschlagene Logins</div>
<div class="value">${stats.failed_logins || 0}</div>
</div>
`;
document.getElementById('statsGrid').innerHTML = html;
}
// Render Logs
function renderLogs(logs) {
if (!logs || logs.length === 0) {
document.getElementById('logsList').innerHTML =
'<tr><td colspan="5" class="empty">Keine Logs verfügbar</td></tr>';
return;
}
const html = logs.reverse().map(log => {
const time = new Date(log.timestamp).toLocaleString('de-DE');
const statusClass = `status-${log.status}`;
return `
<tr>
<td>${time}</td>
<td><strong>${log.username}</strong></td>
<td><code>${log.endpoint}</code></td>
<td>${log.method}</td>
<td><span class="${statusClass}">${log.status}</span></td>
</tr>
`;
}).join('');
document.getElementById('logsList').innerHTML = html;
}
// Filter Logs
function filterLogs() {
const userFilter = document.getElementById('filterUser').value.toLowerCase();
const endpointFilter = document.getElementById('filterEndpoint').value.toLowerCase();
const filtered = allLogs.filter(log =>
log.username.toLowerCase().includes(userFilter) &&
log.endpoint.toLowerCase().includes(endpointFilter)
);
renderLogs(filtered);
}
// Download Logs as JSON
function downloadLogs() {
const data = JSON.stringify(allLogs, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `access-logs-${new Date().toISOString()}.json`;
a.click();
window.URL.revokeObjectURL(url);
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
getAuthUser();
refreshData();
// Auto-refresh every 30 seconds
setInterval(refreshData, 30000);
});
</script>
</body>
</html>
+179
View File
@@ -0,0 +1,179 @@
"""
ADMIN ROUTES FÜR app.py
Diese Routes sollten zu app.py hinzugefügt werden (nach den Error Handlers, vor if __name__)
"""
# ============================================================================
# ADMIN ROUTES (diese zeilen in app.py hinzufügen!)
# ============================================================================
@app.route("/admin", methods=["GET"])
@auth.login_required
def admin_dashboard():
"""Admin Dashboard - nur für Admin-Benutzer"""
user = auth.current_user()
if user not in ADMIN_USERS:
log_access(user, "/admin", "GET", 403)
return jsonify({"error": "Unauthorized - Admin access required"}), 403
log_access(user, "/admin", "GET")
return render_template("admin.html")
@app.route("/api/admin/stats", methods=["GET"])
@auth.login_required
def get_admin_stats():
"""Statistiken für Admin Dashboard"""
user = auth.current_user()
if user not in ADMIN_USERS:
log_access(user, "/api/admin/stats", "GET", 403)
return jsonify({"error": "Unauthorized"}), 403
log_access(user, "/api/admin/stats", "GET")
logs = []
if os.path.exists(LOG_FILE):
try:
with open(LOG_FILE, "r") as f:
logs = json.load(f)
except json.JSONDecodeError:
logs = []
# Berechne Statistiken
total_requests = len(logs)
unique_users = len(set(log.get("username", "unknown") for log in logs))
failed_logins = len([l for l in logs if l.get("status") == 401])
return jsonify({
"total_requests": total_requests,
"unique_users": unique_users,
"failed_logins": failed_logins,
"timestamp": datetime.now().isoformat()
}), 200
@app.route("/api/admin/logs", methods=["GET"])
@auth.login_required
def get_admin_logs():
"""Access Logs für Admin Dashboard"""
user = auth.current_user()
if user not in ADMIN_USERS:
log_access(user, "/api/admin/logs", "GET", 403)
return jsonify({"error": "Unauthorized"}), 403
log_access(user, "/api/admin/logs", "GET")
logs = []
if os.path.exists(LOG_FILE):
try:
with open(LOG_FILE, "r") as f:
logs = json.load(f)
except json.JSONDecodeError:
logs = []
# Sortiere nach Zeitstempel absteigend (neueste zuerst)
logs_sorted = sorted(logs, key=lambda x: x.get("timestamp", ""), reverse=True)
# Optionales Limit (letzte 500 Logs)
return jsonify({"logs": logs_sorted[-500:]}), 200
@app.route("/api/admin/export", methods=["GET"])
@auth.login_required
def export_logs():
"""Exportiere Logs als CSV/JSON"""
user = auth.current_user()
if user not in ADMIN_USERS:
return jsonify({"error": "Unauthorized"}), 403
log_access(user, "/api/admin/export", "GET")
logs = []
if os.path.exists(LOG_FILE):
try:
with open(LOG_FILE, "r") as f:
logs = json.load(f)
except json.JSONDecodeError:
logs = []
# Optional: Als CSV exportieren
fmt = request.args.get("format", "json")
if fmt == "csv":
import csv
from io import StringIO
output = StringIO()
writer = csv.DictWriter(output, fieldnames=["timestamp", "username", "endpoint", "method", "status"])
writer.writeheader()
writer.writerows(logs)
response = app.make_response(output.getvalue())
response.headers["Content-Disposition"] = f"attachment; filename=logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
response.headers["Content-Type"] = "text/csv"
return response
else:
response = app.make_response(json.dumps(logs, indent=2))
response.headers["Content-Disposition"] = f"attachment; filename=logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
response.headers["Content-Type"] = "application/json"
return response
# ============================================================================
# OPTIONAL: Admin-Utility-Routes
# ============================================================================
@app.route("/api/admin/clear-logs", methods=["POST"])
@auth.login_required
def clear_logs():
"""Lösche alle Logs (nur für Admin, mit Bestätigung)"""
user = auth.current_user()
if user not in ADMIN_USERS:
return jsonify({"error": "Unauthorized"}), 403
# Safety: Verlange "confirm=true" Query Parameter
confirm = request.args.get("confirm") == "true"
if not confirm:
return jsonify({"error": "Confirmation required (add ?confirm=true)"}), 400
log_access(user, "/api/admin/clear-logs", "POST")
# Backup erstellen vor dem Löschen
if os.path.exists(LOG_FILE):
backup_file = LOG_FILE + f".backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
import shutil
shutil.copy(LOG_FILE, backup_file)
# Logs leeren
with open(LOG_FILE, "w") as f:
json.dump([], f)
return jsonify({
"status": "success",
"message": "Alle Logs wurden gelöscht (Backup erstellt)"
}), 200
@app.route("/api/admin/system-info", methods=["GET"])
@auth.login_required
def get_system_info():
"""System-Information für Admin"""
user = auth.current_user()
if user not in ADMIN_USERS:
return jsonify({"error": "Unauthorized"}), 403
log_access(user, "/api/admin/system-info", "GET")
import psutil
return jsonify({
"environment": os.getenv("FLASK_ENV", "production"),
"debug_mode": app.debug,
"database_file": LOG_FILE,
"database_size_kb": os.path.getsize(LOG_FILE) / 1024 if os.path.exists(LOG_FILE) else 0,
"timestamp": datetime.now().isoformat(),
# Optional: CPU/RAM Info
# "cpu_percent": psutil.cpu_percent(),
# "memory_percent": psutil.virtual_memory().percent,
}), 200
+167
View File
@@ -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
)
+68
View File
@@ -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}")
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Automatischer Fix für article-data.json Fetch-URLs in Flask
Ersetzt relative URLs durch /static/ URLs
"""
import re
import os
from pathlib import Path
def fix_html_file(html_path):
"""Ersetzt die alten Fetch-URLs durch die neuen Flask-URLs"""
if not os.path.exists(html_path):
print(f"❌ Datei nicht gefunden: {html_path}")
return False
print(f"📖 Öffne: {html_path}")
with open(html_path, 'r', encoding='utf-8') as f:
content = f.read()
original_content = content
changes = []
# Fix 1: fetch('article-data.json') → fetch('/static/article-data.json')
if "fetch('article-data.json')" in content or 'fetch("article-data.json")' in content:
content = content.replace("fetch('article-data.json')", "fetch('/static/article-data.json')")
content = content.replace('fetch("article-data.json")', 'fetch("/static/article-data.json")')
changes.append("✓ article-data.json URL aktualisiert")
# Fix 2: fetch('config.json') → fetch('/static/config.json')
if "fetch('config.json')" in content or 'fetch("config.json")' in content:
content = content.replace("fetch('config.json')", "fetch('/static/config.json')")
content = content.replace('fetch("config.json")', 'fetch("/static/config.json")')
changes.append("✓ config.json URL aktualisiert")
# Fix 3: register('service-worker.js') → register('/static/service-worker.js')
if "register('service-worker.js')" in content or 'register("service-worker.js")' in content:
content = content.replace("register('service-worker.js')", "register('/static/service-worker.js')")
content = content.replace('register("service-worker.js")', 'register("/static/service-worker.js")')
changes.append("✓ service-worker.js URL aktualisiert")
if content != original_content:
# Backup erstellen
backup_path = html_path + ".backup"
with open(backup_path, 'w', encoding='utf-8') as f:
f.write(original_content)
print(f"💾 Backup erstellt: {backup_path}")
# Neue Version speichern
with open(html_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✅ HTML aktualisiert: {html_path}")
for change in changes:
print(f" {change}")
return True
else:
print("⚠️ Keine Änderungen notwendig (URLs sind bereits korrekt)")
return False
def check_static_files():
"""Prüft ob die benötigten Dateien im static/ Ordner sind"""
print("\n🔍 Prüfe static/ Verzeichnis...")
files_to_check = ['article-data.json', 'config.json']
found = []
missing = []
for filename in files_to_check:
path = os.path.join('static', filename)
if os.path.exists(path):
size = os.path.getsize(path)
found.append(f" ✓ {filename} ({size} bytes)")
else:
missing.append(f" ❌ {filename} (NICHT GEFUNDEN)")
for msg in found:
print(msg)
for msg in missing:
print(msg)
return len(missing) == 0
if __name__ == "__main__":
print("=" * 60)
print("Fix für article-data.json Flask-URLs")
print("=" * 60)
html_file = "templates/roll_calculator.html"
success = fix_html_file(html_file)
files_ok = check_static_files()
print("\n" + "=" * 60)
if success or files_ok:
print("✅ Fix abgeschlossen!")
print("\n📝 Nächste Schritte:")
print(" 1. Browser neuladen (Ctrl+Shift+R)")
print(" 2. Öffne Browser Console (F12)")
print(" 3. Du solltest sehen:")
print(" 'Article data loaded: XXX items'")
print(" 'Config updated from file'")
else:
print("⚠️ Keine Änderungen nötig")
print("=" * 60)
+40
View File
@@ -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
+7
View File
@@ -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
+694
View File
@@ -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>
+694
View File
@@ -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