Files
RollCalculator/With Authentification/admin_e57c18.html
T

569 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>