feat: visiteurs uniques par article (7/14/30 j) stockés dans visitors.json

- AccessLogParser : suivi des IPs non-bot uniques par /post/ sur 3 fenêtres (7/14/30 j)
- index.php : écriture de data/UUID/visitors.json à chaque recalcul des stats admin
- post_view.php : affichage du compteur de lecteurs dans la zone hero

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 22:27:10 +02:00
parent 1e41ef207e
commit dbbe60f28e
5 changed files with 95 additions and 22 deletions
+38 -11
View File
@@ -10,7 +10,14 @@ class AccessLogParser
private int $cacheTtl;
private int $days;
/** @var list<string> */
private array $botPatterns;
private array $botPatterns;
/** @var array<string,array<string,true>> */
private array $artIp7 = [];
/** @var array<string,array<string,true>> */
private array $artIp14 = [];
/** @var array<string,array<string,true>> */
private array $artIp30 = [];
private static ?array $memo = null;
@@ -46,7 +53,8 @@ class AccessLogParser
* ip_top_paths:array<string,array<string,array{n:int,ts:int}>>,
* ip_agents:array<string,list<string>>,
* all_uas:array<string,int>,
* unique_visitors:array<int,int>
* unique_visitors:array<int,int>,
* article_unique_visitors:array<string,array<int,int>>
* }
*/
public function stats(): array
@@ -151,16 +159,27 @@ class AccessLogParser
}
}
// Visiteurs uniques par article (IPs publiques non-bot, /post/ statut 200)
$articleUv = [];
foreach ($this->artIp30 as $path => $ips) {
$articleUv[$path] = [
'7' => count($this->artIp7[$path] ?? []),
'14' => count($this->artIp14[$path] ?? []),
'30' => count($ips),
];
}
$result = [
'pages' => $pages,
'books' => $books,
'ips' => $ips,
'pages_by_day' => $pagesByDay,
'ips_by_day' => $ipsByDay,
'ip_top_paths' => $ipTopPaths,
'ip_agents' => $ipTopAgents,
'all_uas' => array_slice($allUas, 0, 300, true),
'unique_visitors' => $uniqueVisitors,
'pages' => $pages,
'books' => $books,
'ips' => $ips,
'pages_by_day' => $pagesByDay,
'ips_by_day' => $ipsByDay,
'ip_top_paths' => $ipTopPaths,
'ip_agents' => $ipTopAgents,
'all_uas' => array_slice($allUas, 0, 300, true),
'unique_visitors' => $uniqueVisitors,
'article_unique_visitors' => $articleUv,
];
@mkdir(dirname($this->cacheFile), 0755, true);
@file_put_contents($this->cacheFile, json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
@@ -290,6 +309,14 @@ class AccessLogParser
if ($tsVal > ($ipPathTs[$ip][$path] ?? 0)) {
$ipPathTs[$ip][$path] = $tsVal;
}
// Visiteurs uniques par article (IPs publiques non-bot uniquement)
$this->artIp30[$path][$ip] = true;
if ($dayOffset >= $this->days - 14) {
$this->artIp14[$path][$ip] = true;
}
if ($dayOffset >= $this->days - 7) {
$this->artIp7[$path][$ip] = true;
}
}
} elseif (str_ends_with($path, '/') === false && str_starts_with($path, '/book/') && strlen($path) > 6) {
$books[$path] = ($books[$path] ?? 0) + 1;