first commit
This commit is contained in:
+238
@@ -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; ?>
|
||||
@@ -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 ?>
|
||||
@@ -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
@@ -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; ?>
|
||||
@@ -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; ?>
|
||||
Reference in New Issue
Block a user