364 lines
15 KiB
HTML
364 lines
15 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="de">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>Dashboard - Watchdog Docker</title>
|
||
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||
|
|
<style>
|
||
|
|
body {
|
||
|
|
background-color: #f5f6fa;
|
||
|
|
}
|
||
|
|
.navbar {
|
||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||
|
|
}
|
||
|
|
.navbar-brand {
|
||
|
|
font-weight: 600;
|
||
|
|
font-size: 24px;
|
||
|
|
}
|
||
|
|
.stats-card {
|
||
|
|
border: none;
|
||
|
|
border-radius: 10px;
|
||
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||
|
|
transition: transform 0.2s;
|
||
|
|
}
|
||
|
|
.stats-card:hover {
|
||
|
|
transform: translateY(-5px);
|
||
|
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||
|
|
}
|
||
|
|
.stats-icon {
|
||
|
|
font-size: 48px;
|
||
|
|
opacity: 0.8;
|
||
|
|
}
|
||
|
|
.events-table-card {
|
||
|
|
border: none;
|
||
|
|
border-radius: 10px;
|
||
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||
|
|
}
|
||
|
|
.badge-dhcp_lease {
|
||
|
|
background-color: #0d6efd;
|
||
|
|
}
|
||
|
|
.badge-new_device {
|
||
|
|
background-color: #dc3545;
|
||
|
|
}
|
||
|
|
.badge-interface_status {
|
||
|
|
background-color: #ffc107;
|
||
|
|
color: #000;
|
||
|
|
}
|
||
|
|
.badge-gateway_status {
|
||
|
|
background-color: #fd7e14;
|
||
|
|
}
|
||
|
|
.table-hover tbody tr:hover {
|
||
|
|
background-color: #f8f9fa;
|
||
|
|
}
|
||
|
|
.filter-section {
|
||
|
|
background-color: white;
|
||
|
|
padding: 15px;
|
||
|
|
border-radius: 10px;
|
||
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
.last-updated {
|
||
|
|
font-size: 12px;
|
||
|
|
color: #6c757d;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<!-- Navbar -->
|
||
|
|
<nav class="navbar navbar-dark mb-4">
|
||
|
|
<div class="container-fluid">
|
||
|
|
<span class="navbar-brand mb-0 h1">
|
||
|
|
<i class="bi bi-shield-check"></i> Watchdog Docker
|
||
|
|
</span>
|
||
|
|
<div class="d-flex align-items-center">
|
||
|
|
<span class="text-white me-3">
|
||
|
|
<i class="bi bi-person-circle"></i> {{ current_user.id }}
|
||
|
|
</span>
|
||
|
|
<a href="{{ url_for('logout') }}" class="btn btn-outline-light btn-sm">
|
||
|
|
<i class="bi bi-box-arrow-right"></i> Logout
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<div class="container-fluid">
|
||
|
|
<!-- Statistics Cards -->
|
||
|
|
<div class="row mb-4">
|
||
|
|
<div class="col-md-3 mb-3">
|
||
|
|
<div class="card stats-card">
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="d-flex justify-content-between align-items-center">
|
||
|
|
<div>
|
||
|
|
<h6 class="text-muted mb-1">Gesamt Events</h6>
|
||
|
|
<h2 class="mb-0">{{ stats.total_events }}</h2>
|
||
|
|
</div>
|
||
|
|
<div class="stats-icon text-primary">
|
||
|
|
<i class="bi bi-database"></i>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-3 mb-3">
|
||
|
|
<div class="card stats-card">
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="d-flex justify-content-between align-items-center">
|
||
|
|
<div>
|
||
|
|
<h6 class="text-muted mb-1">Heute</h6>
|
||
|
|
<h2 class="mb-0">{{ stats.events_today }}</h2>
|
||
|
|
</div>
|
||
|
|
<div class="stats-icon text-success">
|
||
|
|
<i class="bi bi-calendar-check"></i>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-3 mb-3">
|
||
|
|
<div class="card stats-card">
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="d-flex justify-content-between align-items-center">
|
||
|
|
<div>
|
||
|
|
<h6 class="text-muted mb-1">Letzte Stunde</h6>
|
||
|
|
<h2 class="mb-0">{{ stats.events_last_hour }}</h2>
|
||
|
|
</div>
|
||
|
|
<div class="stats-icon text-warning">
|
||
|
|
<i class="bi bi-clock-history"></i>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-3 mb-3">
|
||
|
|
<div class="card stats-card">
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="d-flex justify-content-between align-items-center">
|
||
|
|
<div>
|
||
|
|
<h6 class="text-muted mb-1">Bekannte Geräte</h6>
|
||
|
|
<h2 class="mb-0">{{ stats.known_devices }}</h2>
|
||
|
|
</div>
|
||
|
|
<div class="stats-icon text-info">
|
||
|
|
<i class="bi bi-router"></i>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Filter Section -->
|
||
|
|
<div class="filter-section">
|
||
|
|
<div class="row align-items-center">
|
||
|
|
<div class="col-md-3">
|
||
|
|
<label for="filterType" class="form-label mb-1">Event-Typ</label>
|
||
|
|
<select class="form-select form-select-sm" id="filterType">
|
||
|
|
<option value="">Alle Typen</option>
|
||
|
|
<option value="dhcp_lease">DHCP Lease</option>
|
||
|
|
<option value="new_device">Neues Gerät</option>
|
||
|
|
<option value="interface_status">Interface Status</option>
|
||
|
|
<option value="gateway_status">Gateway Status</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-3">
|
||
|
|
<label for="filterInterface" class="form-label mb-1">Interface</label>
|
||
|
|
<select class="form-select form-select-sm" id="filterInterface">
|
||
|
|
<option value="">Alle Interfaces</option>
|
||
|
|
{% for interface in config.monitoring.monitored_interfaces %}
|
||
|
|
<option value="{{ interface }}">{{ interface }}</option>
|
||
|
|
{% endfor %}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-3">
|
||
|
|
<label class="form-label mb-1">Auto-Refresh</label>
|
||
|
|
<div class="form-check form-switch">
|
||
|
|
<input class="form-check-input" type="checkbox" id="autoRefresh" checked>
|
||
|
|
<label class="form-check-label" for="autoRefresh">
|
||
|
|
Alle 10 Sekunden
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-3 text-end">
|
||
|
|
<label class="form-label mb-1 d-block">Aktualisierung</label>
|
||
|
|
<button class="btn btn-primary btn-sm" onclick="refreshEvents()">
|
||
|
|
<i class="bi bi-arrow-clockwise"></i> Jetzt aktualisieren
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Events Table -->
|
||
|
|
<div class="card events-table-card">
|
||
|
|
<div class="card-header bg-white">
|
||
|
|
<div class="d-flex justify-content-between align-items-center">
|
||
|
|
<h5 class="mb-0">
|
||
|
|
<i class="bi bi-list-ul"></i> Aktuelle Events
|
||
|
|
</h5>
|
||
|
|
<span class="last-updated" id="lastUpdated">
|
||
|
|
Zuletzt aktualisiert: {{ stats.last_updated if stats.last_updated else 'Nie' }}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="card-body p-0">
|
||
|
|
<div class="table-responsive">
|
||
|
|
<table class="table table-hover mb-0" id="eventsTable">
|
||
|
|
<thead class="table-light">
|
||
|
|
<tr>
|
||
|
|
<th style="width: 180px;">Zeitpunkt</th>
|
||
|
|
<th style="width: 150px;">Typ</th>
|
||
|
|
<th style="width: 100px;">Interface</th>
|
||
|
|
<th>Details</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="eventsTableBody">
|
||
|
|
{% for event in events %}
|
||
|
|
<tr data-type="{{ event.type }}" data-interface="{{ event.interface }}">
|
||
|
|
<td>
|
||
|
|
<small>{{ event.timestamp }}</small>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<span class="badge badge-{{ event.type }}">
|
||
|
|
{{ event.type.replace('_', ' ').upper() }}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<span class="badge bg-secondary">{{ event.interface or 'N/A' }}</span>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
{{ event.details }}
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
{% else %}
|
||
|
|
<tr>
|
||
|
|
<td colspan="4" class="text-center text-muted py-4">
|
||
|
|
<i class="bi bi-inbox"></i> Keine Events vorhanden
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
{% endfor %}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||
|
|
<script>
|
||
|
|
let autoRefreshInterval;
|
||
|
|
|
||
|
|
// Filter events
|
||
|
|
function filterEvents() {
|
||
|
|
const typeFilter = document.getElementById('filterType').value;
|
||
|
|
const interfaceFilter = document.getElementById('filterInterface').value;
|
||
|
|
const rows = document.querySelectorAll('#eventsTableBody tr');
|
||
|
|
|
||
|
|
rows.forEach(row => {
|
||
|
|
const type = row.getAttribute('data-type');
|
||
|
|
const iface = row.getAttribute('data-interface');
|
||
|
|
|
||
|
|
const typeMatch = !typeFilter || type === typeFilter;
|
||
|
|
const interfaceMatch = !interfaceFilter || iface === interfaceFilter;
|
||
|
|
|
||
|
|
if (typeMatch && interfaceMatch) {
|
||
|
|
row.style.display = '';
|
||
|
|
} else {
|
||
|
|
row.style.display = 'none';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Refresh events via AJAX
|
||
|
|
async function refreshEvents() {
|
||
|
|
try {
|
||
|
|
const typeFilter = document.getElementById('filterType').value;
|
||
|
|
const interfaceFilter = document.getElementById('filterInterface').value;
|
||
|
|
|
||
|
|
let url = '/api/events?limit=100';
|
||
|
|
if (typeFilter) url += `&type=${typeFilter}`;
|
||
|
|
if (interfaceFilter) url += `&interface=${interfaceFilter}`;
|
||
|
|
|
||
|
|
const response = await fetch(url);
|
||
|
|
const events = await response.json();
|
||
|
|
|
||
|
|
// Update table
|
||
|
|
const tbody = document.getElementById('eventsTableBody');
|
||
|
|
|
||
|
|
if (events.length === 0) {
|
||
|
|
tbody.innerHTML = `
|
||
|
|
<tr>
|
||
|
|
<td colspan="4" class="text-center text-muted py-4">
|
||
|
|
<i class="bi bi-inbox"></i> Keine Events vorhanden
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
} else {
|
||
|
|
tbody.innerHTML = events.map(event => `
|
||
|
|
<tr data-type="${event.type}" data-interface="${event.interface || ''}">
|
||
|
|
<td><small>${event.timestamp}</small></td>
|
||
|
|
<td>
|
||
|
|
<span class="badge badge-${event.type}">
|
||
|
|
${event.type.replace('_', ' ').toUpperCase()}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<span class="badge bg-secondary">${event.interface || 'N/A'}</span>
|
||
|
|
</td>
|
||
|
|
<td>${event.details}</td>
|
||
|
|
</tr>
|
||
|
|
`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update timestamp
|
||
|
|
const now = new Date();
|
||
|
|
document.getElementById('lastUpdated').textContent =
|
||
|
|
`Zuletzt aktualisiert: ${now.toLocaleString('de-DE')}`;
|
||
|
|
|
||
|
|
// Refresh stats
|
||
|
|
const statsResponse = await fetch('/api/stats');
|
||
|
|
const stats = await statsResponse.json();
|
||
|
|
updateStats(stats);
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error refreshing events:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update statistics
|
||
|
|
function updateStats(stats) {
|
||
|
|
const statsElements = document.querySelectorAll('.stats-card h2');
|
||
|
|
if (statsElements.length >= 4) {
|
||
|
|
statsElements[0].textContent = stats.total_events || 0;
|
||
|
|
statsElements[1].textContent = stats.events_today || 0;
|
||
|
|
statsElements[2].textContent = stats.events_last_hour || 0;
|
||
|
|
statsElements[3].textContent = stats.known_devices || 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auto-refresh toggle
|
||
|
|
function toggleAutoRefresh() {
|
||
|
|
const checkbox = document.getElementById('autoRefresh');
|
||
|
|
|
||
|
|
if (checkbox.checked) {
|
||
|
|
// Start auto-refresh every 10 seconds
|
||
|
|
autoRefreshInterval = setInterval(refreshEvents, 10000);
|
||
|
|
} else {
|
||
|
|
// Stop auto-refresh
|
||
|
|
if (autoRefreshInterval) {
|
||
|
|
clearInterval(autoRefreshInterval);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Event listeners
|
||
|
|
document.getElementById('filterType').addEventListener('change', filterEvents);
|
||
|
|
document.getElementById('filterInterface').addEventListener('change', filterEvents);
|
||
|
|
document.getElementById('autoRefresh').addEventListener('change', toggleAutoRefresh);
|
||
|
|
|
||
|
|
// Start auto-refresh on page load
|
||
|
|
toggleAutoRefresh();
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|