feat : TrendingParser + flux RSS tendances + page publique /tendances (v1.6.4)
- TrendingParser : lit les logs Apache, compte les visiteurs uniques (IPs, HTTP 200), supporte plusieurs préfixes (/post/, /book/) et un seul parse via topGrouped() - /trending?period=… : flux RSS des 50 articles les plus consultés, 10 périodes de 10 min à 1 an, cache TTL adaptatif - /tendances : page publique avec sélecteur de période, top 20 articles, tableau des flux RSS et section méthodologie - /admin/stats : remplace AccessLogParser (hits) par TrendingParser (visiteurs uniques) - Page d'accueil : rubrique Tendances alimentée par les logs 1h (fallback DB) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+75
-33
@@ -2549,6 +2549,7 @@ switch ($action) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
require_once BASE_PATH . '/src/TrendingParser.php';
|
||||
require_once BASE_PATH . '/src/AccessLogParser.php';
|
||||
require_once BASE_PATH . '/src/AsnLookup.php';
|
||||
|
||||
@@ -2558,15 +2559,20 @@ switch ($action) {
|
||||
$statsRaw = json_decode((string) file_get_contents($statsCacheFile), true) ?: null;
|
||||
}
|
||||
if ($statsRaw === null) {
|
||||
$cutoff14 = strtotime('-14 days midnight') ?: (time() - 14 * 86400);
|
||||
$tParser = new TrendingParser('/var/log/apache2', apacheAccessLog());
|
||||
$grouped = $tParser->topGrouped($cutoff14, ['/post/' => 30, '/book/' => 20]);
|
||||
|
||||
// IPs pour le lookup ASN (AccessLogParser conserve le comptage brut par IP)
|
||||
$accessParser = new AccessLogParser('/var/log/apache2', apacheAccessLog());
|
||||
$accessStats = $accessParser->stats();
|
||||
$topIps = array_slice($accessStats['ips'], 0, 200, true);
|
||||
$topIps = array_slice($accessParser->stats()['ips'], 0, 200, true);
|
||||
$asnMap = (new AsnLookup())->batchLookup(array_keys($topIps));
|
||||
|
||||
$statsRaw = [
|
||||
'readable' => $accessParser->isReadable(),
|
||||
'pages' => array_slice($accessStats['pages'], 0, 30, true),
|
||||
'books' => array_slice($accessStats['books'], 0, 20, true),
|
||||
'as' => AsnLookup::aggregateByAs($topIps, $asnMap),
|
||||
'readable' => $tParser->isReadable(),
|
||||
'pages' => $grouped['/post/'],
|
||||
'books' => $grouped['/book/'],
|
||||
'as' => AsnLookup::aggregateByAs($topIps, $asnMap),
|
||||
];
|
||||
@file_put_contents($statsCacheFile, json_encode($statsRaw));
|
||||
}
|
||||
@@ -3445,38 +3451,74 @@ switch ($action) {
|
||||
unset($_heroUuid, $_count, $_hp);
|
||||
|
||||
$allPostsMap = array_column($allPosts, null, 'uuid');
|
||||
$_slugMap = array_column($allPosts, null, 'slug');
|
||||
|
||||
// Articles populaires (10 derniers jours) — score pondéré
|
||||
// Tendances 1 h — logs Apache (visiteurs uniques, cache 12 min)
|
||||
require_once BASE_PATH . '/src/TrendingParser.php';
|
||||
$_trendCache = DATA_PATH . '/_cache/trending_1h.json';
|
||||
$_trendPaths = null;
|
||||
if (file_exists($_trendCache) && (time() - filemtime($_trendCache)) < 720) {
|
||||
$_trendPaths = json_decode((string) file_get_contents($_trendCache), true) ?: null;
|
||||
}
|
||||
if ($_trendPaths === null) {
|
||||
$_tp = new TrendingParser('/var/log/apache2', apacheAccessLog());
|
||||
if ($_tp->isReadable()) {
|
||||
$_trendPaths = $_tp->top(time() - 3600, 20);
|
||||
@mkdir(DATA_PATH . '/_cache', 0755, true);
|
||||
@file_put_contents($_trendCache, json_encode($_trendPaths));
|
||||
}
|
||||
unset($_tp);
|
||||
}
|
||||
if (!empty($_trendPaths)) {
|
||||
foreach ($_trendPaths as $_path => $_cnt) {
|
||||
if (count($popularPosts) >= 6) {
|
||||
break;
|
||||
}
|
||||
if (!preg_match('#^/post/([^/]+)$#', $_path, $_m)) {
|
||||
continue;
|
||||
}
|
||||
$_a = $_slugMap[rawurldecode($_m[1])] ?? null;
|
||||
if ($_a !== null) {
|
||||
$popularPosts[] = $_a;
|
||||
}
|
||||
}
|
||||
unset($_path, $_cnt, $_m, $_a);
|
||||
}
|
||||
unset($_trendCache, $_trendPaths, $_slugMap);
|
||||
|
||||
// Fallback : score pondéré DB (réactions, notes, commentaires sur 10 j)
|
||||
$_pdo = dbPdo();
|
||||
if ($_pdo) {
|
||||
try {
|
||||
$_stmt = $_pdo->query("
|
||||
SELECT article_uuid, SUM(score) AS total
|
||||
FROM (
|
||||
SELECT article_uuid, 1 AS score FROM article_reactions
|
||||
WHERE created_at >= NOW() - INTERVAL '10 days'
|
||||
UNION ALL
|
||||
SELECT article_uuid, 2 AS score FROM article_ratings
|
||||
WHERE rated_at >= NOW() - INTERVAL '10 days'
|
||||
UNION ALL
|
||||
SELECT article_uuid, 3 AS score FROM comments
|
||||
WHERE created_at >= NOW() - INTERVAL '10 days' AND published = TRUE
|
||||
) ev
|
||||
GROUP BY article_uuid
|
||||
ORDER BY total DESC
|
||||
LIMIT 20
|
||||
");
|
||||
foreach ($_stmt->fetchAll(PDO::FETCH_ASSOC) as $_row) {
|
||||
if (count($popularPosts) >= 6) {
|
||||
break;
|
||||
if (empty($popularPosts)) {
|
||||
try {
|
||||
$_stmt = $_pdo->query("
|
||||
SELECT article_uuid, SUM(score) AS total
|
||||
FROM (
|
||||
SELECT article_uuid, 1 AS score FROM article_reactions
|
||||
WHERE created_at >= NOW() - INTERVAL '10 days'
|
||||
UNION ALL
|
||||
SELECT article_uuid, 2 AS score FROM article_ratings
|
||||
WHERE rated_at >= NOW() - INTERVAL '10 days'
|
||||
UNION ALL
|
||||
SELECT article_uuid, 3 AS score FROM comments
|
||||
WHERE created_at >= NOW() - INTERVAL '10 days' AND published = TRUE
|
||||
) ev
|
||||
GROUP BY article_uuid
|
||||
ORDER BY total DESC
|
||||
LIMIT 20
|
||||
");
|
||||
foreach ($_stmt->fetchAll(PDO::FETCH_ASSOC) as $_row) {
|
||||
if (count($popularPosts) >= 6) {
|
||||
break;
|
||||
}
|
||||
$_uuid = $_row['article_uuid'];
|
||||
if (!isset($allPostsMap[$_uuid])) {
|
||||
continue;
|
||||
}
|
||||
$popularPosts[] = $allPostsMap[$_uuid];
|
||||
}
|
||||
$_uuid = $_row['article_uuid'];
|
||||
if (!isset($allPostsMap[$_uuid])) {
|
||||
continue;
|
||||
}
|
||||
$popularPosts[] = $allPostsMap[$_uuid];
|
||||
} catch (Throwable) {
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
|
||||
// Redécouvertes : anciens articles (> 30 j) avec activité récente
|
||||
|
||||
Reference in New Issue
Block a user