first commit

This commit is contained in:
2026-07-30 16:26:21 +02:00
commit 9e32a4d786
33 changed files with 3475 additions and 0 deletions
+238
View File
@@ -0,0 +1,238 @@
<?php
use SyslogDash\SyslogQuery;
use SyslogDash\GeoIP;
$sq = new SyslogQuery();
$period = (int)($_GET['period'] ?? 30);
if (!in_array($period, [7, 30, 90, 365, 0])) $period = 30;
$report = $_GET['report'] ?? 'daily';
$allowedReports = ['daily', 'hosts', 'countries', 'severity', 'tags', 'hourly', 'custom'];
if (!in_array($report, $allowedReports)) $report = 'daily';
$chartData = [];
$chartType = 'bar';
$chartLabels = [];
$chartValues = [];
$chartLabel = '';
$extraRows = [];
switch ($report) {
case 'daily':
$chartData = $sq->getDailyStats($period);
$chartLabels = array_map(fn($d) => date('d-m', strtotime($d['date'])), $chartData);
$chartValues = array_column($chartData, 'cnt');
$chartLabel = 'Berichten per dag';
$chartType = 'bar';
$extraRows = $chartData;
break;
case 'hosts':
$chartData = $sq->getHostStats($period);
$chartLabels = array_column($chartData, 'FromHost');
$chartValues = array_column($chartData, 'cnt');
$chartLabel = 'Top hosts';
$chartType = 'bar';
break;
case 'countries':
$chartData = $sq->getCountryStats($period);
$chartLabels = array_map(fn($c) => GeoIP::getCountryName($c['code']) . ' (' . $c['code'] . ')', $chartData);
$chartValues = array_column($chartData, 'count');
$chartLabel = 'Berichten per land';
$chartType = 'bar';
break;
case 'severity':
$chartData = $sq->getSeverityStats($period);
$chartLabels = array_map(fn($s) => SyslogQuery::severityName((int)$s['Priority']), $chartData);
$chartValues = array_column($chartData, 'cnt');
$chartLabel = 'Severity verdeling';
$chartType = 'doughnut';
break;
case 'tags':
$chartData = $sq->getTagStats($period);
$chartLabels = array_column($chartData, 'SysLogTag');
$chartValues = array_column($chartData, 'cnt');
$chartLabel = 'Top tags';
$chartType = 'bar';
break;
case 'hourly':
$db = \SyslogDash\Database::getInstance();
$table = Database::table();
$stmt = $db->prepare("
SELECT HOUR(`ReceivedAt`) AS h, COUNT(*) AS cnt
FROM `{$table}`
WHERE `ReceivedAt` >= :since
GROUP BY HOUR(`ReceivedAt`)
ORDER BY h ASC
");
$stmt->execute([':since' => date('Y-m-d H:i:s', strtotime("-{$period} days"))]);
$chartData = $stmt->fetchAll();
$hourlyData = array_fill(0, 24, 0);
foreach ($chartData as $r) $hourlyData[(int)$r['h']] = (int)$r['cnt'];
$chartLabels = array_map(fn($h) => sprintf('%02d:00', $h), range(0, 23));
$chartValues = $hourlyData;
$chartLabel = 'Berichten per uur';
$chartType = 'line';
break;
case 'custom':
$sql = $_POST['sql'] ?? '';
$customResult = null;
if ($sql && preg_match('/^\s*SELECT/i', $sql)) {
$customResult = $sq->runQuery($sql);
}
break;
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0"><i class="bi bi-bar-chart-line me-2"></i>BI Rapporten</h2>
<div class="btn-group btn-group-sm">
<a href="?page=bi&period=7&report=<?= $report ?>" class="btn btn-outline-primary <?= $period === 7 ? 'active' : '' ?>">7d</a>
<a href="?page=bi&period=30&report=<?= $report ?>" class="btn btn-outline-primary <?= $period === 30 ? 'active' : '' ?>">30d</a>
<a href="?page=bi&period=90&report=<?= $report ?>" class="btn btn-outline-primary <?= $period === 90 ? 'active' : '' ?>">90d</a>
<a href="?page=bi&period=365&report=<?= $report ?>" class="btn btn-outline-primary <?= $period === 365 ? 'active' : '' ?>">365d</a>
<a href="?page=bi&period=0&report=<?= $report ?>" class="btn btn-outline-primary <?= $period === 0 ? 'active' : '' ?>">Alles</a>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-2">
<div class="list-group list-group-sm">
<a href="?page=bi&period=<?= $period ?>&report=daily" class="list-group-item list-group-item-action py-2 <?= $report === 'daily' ? 'active' : '' ?>">
<i class="bi bi-calendar me-2"></i>Dagelijks
</a>
<a href="?page=bi&period=<?= $period ?>&report=hourly" class="list-group-item list-group-item-action py-2 <?= $report === 'hourly' ? 'active' : '' ?>">
<i class="bi bi-clock me-2"></i>Per uur
</a>
<a href="?page=bi&period=<?= $period ?>&report=hosts" class="list-group-item list-group-item-action py-2 <?= $report === 'hosts' ? 'active' : '' ?>">
<i class="bi bi-hdd-network me-2"></i>Top hosts
</a>
<a href="?page=bi&period=<?= $period ?>&report=countries" class="list-group-item list-group-item-action py-2 <?= $report === 'countries' ? 'active' : '' ?>">
<i class="bi bi-globe me-2"></i>Landen
</a>
<a href="?page=bi&period=<?= $period ?>&report=severity" class="list-group-item list-group-item-action py-2 <?= $report === 'severity' ? 'active' : '' ?>">
<i class="bi bi-exclamation-triangle me-2"></i>Severity
</a>
<a href="?page=bi&period=<?= $period ?>&report=tags" class="list-group-item list-group-item-action py-2 <?= $report === 'tags' ? 'active' : '' ?>">
<i class="bi bi-tags me-2"></i>Tags
</a>
<a href="?page=bi&period=<?= $period ?>&report=custom" class="list-group-item list-group-item-action py-2 <?= $report === 'custom' ? 'active' : '' ?>">
<i class="bi bi-code-slash me-2"></i>Custom SQL
</a>
</div>
</div>
<div class="col-md-10">
<?php if ($report === 'custom'): ?>
<div class="card shadow-sm">
<div class="card-header bg-white"><i class="bi bi-code-slash me-2"></i>Custom SQL Query</div>
<div class="card-body">
<form method="POST" action="?page=bi&period=<?= $period ?>&report=custom">
<div class="mb-3">
<textarea name="sql" class="form-control font-monospace" rows="4" placeholder="SELECT COUNT(*), FromHost FROM SystemEvents GROUP BY FromHost ORDER BY 1 DESC LIMIT 10"><?= htmlspecialchars($_POST['sql'] ?? '') ?></textarea>
</div>
<button type="submit" class="btn btn-primary"><i class="bi bi-play me-1"></i>Uitvoeren</button>
</form>
<?php if (isset($customResult)): ?>
<?php if ($customResult['error']): ?>
<div class="alert alert-danger mt-3"><?= htmlspecialchars($customResult['error']) ?></div>
<?php elseif (!empty($customResult['rows'])): ?>
<div class="table-responsive mt-3" style="max-height:500px">
<table class="table table-sm table-hover table-bordered small mb-0">
<thead class="table-light">
<tr>
<?php foreach (array_keys($customResult['rows'][0]) as $col): ?>
<th><?= htmlspecialchars($col) ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($customResult['rows'] as $r): ?>
<tr>
<?php foreach ($r as $v): ?>
<td><?= htmlspecialchars(mb_substr((string)$v, 0, 200)) ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<small class="text-muted mt-2 d-block"><?= count($customResult['rows']) ?> rijen</small>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<div class="card shadow-sm mb-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span><i class="bi bi-graph-up me-2"></i><?= $chartLabel ?></span>
<div>
<a href="?page=export&report=<?= $report ?>&period=<?= $period ?>" class="btn btn-sm btn-outline-success"><i class="bi bi-download"></i> CSV</a>
</div>
</div>
<div class="card-body">
<canvas id="biChart" height="300"></canvas>
</div>
</div>
<?php if ($report === 'daily' && !empty($extraRows)): ?>
<div class="card shadow-sm">
<div class="card-header bg-white"><i class="bi bi-table me-2"></i>Dagelijkse gegevens</div>
<div class="card-body p-0">
<div class="table-responsive" style="max-height:400px">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr><th>Datum</th><th>Berichten</th><th>Hosts</th></tr>
</thead>
<tbody>
<?php foreach ($extraRows as $r): ?>
<tr>
<td><?= date('d-m-Y', strtotime($r['date'])) ?></td>
<td><?= number_format((int)$r['cnt'], 0, ',', '.') ?></td>
<td><?= $r['hosts'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php if ($report !== 'custom' && !empty($chartLabels)): ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
var ctx = document.getElementById('biChart');
if (!ctx) return;
var config = {
type: '<?= $chartType ?>',
data: {
labels: <?= json_encode($chartLabels) ?>,
datasets: [{
label: '<?= $chartLabel ?>',
data: <?= json_encode($chartValues) ?>,
backgroundColor: <?= $chartType === 'doughnut'
? "['#dc3545','#ffc107','#0dcaf0','#198754','#6f42c1','#fd7e14','#20c997','#0d6efd']"
: "'#0d6efd'" ?>,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top', labels: { boxWidth: 12 } } },
scales: <?= $chartType !== 'doughnut' ? "{ y: { beginAtZero: true } }" : "{}" ?>
}
};
new Chart(ctx, config);
});
</script>
<?php endif; ?>
<?php $sq = null; ?>
+163
View File
@@ -0,0 +1,163 @@
<?php
use SyslogDash\SyslogQuery;
use SyslogDash\GeoIP;
$sq = new SyslogQuery();
$kpi = $sq->getKPIStats(30);
$daily = $sq->getDailyStats(30);
$hostStats = $sq->getHostStats(30);
$sevStats = $sq->getSeverityStats(30);
$tagStats = $sq->getTagStats(30);
$topCountry = $kpi['topCountry'];
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Dashboard</h2>
<span class="text-muted small">Laatste 30 dagen</span>
</div>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-primary-subtle text-primary"><i class="bi bi-envelope"></i></div>
<div class="stat-body">
<div class="stat-value"><?= number_format($kpi['total'], 0, ',', '.') ?></div>
<div class="stat-label">Log berichten</div>
<small class="text-muted"><?= number_format($kpi['avgPerDay'], 0, ',', '.') ?>/dag</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-success-subtle text-success"><i class="bi bi-hdd-network"></i></div>
<div class="stat-body">
<div class="stat-value"><?= $kpi['uniqueHosts'] ?></div>
<div class="stat-label">Unieke hosts</div>
<small class="text-muted">apparaten</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-info-subtle text-info"><i class="bi bi-globe"></i></div>
<div class="stat-body">
<div class="stat-value"><?php if ($topCountry): ?><?= GeoIP::getCountryFlagEmoji($topCountry['code']) ?> <?= GeoIP::getCountryName($topCountry['code']) ?><?php else: ?>—<?php endif; ?></div>
<div class="stat-label">Grootste land</div>
<small class="text-muted"><?= $topCountry ? number_format($topCountry['count'], 0, ',', '.') . ' berichten' : '' ?></small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-warning-subtle text-warning"><i class="bi bi-clock-history"></i></div>
<div class="stat-body">
<div class="stat-value"><?= number_format($kpi['totalAll'], 0, ',', '.') ?></div>
<div class="stat-label">Totaal (all-time)</div>
<small class="text-muted">sinds begin</small>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-lg-8">
<div class="card shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span><i class="bi bi-graph-up me-2"></i>Berichten per dag</span>
</div>
<div class="card-body">
<canvas id="dailyChart" height="200"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white"><i class="bi bi-pie-chart me-2"></i>Severity</div>
<div class="card-body">
<canvas id="sevChart" height="200"></canvas>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-white"><i class="bi bi-hdd-network me-2"></i>Top hosts</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
<?php $maxHost = max(array_column($hostStats, 'cnt')); ?>
<?php foreach ($hostStats as $h): ?>
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-1">
<a href="?page=logs&host=<?= urlencode($h['FromHost']) ?>" class="text-decoration-none"><?= htmlspecialchars($h['FromHost']) ?></a>
<span class="badge bg-primary rounded-pill"><?= number_format($h['cnt'], 0, ',', '.') ?></span>
</div>
<div class="progress" style="height:4px">
<div class="progress-bar" style="width:<?= $maxHost > 0 ? ($h['cnt']/$maxHost*100) : 0 ?>%"></div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-white"><i class="bi bi-tags me-2"></i>Top tags</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
<?php $maxTag = max(array_column($tagStats, 'cnt')); ?>
<?php foreach ($tagStats as $t): ?>
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-muted small font-monospace"><?= htmlspecialchars($t['SysLogTag'] ?? '—') ?></span>
<span class="badge bg-secondary rounded-pill"><?= number_format($t['cnt'], 0, ',', '.') ?></span>
</div>
<div class="progress" style="height:4px">
<div class="progress-bar bg-info" style="width:<?= $maxTag > 0 ? ($t['cnt']/$maxTag*100) : 0 ?>%"></div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
new Chart(document.getElementById('dailyChart'), {
type: 'bar',
data: {
labels: <?= json_encode(array_map(fn($d) => date('d-m', strtotime($d['date'])), $daily)) ?>,
datasets: [
{ label: 'Berichten', data: <?= json_encode(array_column($daily, 'cnt')) ?>, backgroundColor: '#0d6efd', borderRadius: 2 },
{ label: 'Hosts', data: <?= json_encode(array_column($daily, 'hosts')) ?>, backgroundColor: '#6ea8fe', borderRadius: 2 }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top', labels: { boxWidth: 12 } } },
scales: { y: { beginAtZero: true } }
}
});
new Chart(document.getElementById('sevChart'), {
type: 'doughnut',
data: {
labels: <?= json_encode(array_map(fn($s) => SyslogQuery::severityName((int)$s['Priority']), $sevStats)) ?>,
datasets: [{
data: <?= json_encode(array_column($sevStats, 'cnt')) ?>,
backgroundColor: ['#dc3545','#dc3545','#dc3545','#ffc107','#ffc107','#0dcaf0','#6c757d','#6c757d']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'right', labels: { boxWidth: 12, font: { size: 11 } } } }
}
});
});
</script>
<?php $sq = null; // close PDO connection ?>
+74
View File
@@ -0,0 +1,74 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use SyslogDash\SyslogQuery;
use SyslogDash\GeoIP;
$sq = new SyslogQuery();
$format = $_GET['format'] ?? 'csv';
$period = (int)($_GET['period'] ?? 30);
if (!in_array($period, [7, 30, 90, 365, 0])) $period = 30;
$report = $_GET['report'] ?? 'logs';
$filename = 'syslog-export-' . date('Ymd') . '.' . $format;
switch ($report) {
case 'logs':
$filters = [];
foreach (['host','country','tag','facility','priority','search','from','to'] as $k) {
if (!empty($_GET[$k])) $filters[$k] = $_GET[$k];
}
$filters['sort'] = 'ASC';
$result = $sq->getLogs($filters, 1, 100000);
$data = $result['rows'];
$fields = ['ID','ReceivedAt','FromHost','Facility','Priority','SysLogTag','Message'];
break;
case 'daily':
$data = $sq->getDailyStats($period);
$fields = ['Datum','Berichten','Hosts'];
break;
case 'hosts':
$data = $sq->getHostStats($period);
$fields = ['Host','Aantal'];
break;
case 'countries':
$data = $sq->getCountryStats($period);
$fields = ['Code','Aantal'];
break;
case 'severity':
$data = $sq->getSeverityStats($period);
$fields = ['Priority','Severity','Aantal'];
array_walk($data, fn(&$r) => $r['SeverityName'] = SyslogQuery::severityName((int)$r['Priority']));
$fields = ['Priority','SeverityName','Aantal'];
break;
default:
$data = [];
$fields = [];
}
if ($format === 'json') {
header('Content-Type: application/json; charset=utf-8');
header("Content-Disposition: attachment; filename=\"{$filename}\"");
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
header('Content-Type: text/csv; charset=utf-8');
header("Content-Disposition: attachment; filename=\"{$filename}\"");
echo "\xEF\xBB\xBF"; // BOM for Excel UTF-8
$out = fopen('php://output', 'w');
fputcsv($out, $fields, ';');
foreach ($data as $row) {
$line = [];
foreach ($fields as $f) {
$line[] = $row[$f] ?? '';
}
fputcsv($out, $line, ';');
}
fclose($out);
+148
View File
@@ -0,0 +1,148 @@
<?php
use SyslogDash\SyslogQuery;
$sq = new SyslogQuery();
$filters = [
'host' => $_GET['host'] ?? '',
'country' => $_GET['country'] ?? '',
'tag' => $_GET['tag'] ?? '',
'facility' => $_GET['facility'] ?? '',
'priority' => $_GET['priority'] ?? '',
'search' => $_GET['search'] ?? '',
'from' => $_GET['from'] ?? '',
'to' => $_GET['to'] ?? '',
'sort' => $_GET['sort'] ?? 'DESC',
'sort_col' => $_GET['sort_col'] ?? 'ReceivedAt',
];
$filters = array_filter($filters, fn($v) => $v !== '');
$page = max(1, (int)($_GET['p'] ?? 1));
$result = $sq->getLogs($filters, $page, 50);
$hosts = $sq->getDistinct('FromHost');
$facilities = $sq->getDistinct('Facility');
$priorities = $sq->getDistinct('Priority');
function sortUrl($col, $filters, $currentSort, $currentCol) {
$dir = 'ASC';
if ($currentCol === $col && $currentSort === 'ASC') $dir = 'DESC';
$params = array_merge($filters, ['sort_col' => $col, 'sort' => $dir, 'p' => 1]);
return '?page=logs&' . http_build_query($params);
}
function filterUrl($filters, $override) {
$params = array_merge($filters, $override, ['p' => 1]);
return '?page=logs&' . http_build_query($params);
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0"><i class="bi bi-list-ul me-2"></i>Logs</h2>
<div>
<a href="?page=export&<?= http_build_query($_GET) ?>" class="btn btn-sm btn-outline-success me-1"><i class="bi bi-download"></i> CSV</a>
<a href="?page=export&format=json&<?= http_build_query($_GET) ?>" class="btn btn-sm btn-outline-info"><i class="bi bi-download"></i> JSON</a>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header bg-white"><i class="bi bi-funnel me-2"></i>Filters</div>
<div class="card-body">
<form method="GET" action="" class="row g-3">
<input type="hidden" name="page" value="logs">
<div class="col-md-2">
<label class="form-label small">Host</label>
<select name="host" class="form-select form-select-sm">
<option value="">Alle</option>
<?php foreach ($hosts as $h): ?>
<option value="<?= htmlspecialchars($h) ?>" <?= ($filters['host'] ?? '') === $h ? 'selected' : '' ?>><?= htmlspecialchars($h) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small">Facility</label>
<select name="facility" class="form-select form-select-sm">
<option value="">Alle</option>
<?php foreach ($facilities as $f): ?>
<option value="<?= $f ?>" <?= ($filters['facility'] ?? '') === (string)$f ? 'selected' : '' ?>><?= SyslogQuery::facilityName((int)$f) ?> (<?= $f ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small">Severity</label>
<select name="priority" class="form-select form-select-sm">
<option value="">Alle</option>
<?php foreach ($priorities as $p): ?>
<option value="<?= $p ?>" <?= ($filters['priority'] ?? '') === (string)$p ? 'selected' : '' ?>><?= SyslogQuery::severityName((int)$p) ?> (<?= $p ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small">Vanaf</label>
<input type="date" name="from" class="form-control form-control-sm" value="<?= htmlspecialchars($filters['from'] ?? '') ?>">
</div>
<div class="col-md-2">
<label class="form-label small">Tot</label>
<input type="date" name="to" class="form-control form-control-sm" value="<?= htmlspecialchars($filters['to'] ?? '') ?>">
</div>
<div class="col-md-2">
<label class="form-label small">Zoeken</label>
<input type="text" name="search" class="form-control form-control-sm" placeholder="zoek in bericht..." value="<?= htmlspecialchars($filters['search'] ?? '') ?>">
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-search me-1"></i>Filteren</button>
<a href="?page=logs" class="btn btn-outline-secondary btn-sm"><i class="bi bi-x"></i>Wissen</a>
<small class="text-muted ms-2"><?= number_format($result['total'], 0, ',', '.') ?> resultaten (pagina <?= $result['page'] ?>/<?= $result['pages'] ?>)</small>
</div>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0" style="font-size:0.85rem;">
<thead class="table-light">
<tr>
<th><a href="<?= sortUrl('ReceivedAt', $filters, $filters['sort'] ?? 'DESC', $filters['sort_col'] ?? 'ReceivedAt') ?>" class="text-decoration-none text-dark">Ontvangen <?= ($filters['sort_col'] ?? 'ReceivedAt') === 'ReceivedAt' ? (($filters['sort'] ?? 'DESC') === 'ASC' ? '↑' : '↓') : '' ?></a></th>
<th><a href="<?= sortUrl('FromHost', $filters, $filters['sort'] ?? 'DESC', $filters['sort_col'] ?? 'ReceivedAt') ?>" class="text-decoration-none text-dark">Host <?= ($filters['sort_col'] ?? '') === 'FromHost' ? (($filters['sort'] ?? 'DESC') === 'ASC' ? '↑' : '↓') : '' ?></a></th>
<th>Facility</th>
<th>Severity</th>
<th>Tag</th>
<th>Bericht</th>
</tr>
</thead>
<tbody>
<?php foreach ($result['rows'] as $row): ?>
<tr>
<td class="text-nowrap small"><?= date('d-m-Y H:i:s', strtotime($row['ReceivedAt'])) ?></td>
<td class="text-nowrap">
<a href="?page=logs&host=<?= urlencode($row['FromHost']) ?>" class="text-decoration-none"><?= htmlspecialchars($row['FromHost']) ?></a>
</td>
<td><span class="badge bg-light text-dark"><?= SyslogQuery::facilityName((int)$row['Facility']) ?></span></td>
<td><span class="badge bg-<?= SyslogQuery::severityClass((int)$row['Priority']) ?>"><?= SyslogQuery::severityName((int)$row['Priority']) ?></span></td>
<td class="font-monospace small text-muted"><?= htmlspecialchars($row['SysLogTag'] ?? '') ?></td>
<td style="max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="<?= htmlspecialchars($row['Message']) ?>"><?= htmlspecialchars(mb_substr($row['Message'] ?? '', 0, 120)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php if ($result['pages'] > 1): ?>
<nav class="mt-3">
<ul class="pagination pagination-sm justify-content-center">
<li class="page-item <?= $result['page'] <= 1 ? 'disabled' : '' ?>">
<a class="page-link" href="<?= filterUrl($filters, ['p' => $result['page'] - 1]) ?>">Vorige</a>
</li>
<?php for ($i = 1; $i <= $result['pages']; $i++): ?>
<li class="page-item <?= $i === $result['page'] ? 'active' : '' ?>">
<a class="page-link" href="<?= filterUrl($filters, ['p' => $i]) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
<li class="page-item <?= $result['page'] >= $result['pages'] ? 'disabled' : '' ?>">
<a class="page-link" href="<?= filterUrl($filters, ['p' => $result['page'] + 1]) ?>">Volgende</a>
</li>
</ul>
</nav>
<?php endif; ?>
<?php $sq = null; ?>
+222
View File
@@ -0,0 +1,222 @@
<?php
use SyslogDash\SyslogQuery;
use SyslogDash\GeoIP;
$period = (int)($_GET['period'] ?? 30);
if (!in_array($period, [7, 30, 90, 365, 0])) $period = 30;
$sq = new SyslogQuery();
$countryStats = $sq->getCountryStats($period);
$daily = $sq->getDailyStats($period);
$totalLogs = array_sum(array_column($countryStats, 'count'));
$mapCountries = [];
foreach ($countryStats as $c) {
$mapCountries[$c['code']] = $c['count'];
}
$drillCountry = $_GET['country'] ?? null;
$drillHosts = [];
if ($drillCountry) {
foreach ($countryStats as $c) {
if (strtoupper($c['code']) === strtoupper($drillCountry)) {
$drillHosts = $c['hosts'];
break;
}
}
}
$svgPath = __DIR__ . '/assets/img/world-map.svg';
$svgContent = '';
if (file_exists($svgPath)) {
$svgContent = file_get_contents($svgPath);
$styles = '';
$maxCountry = count($countryStats) > 0 ? $countryStats[0]['count'] : 1;
foreach ($countryStats as $c) {
if ($c['code'] === 'UNKNOWN') continue;
$ratio = $c['count'] / $maxCountry;
if ($ratio > 0.66) $fill = '#052c65';
elseif ($ratio > 0.33) $fill = '#0d6efd';
elseif ($ratio > 0.1) $fill = '#6ea8fe';
else $fill = '#cfe2ff';
$styles .= "#{$c['code']} { fill: {$fill}; }";
}
$svgContent = str_replace('</style>', $styles . '</style>', $svgContent);
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0"><i class="bi bi-globe2 me-2"></i>Wereldkaart</h2>
<div class="btn-group btn-group-sm">
<a href="?page=worldmap&period=7" class="btn btn-outline-primary <?= $period === 7 ? 'active' : '' ?>">7 dagen</a>
<a href="?page=worldmap&period=30" class="btn btn-outline-primary <?= $period === 30 ? 'active' : '' ?>">30 dagen</a>
<a href="?page=worldmap&period=90" class="btn btn-outline-primary <?= $period === 90 ? 'active' : '' ?>">90 dagen</a>
<a href="?page=worldmap&period=365" class="btn btn-outline-primary <?= $period === 365 ? 'active' : '' ?>">365 dagen</a>
<a href="?page=worldmap&period=0" class="btn btn-outline-primary <?= $period === 0 ? 'active' : '' ?>">Alles</a>
</div>
</div>
<?php if ($drillCountry): ?>
<div class="alert alert-info d-flex justify-content-between align-items-center">
<span><i class="bi bi-funnel me-2"></i>Gefilterd op land: <strong><?= GeoIP::getCountryFlagEmoji($drillCountry) ?> <?= GeoIP::getCountryName($drillCountry) ?></strong></span>
<a href="?page=worldmap&period=<?= $period ?>" class="btn btn-sm btn-outline-danger"><i class="bi bi-x"></i> Filter wissen</a>
</div>
<?php endif; ?>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-primary-subtle text-primary"><i class="bi bi-globe"></i></div>
<div class="stat-body">
<div class="stat-value"><?= count($countryStats) ?></div>
<div class="stat-label">Landen</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-success-subtle text-success"><i class="bi bi-envelope"></i></div>
<div class="stat-body">
<div class="stat-value"><?= number_format($totalLogs, 0, ',', '.') ?></div>
<div class="stat-label">Berichten</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-info-subtle text-info"><i class="bi bi-question-circle"></i></div>
<div class="stat-body">
<div class="stat-value"><?= $countryStats[0]['code'] === 'UNKNOWN' ? number_format($countryStats[0]['count'], 0, ',', '.') : '0' ?></div>
<div class="stat-label">Onbekend (geen GeoIP)</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-warning-subtle text-warning"><i class="bi bi-hdd-network"></i></div>
<div class="stat-body">
<div class="stat-value"><?= $sq->getUniqueHosts($period) ?></div>
<div class="stat-label">Unieke hosts</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span><i class="bi bi-map me-2"></i>Wereldkaart</span>
<small class="text-muted">
<span class="d-inline-block me-1" style="width:14px;height:14px;background:#cfe2ff;border-radius:2px;vertical-align:middle"></span> weinig
<span class="d-inline-block mx-1" style="width:14px;height:14px;background:#6ea8fe;border-radius:2px;vertical-align:middle"></span> gemiddeld
<span class="d-inline-block mx-1" style="width:14px;height:14px;background:#0d6efd;border-radius:2px;vertical-align:middle"></span> veel
<span class="d-inline-block mx-1" style="width:14px;height:14px;background:#052c65;border-radius:2px;vertical-align:middle"></span> meest
</small>
</div>
<div class="card-body p-3 position-relative world-map-wrapper">
<?php if ($svgContent): ?>
<div id="worldMapContainer" style="max-width:1000px;margin:0 auto;cursor:pointer;">
<?= $svgContent ?>
</div>
<div id="mapTooltip" class="map-tooltip" style="display:none;"></div>
<?php else: ?>
<div class="alert alert-warning mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>Geen wereldkaart SVG gevonden. Genereer deze met:
<code>php cli/generate-map.php</code>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($drillCountry && !empty($drillHosts)): ?>
<div class="card shadow-sm mb-4">
<div class="card-header bg-white"><i class="bi bi-hdd-network me-2"></i>Hosts in <?= GeoIP::getCountryName($drillCountry) ?></div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
<?php foreach ($drillHosts as $host): ?>
<a href="?page=logs&host=<?= urlencode($host) ?>" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<?= htmlspecialchars($host) ?>
<i class="bi bi-arrow-right text-muted"></i>
</a>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
<div class="card shadow-sm">
<div class="card-header bg-white"><i class="bi bi-list-columns me-2"></i>Landen (<?= count($countryStats) ?>)</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>#</th>
<th>Land</th>
<th>Berichten</th>
<th>%</th>
<th></th>
</tr>
</thead>
<tbody>
<?php $maxC = $countryStats[0]['count'] ?? 1; ?>
<?php foreach ($countryStats as $i => $c): ?>
<tr class="<?= $drillCountry && strtoupper($c['code']) === strtoupper($drillCountry) ? 'table-active' : '' ?>">
<td class="text-muted"><?= $i + 1 ?></td>
<td>
<a href="?page=worldmap&period=<?= $period ?>&country=<?= urlencode($c['code']) ?>" class="text-decoration-none">
<?= GeoIP::getCountryFlagEmoji($c['code']) ?>
<?= GeoIP::getCountryName($c['code']) ?>
</a>
</td>
<td><?= number_format($c['count'], 0, ',', '.') ?></td>
<td><?= $totalLogs > 0 ? round($c['count'] / $totalLogs * 100, 1) : 0 ?>%</td>
<td>
<div class="progress" style="height:6px;min-width:80px">
<div class="progress-bar" style="width:<?= $c['count'] / $maxC * 100 ?>%"></div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var counts = <?= json_encode($mapCountries) ?>;
var container = document.getElementById('worldMapContainer');
var tooltip = document.getElementById('mapTooltip');
if (!container || !tooltip) return;
container.addEventListener('mousemove', function (e) {
var target = e.target.closest('.country');
if (!target) { tooltip.style.display = 'none'; return; }
var code = target.getAttribute('id');
var name = target.getAttribute('data-name') || code;
var n = counts[code] || 0;
tooltip.textContent = name + ': ' + n.toLocaleString('nl-NL') + ' bericht' + (n === 1 ? '' : 'en');
tooltip.style.display = 'block';
var r = container.getBoundingClientRect();
var x = e.clientX - r.left + 12;
var y = e.clientY - r.top + 12;
if (x + 200 > r.width) x = e.clientX - r.left - 200;
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
});
container.addEventListener('mouseleave', function () {
tooltip.style.display = 'none';
});
container.addEventListener('click', function (e) {
var target = e.target.closest('.country');
if (!target) return;
var code = target.getAttribute('id');
if (code) {
window.location.href = '?page=worldmap&period=<?= $period ?>&country=' + encodeURIComponent(code);
}
});
});
</script>
<?php $sq = null; ?>