diff --git a/CHANGELOG.md b/CHANGELOG.md index a40e590..aada001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag --- +## [1.6.29] - 2026-05-19 + +### Modifié +- Admin stats / drill-down IP : chemins affichés un par ligne avec compteur entre parenthèses, triés par date de dernier accès (plus récent en premier) +- AccessLogParser : suivi du dernier horodatage par chemin/IP (`ipPathTs`), `ip_top_paths` devient `{n: count, ts: timestamp}` + +--- + ## [1.6.28] - 2026-05-19 ### Ajouté diff --git a/public/assets/js/admin-stats.js b/public/assets/js/admin-stats.js index 7aa88c0..182a299 100644 --- a/public/assets/js/admin-stats.js +++ b/public/assets/js/admin-stats.js @@ -20,7 +20,7 @@ function esc(s) { function flag(code) { if (!code || code.length !== 2) { return ''; } var cp = Array.from(code.toUpperCase()).map(function (c) { return 0x1F1E6 + c.charCodeAt(0) - 65; }); - return String.fromCodePoint(cp[0], cp[1]) + ' '; + return String.fromCodePoint(cp[0], cp[1]) + ' '; } // Index IPs par ASN pour le drill-down @@ -35,7 +35,7 @@ function esc(s) { ipsByAsn[k].sort(function (a, b) { return b.hits - a.hits; }); }); - // Mini sparkline (80×20px polyline) pour chaque IP + // Mini sparkline (80x20px polyline) pour chaque IP function ipSparkline(daily) { if (!daily || !daily.length) { return ''; } var W = 80, H = 20, padX = 1, padY = 2; @@ -85,44 +85,45 @@ function esc(s) { var asnKey = n.asn || '__unknown__'; var ips = ipsByAsn[asnKey] || []; - // Lignes IP avec mini sparkline + chemins + // Lignes IP avec mini sparkline + chemins triés par date desc var ipRows = ips.slice(0, 20).map(function (ipInfo) { var articles = [], books = []; Object.keys(ipInfo.paths || {}).forEach(function (path) { - var cnt = ipInfo.paths[path]; - if (path.indexOf('/post/') === 0) { articles.push({ path: path, cnt: cnt }); } - else if (path.indexOf('/book/') === 0) { books.push({ path: path, cnt: cnt }); } + var p = ipInfo.paths[path]; + var cnt = (p && typeof p === 'object') ? p.n : p; + var ts = (p && typeof p === 'object') ? p.ts : 0; + if (path.indexOf('/post/') === 0) { articles.push({ path: path, cnt: cnt, ts: ts }); } + else if (path.indexOf('/book/') === 0) { books.push({ path: path, cnt: cnt, ts: ts }); } }); - articles.sort(function (a, b) { return b.cnt - a.cnt; }); - books.sort(function (a, b) { return b.cnt - a.cnt; }); + articles.sort(function (a, b) { return b.ts - a.ts; }); + books.sort(function (a, b) { return b.ts - a.ts; }); + + function pathLine(p, prefix) { + var slug = decodeURIComponent(p.path.replace(prefix, '')); + var label = slug.length > 40 ? slug.slice(0, 40) + '…' : slug; + return '
'
+ return ''
+ + ''
+ esc(ipInfo.ip) + ''
- + ipSparkline(ipInfo.daily || [])
+ + '' + ipSparkline(ipInfo.daily || []) + ''
+ '' + pathsHtml + ''
- + ''
+ + ''
+ (ipInfo.hits || 0).toLocaleString('fr-FR') + ''
+ '';
}).join('');
diff --git a/public/version.txt b/public/version.txt
index 10010a4..69e4e62 100644
--- a/public/version.txt
+++ b/public/version.txt
@@ -1 +1 @@
-1.6.28
+1.6.29
diff --git a/src/AccessLogParser.php b/src/AccessLogParser.php
index 54ae405..68e6919 100644
--- a/src/AccessLogParser.php
+++ b/src/AccessLogParser.php
@@ -30,7 +30,7 @@ class AccessLogParser
}
/**
- * @return array{pages:array,books:array,ips:array,pages_by_day:array>,ips_by_day:array>,ip_top_paths:array>}
+ * @return array{pages:array,books:array,ips:array,pages_by_day:array>,ips_by_day:array>,ip_top_paths:array>}
*/
public function stats(): array
{
@@ -44,16 +44,17 @@ class AccessLogParser
}
}
- $cutoff = strtotime("-{$this->days} days midnight") ?: (time() - $this->days * 86400);
- $pages = [];
- $books = [];
- $ips = [];
- $dayPages = [];
- $ipDays = []; // [ip => [dayOffset => count]]
- $ipPaths = []; // [ip => [path => count]]
+ $cutoff = strtotime("-{$this->days} days midnight") ?: (time() - $this->days * 86400);
+ $pages = [];
+ $books = [];
+ $ips = [];
+ $dayPages = [];
+ $ipDays = []; // [ip => [dayOffset => count]]
+ $ipPaths = []; // [ip => [path => count]]
+ $ipPathTs = []; // [ip => [path => last_timestamp]]
foreach ($this->logFiles() as $file) {
- $this->parseFile($file, $cutoff, $pages, $books, $ips, $dayPages, $ipDays, $ipPaths);
+ $this->parseFile($file, $cutoff, $pages, $books, $ips, $dayPages, $ipDays, $ipPaths, $ipPathTs);
}
arsort($pages);
@@ -86,7 +87,10 @@ class AccessLogParser
$paths = $ipPaths[$ip] ?? [];
arsort($paths);
- $ipTopPaths[$ip] = array_slice($paths, 0, 10, true);
+ $ipTopPaths[$ip] = [];
+ foreach (array_slice($paths, 0, 10, true) as $p => $cnt) {
+ $ipTopPaths[$ip][$p] = ['n' => $cnt, 'ts' => $ipPathTs[$ip][$p] ?? 0];
+ }
}
$result = [
@@ -152,7 +156,7 @@ class AccessLogParser
return (int) strtotime("{$m[1]} {$m[2]} {$m[3]} {$m[4]} {$m[5]}");
}
- private function parseLine(string $line, int $cutoff, array &$pages, array &$books, array &$ips, array &$dayPages, array &$ipDays, array &$ipPaths): void
+ private function parseLine(string $line, int $cutoff, array &$pages, array &$books, array &$ips, array &$dayPages, array &$ipDays, array &$ipPaths, array &$ipPathTs): void
{
if (!preg_match(self::RE, $line, $m)) {
return;
@@ -175,9 +179,12 @@ class AccessLogParser
if ($publicIp) {
$ips[$ip] = ($ips[$ip] ?? 0) + 1;
}
- $dayPages[$path][$dayOffset] = ($dayPages[$path][$dayOffset] ?? 0) + 1;
- $ipDays[$ip][$dayOffset] = ($ipDays[$ip][$dayOffset] ?? 0) + 1;
- $ipPaths[$ip][$path] = ($ipPaths[$ip][$path] ?? 0) + 1;
+ $dayPages[$path][$dayOffset] = ($dayPages[$path][$dayOffset] ?? 0) + 1;
+ $ipDays[$ip][$dayOffset] = ($ipDays[$ip][$dayOffset] ?? 0) + 1;
+ $ipPaths[$ip][$path] = ($ipPaths[$ip][$path] ?? 0) + 1;
+ if ($tsVal > ($ipPathTs[$ip][$path] ?? 0)) {
+ $ipPathTs[$ip][$path] = $tsVal;
+ }
} elseif (str_starts_with($path, '/book/') && strlen($path) > 6) {
$books[$path] = ($books[$path] ?? 0) + 1;
if ($publicIp) {
@@ -185,10 +192,13 @@ class AccessLogParser
}
$ipDays[$ip][$dayOffset] = ($ipDays[$ip][$dayOffset] ?? 0) + 1;
$ipPaths[$ip][$path] = ($ipPaths[$ip][$path] ?? 0) + 1;
+ if ($tsVal > ($ipPathTs[$ip][$path] ?? 0)) {
+ $ipPathTs[$ip][$path] = $tsVal;
+ }
}
}
- private function parseFile(array $file, int $cutoff, array &$pages, array &$books, array &$ips, array &$dayPages, array &$ipDays, array &$ipPaths): void
+ private function parseFile(array $file, int $cutoff, array &$pages, array &$books, array &$ips, array &$dayPages, array &$ipDays, array &$ipPaths, array &$ipPathTs): void
{
if ($file['type'] === 'tgz') {
try {
@@ -199,7 +209,7 @@ class AccessLogParser
continue;
}
foreach (explode("\n", $content) as $line) {
- $this->parseLine($line, $cutoff, $pages, $books, $ips, $dayPages, $ipDays, $ipPaths);
+ $this->parseLine($line, $cutoff, $pages, $books, $ips, $dayPages, $ipDays, $ipPaths, $ipPathTs);
}
}
} catch (\Exception $e) {
@@ -212,7 +222,7 @@ class AccessLogParser
while (!gzeof($h)) {
$line = gzgets($h, 8192);
if ($line !== false) {
- $this->parseLine($line, $cutoff, $pages, $books, $ips, $dayPages, $ipDays, $ipPaths);
+ $this->parseLine($line, $cutoff, $pages, $books, $ips, $dayPages, $ipDays, $ipPaths, $ipPathTs);
}
}
gzclose($h);
@@ -222,7 +232,7 @@ class AccessLogParser
return;
}
while (($line = fgets($h)) !== false) {
- $this->parseLine($line, $cutoff, $pages, $books, $ips, $dayPages, $ipDays, $ipPaths);
+ $this->parseLine($line, $cutoff, $pages, $books, $ips, $dayPages, $ipDays, $ipPaths, $ipPathTs);
}
fclose($h);
}