CodePress CMS v1.9.0: visitor statistics with SVG world map and GeoIP

- Add GeoIP class with provider chain: local DB-IP Lite, MaxMind .mmdb, external API
- Add built-in pure-PHP MMDBReader so .mmdb works without Composer
- Add cli/geoip-update.php to download DB-IP Lite and build a compact binary index
- Add cli/generate-world-map.php to generate the world map SVG from Natural Earth TopoJSON
- Add Analytics class aggregating stats in admin/storage/stats.json with LOCK_EX
- Add admin statistics page with choropleth world map, country list, top pages,
  daily chart, referrers and a period filter
- Add GeoIP and privacy settings with database update and stats reset buttons
- Add optional IP anonymization and configurable retention period
- Add country field to requests.log (parser accepts 7, 8 or 9 fields)
- Add country column to request log and KPI cards to the dashboard
- Ignore GeoIP binaries and stats.json in Git
- Update TODO.md, guides and version to 1.9.0
This commit is contained in:
2026-07-29 15:40:02 +02:00
parent a795307664
commit 5357bc8915
20 changed files with 1804 additions and 11 deletions
+47
View File
@@ -1,5 +1,52 @@
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h2>
<?php
$aTotals = $analyticsSummary['totals'] ?? [];
$aCountries = $analyticsSummary['countries'] ?? [];
$topCountry = null;
foreach ($aCountries as $cc => $cnt) {
if ($cc !== 'UNKNOWN') { $topCountry = $cc; break; }
}
?>
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Weergaven (30 dagen)</h6>
<h3 class="mb-0"><?= number_format((int)($aTotals['views'] ?? 0), 0, ',', '.') ?></h3>
</div>
<i class="bi bi-eye stat-icon text-primary"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Unieke bezoekers (30 dagen)</h6>
<h3 class="mb-0"><?= number_format((int)($aTotals['uniques'] ?? 0), 0, ',', '.') ?></h3>
</div>
<i class="bi bi-people stat-icon text-success"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Grootste land</h6>
<h3 class="mb-0">
<?= GeoIP::getCountryFlagEmoji($topCountry) ?>
<span class="fs-5"><?= htmlspecialchars(GeoIP::getCountryName($topCountry)) ?></span>
</h3>
</div>
<a href="/admin/statistics" class="btn btn-sm btn-outline-secondary">Statistieken →</a>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card stat-card shadow-sm">