Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f621ba772b |
+2
-2
@@ -30,5 +30,5 @@ instance/
|
||||
node_modules/
|
||||
|
||||
# Backup files
|
||||
.bak
|
||||
.backup
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
@@ -1,694 +0,0 @@
|
||||
<!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>
|
||||
-241
@@ -1,241 +0,0 @@
|
||||
"""
|
||||
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
|
||||
"cniehues": "beta_test_2026", # Christian Niehues EDD
|
||||
"lvollmert": "Geheim!", # Lars Vollmert EDD
|
||||
"mtazl": "rollcalc", # Martin Tazl EDD
|
||||
"controlling": "beta2026" # Lars Krückemeier und Lea-Marie
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
# ============================================================================
|
||||
# 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
|
||||
|
||||
# ============================================================================
|
||||
# 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
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,226 +0,0 @@
|
||||
/**
|
||||
* SERVICE WORKER FOR NAUE ROLL CALCULATOR
|
||||
* ========================================
|
||||
* Caching strategy:
|
||||
* 1. CACHE_FIRST for static assets (CSS, JS, images)
|
||||
* 2. NETWORK_FIRST for API/data (with fallback to cache)
|
||||
* 3. STALE_WHILE_REVALIDATE for article data
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'rollcalc-v14-' + new Date().getTime();
|
||||
const STATIC_CACHE = 'rollcalc-static-v14';
|
||||
const DATA_CACHE = 'rollcalc-data-v14';
|
||||
|
||||
// Assets to cache on install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/static/article-data.json',
|
||||
'/static/config.json'
|
||||
];
|
||||
|
||||
// ===================================================================
|
||||
// INSTALL EVENT - Cache static assets
|
||||
// ===================================================================
|
||||
self.addEventListener('install', event => {
|
||||
console.log('[ServiceWorker] Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE)
|
||||
.then(cache => {
|
||||
console.log('[ServiceWorker] Caching static assets');
|
||||
return cache.addAll(STATIC_ASSETS).catch(err => {
|
||||
console.warn('[ServiceWorker] Some assets failed to cache:', err);
|
||||
// Don't fail installation if some assets can't be cached
|
||||
return Promise.resolve();
|
||||
});
|
||||
})
|
||||
.then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
// ===================================================================
|
||||
// ACTIVATE EVENT - Clean up old caches
|
||||
// ===================================================================
|
||||
self.addEventListener('activate', event => {
|
||||
console.log('[ServiceWorker] Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (cacheName !== STATIC_CACHE &&
|
||||
cacheName !== DATA_CACHE &&
|
||||
cacheName !== CACHE_NAME) {
|
||||
console.log('[ServiceWorker] Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
}).then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// ===================================================================
|
||||
// FETCH EVENT - Intelligent caching strategy
|
||||
// ===================================================================
|
||||
self.addEventListener('fetch', event => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip cross-origin requests
|
||||
if (url.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 1. JSON DATA (article-data.json, config.json)
|
||||
// STALE_WHILE_REVALIDATE: Use cache first, update in background
|
||||
// ---------------------------------------------------------------
|
||||
if (url.pathname.includes('.json')) {
|
||||
event.respondWith(
|
||||
caches.open(DATA_CACHE).then(cache => {
|
||||
return cache.match(request).then(response => {
|
||||
const fetchPromise = fetch(request).then(networkResponse => {
|
||||
// Update cache with fresh version
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
}).catch(() => {
|
||||
// Network failed, return cached version
|
||||
return response || cacheNotFound('JSON data not found');
|
||||
});
|
||||
|
||||
// Return cached response immediately, update in background
|
||||
return response || fetchPromise;
|
||||
});
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 2. HTML PAGES
|
||||
// NETWORK_FIRST: Try network, fall back to cache
|
||||
// ---------------------------------------------------------------
|
||||
if (request.method === 'GET' &&
|
||||
(url.pathname === '/' || url.pathname.endsWith('.html'))) {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then(response => {
|
||||
if (response && response.status === 200) {
|
||||
const cache_copy = response.clone();
|
||||
caches.open(STATIC_CACHE).then(cache => {
|
||||
cache.put(request, cache_copy);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match(request)
|
||||
.then(response => response || cacheNotFound('Page not found'));
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 3. STATIC ASSETS (CSS, JS, images)
|
||||
// CACHE_FIRST: Use cache if available, fall back to network
|
||||
// ---------------------------------------------------------------
|
||||
if (request.method === 'GET') {
|
||||
event.respondWith(
|
||||
caches.open(STATIC_CACHE).then(cache => {
|
||||
return cache.match(request).then(response => {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return fetch(request).then(networkResponse => {
|
||||
// Only cache successful responses
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
}).catch(() => {
|
||||
// Return offline placeholder
|
||||
if (request.destination === 'image') {
|
||||
return createPlaceholderImage();
|
||||
}
|
||||
return cacheNotFound('Asset not found');
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 4. POST/PUT/DELETE (API calls)
|
||||
// NETWORK_ONLY: Do not cache, but handle errors gracefully
|
||||
// ---------------------------------------------------------------
|
||||
if (['POST', 'PUT', 'DELETE'].includes(request.method)) {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.catch(() => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'You are offline. API calls are not available.',
|
||||
status: 'offline'
|
||||
}),
|
||||
{
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// ===================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ===================================================================
|
||||
|
||||
function cacheNotFound(message) {
|
||||
return new Response(
|
||||
`<html><body style="font-family: sans-serif; padding: 20px; color: #666;">
|
||||
<h2>Offline</h2>
|
||||
<p>${message}</p>
|
||||
<p style="font-size: 12px; color: #999;">Roll Calculator is offline or the resource is not cached.</p>
|
||||
</body></html>`,
|
||||
{
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'text/html'
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createPlaceholderImage() {
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<rect fill="#f0f0f0" width="100" height="100"/>
|
||||
<text x="50" y="50" text-anchor="middle" dy=".3em" fill="#999" font-size="12">
|
||||
Image offline
|
||||
</text>
|
||||
</svg>`;
|
||||
|
||||
return new Response(svg, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
'Cache-Control': 'public, max-age=86400'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// LOG SERVICE WORKER STATUS
|
||||
// ===================================================================
|
||||
console.log('[ServiceWorker] Naue Roll Calculator Service Worker loaded');
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user