From e3d7e433e0f3868bd26704d515f546694931df0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Tue, 19 May 2026 20:09:44 +0200 Subject: [PATCH] =?UTF-8?q?v1.6.29=20:=20chemins=20IP=20tri=C3=A9s=20par?= =?UTF-8?q?=20date,=20un=20par=20ligne=20avec=20compteur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drill-down IP : articles/livres affichés un par ligne (compteur entre parenthèses), triés par date de dernier accès desc - AccessLogParser : ipPathTs trace le dernier timestamp par chemin/IP - ip_top_paths : structure {n, ts} au lieu de count simple Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 8 +++++ public/assets/js/admin-stats.js | 53 +++++++++++++++++---------------- public/version.txt | 2 +- src/AccessLogParser.php | 46 +++++++++++++++++----------- 4 files changed, 64 insertions(+), 45 deletions(-) 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 '
' + + '' + + esc(label) + '' + + ' (' + p.cnt + ')
'; + } var pathsHtml = ''; if (articles.length) { - pathsHtml += '
Articles : ' - + articles.slice(0, 3).map(function (p) { - var slug = decodeURIComponent(p.path.replace('/post/', '')); - return '' - + esc(slug.length > 28 ? slug.slice(0, 28) + '…' : slug) - + ' (' + p.cnt + ')'; - }).join(', ') + '
'; + pathsHtml += '
Articles
' + + articles.map(function (p) { return pathLine(p, '/post/'); }).join(''); } if (books.length) { - pathsHtml += '
Livres : ' - + books.slice(0, 3).map(function (p) { - var slug = decodeURIComponent(p.path.replace('/book/', '')); - return '' - + esc(slug.length > 28 ? slug.slice(0, 28) + '…' : slug) - + ' (' + p.cnt + ')'; - }).join(', ') + '
'; + pathsHtml += '
Livres
' + + books.map(function (p) { return pathLine(p, '/book/'); }).join(''); } if (!pathsHtml) { pathsHtml = ''; } - 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); }