5cea473d17
- post_list.php : section AJAX qui lit /trending?period=1h en XML (DOMParser) — plus de rendu PHP - admin_stats.php : colonne "Pages les plus visitées" chargée en AJAX depuis /trending?period=14d XML - index.php/stats : suppression de topGrouped pour /post/ ; seuls /book/ et ASN restent côté serveur Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
255 lines
12 KiB
PHP
255 lines
12 KiB
PHP
<?php
|
|
$_statsSaved = isset($_GET['saved']);
|
|
$_statsError = ($_GET['error'] ?? '') === 'write';
|
|
$_readable = $adminData['stats_readable'] ?? false;
|
|
$_books = $adminData['stats_books'] ?? [];
|
|
$_asList = $adminData['stats_as'] ?? [];
|
|
$_asGroups = $adminData['stats_as_groups'] ?? [];
|
|
$_groups = $adminData['as_groups'] ?? [];
|
|
$_activeGroup = trim($_GET['group'] ?? '');
|
|
?>
|
|
|
|
<?php if ($_statsSaved): ?>
|
|
<div class="alert alert-success py-2 mb-3">Configuration enregistrée.</div>
|
|
<?php elseif ($_statsError): ?>
|
|
<div class="alert alert-danger py-2 mb-3">Impossible d'enregistrer : fichier non accessible en écriture.</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!$_readable): ?>
|
|
<div class="alert alert-warning">
|
|
Les logs ne sont pas lisibles. Vérifiez le pattern dans l'onglet <a href="/admin/searches">Recherches</a>
|
|
et que <code>www-data</code> appartient au groupe <code>adm</code>.
|
|
</div>
|
|
<?php else: ?>
|
|
|
|
<p class="text-muted small mb-4">14 derniers jours · visiteurs uniques · flux RSS XML</p>
|
|
|
|
<div class="row g-4">
|
|
|
|
<!-- Pages (chargées via le flux RSS XML /trending?period=14d) -->
|
|
<div class="col-lg-6">
|
|
<div class="card h-100">
|
|
<div class="card-header bg-transparent py-2 small fw-semibold d-flex justify-content-between">
|
|
<span>Pages les plus visitées</span>
|
|
<span class="text-muted" id="stats-pages-count"></span>
|
|
</div>
|
|
<div class="card-body p-0" id="stats-pages-container">
|
|
<p class="text-muted p-3 mb-0">Chargement…</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Livres -->
|
|
<div class="col-lg-6">
|
|
<div class="card h-100">
|
|
<div class="card-header bg-transparent py-2 small fw-semibold d-flex justify-content-between">
|
|
<span>Livres consultés</span>
|
|
<span class="text-muted"><?= count($_books) ?> livres</span>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<?php if (empty($_books)): ?>
|
|
<p class="text-muted p-3 mb-0">Aucun accès à <code>/book/</code> dans les logs.</p>
|
|
<?php else: ?>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover mb-0 small">
|
|
<tbody>
|
|
<?php
|
|
$maxB = max($_books) ?: 1;
|
|
$rankB = 0;
|
|
foreach ($_books as $url => $hits):
|
|
$rankB++;
|
|
$slug = rawurldecode(substr($url, 6));
|
|
$pct = round($hits / $maxB * 100);
|
|
?>
|
|
<tr>
|
|
<td class="text-muted ps-3" style="width:2rem"><?= $rankB ?></td>
|
|
<td>
|
|
<a href="<?= htmlspecialchars($url) ?>" target="_blank"
|
|
class="text-decoration-none text-truncate d-block" style="max-width:260px"
|
|
title="<?= htmlspecialchars($slug) ?>">
|
|
<?= htmlspecialchars($slug) ?>
|
|
</a>
|
|
<div class="progress mt-1" style="height:3px">
|
|
<div class="progress-bar bg-success" style="width:<?= $pct ?>%"></div>
|
|
</div>
|
|
</td>
|
|
<td class="text-end fw-semibold pe-3"><?= number_format($hits, 0, ',', '\u{202F}') ?> <span class="text-muted fw-normal">vis.</span></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /row -->
|
|
|
|
<!-- Répartition par réseau -->
|
|
<div class="card mt-4">
|
|
<div class="card-header bg-transparent py-2 small fw-semibold d-flex align-items-center gap-3 flex-wrap">
|
|
<span>Répartition par réseau</span>
|
|
<?php if (!empty($_groups)): ?>
|
|
<div class="d-flex gap-1 flex-wrap">
|
|
<a href="/admin/stats" class="badge <?= $_activeGroup === '' ? 'bg-primary' : 'bg-secondary' ?> text-decoration-none">Tous</a>
|
|
<?php foreach ($_groups as $g): ?>
|
|
<a href="/admin/stats?group=<?= rawurlencode($g['label']) ?>"
|
|
class="badge <?= $_activeGroup === $g['label'] ? 'bg-primary' : 'bg-secondary' ?> text-decoration-none">
|
|
<?= htmlspecialchars($g['label']) ?>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
<a href="/admin/stats?group=Autres"
|
|
class="badge <?= $_activeGroup === 'Autres' ? 'bg-primary' : 'bg-secondary' ?> text-decoration-none">Autres</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<?php
|
|
// Sélectionner les AS à afficher
|
|
if ($_activeGroup !== '' && isset($_asGroups[$_activeGroup])) {
|
|
$displayAs = $_asGroups[$_activeGroup];
|
|
} else {
|
|
$displayAs = $_asList;
|
|
}
|
|
?>
|
|
<?php if (empty($displayAs)): ?>
|
|
<p class="text-muted p-3 mb-0">
|
|
<?= empty($_asList) ? 'Aucune IP résolue (LAN ou logs vides).' : 'Aucun AS dans ce groupe.' ?>
|
|
</p>
|
|
<?php else: ?>
|
|
<?php $maxAS = max(array_column($displayAs, 'hits')) ?: 1; ?>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover mb-0 small">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th class="ps-3" style="width:2rem">#</th>
|
|
<th>Réseau</th>
|
|
<th style="width:3rem">Pays</th>
|
|
<th style="width:5rem" class="text-end pe-3">Visites</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($displayAs as $i => $as): ?>
|
|
<tr>
|
|
<td class="text-muted ps-3"><?= $i + 1 ?></td>
|
|
<td>
|
|
<span class="fw-medium"><?= htmlspecialchars($as['name'] ?: '?') ?></span>
|
|
<?php if ($as['asn'] !== ''): ?>
|
|
<span class="text-muted ms-1">AS<?= htmlspecialchars($as['asn']) ?></span>
|
|
<?php endif; ?>
|
|
<div class="progress mt-1" style="height:3px">
|
|
<div class="progress-bar bg-info" style="width:<?= round($as['hits'] / $maxAS * 100) ?>%"></div>
|
|
</div>
|
|
</td>
|
|
<td class="text-muted"><?= htmlspecialchars($as['country']) ?></td>
|
|
<td class="text-end fw-semibold pe-3"><?= number_format($as['hits'], 0, ',', '\u{202F}') ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<?php endif; // readable ?>
|
|
|
|
<!-- Groupes de réseaux -->
|
|
<div class="card mt-4" style="max-width:600px">
|
|
<div class="card-header bg-transparent py-2 small fw-semibold">Groupes de réseaux</div>
|
|
<div class="card-body">
|
|
<p class="text-muted small">Regroupez plusieurs réseaux sous un label. Chaque ligne est un motif cherché dans le nom du réseau (insensible à la casse).</p>
|
|
<form method="post" action="/?action=admin_save_as_groups" id="as-groups-form">
|
|
<div id="as-groups-list">
|
|
<?php foreach ($_groups as $gi => $g): ?>
|
|
<div class="as-group-row border rounded p-3 mb-3">
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
<input type="text" name="as_group_label[]" class="form-control form-control-sm"
|
|
placeholder="Label (ex : Opérateurs FR)"
|
|
value="<?= htmlspecialchars($g['label']) ?>" required>
|
|
<button type="button" class="btn btn-outline-danger btn-sm as-group-delete" title="Supprimer">✕</button>
|
|
</div>
|
|
<textarea name="as_group_patterns[]" class="form-control form-control-sm font-monospace"
|
|
rows="3" placeholder="Un motif par ligne ex : Free SAS Orange SFR"><?= htmlspecialchars(implode("\n", $g['patterns'])) ?></textarea>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="d-flex gap-2 mt-2">
|
|
<button type="button" id="as-group-add" class="btn btn-outline-secondary btn-sm">+ Ajouter un groupe</button>
|
|
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<template id="as-group-tpl">
|
|
<div class="as-group-row border rounded p-3 mb-3">
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
<input type="text" name="as_group_label[]" class="form-control form-control-sm"
|
|
placeholder="Label (ex : Moteurs de recherche)" required>
|
|
<button type="button" class="btn btn-outline-danger btn-sm as-group-delete" title="Supprimer">✕</button>
|
|
</div>
|
|
<textarea name="as_group_patterns[]" class="form-control form-control-sm font-monospace"
|
|
rows="3" placeholder="Un motif par ligne ex : Googlebot Bingbot"></textarea>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
document.getElementById('as-group-add').addEventListener('click', () => {
|
|
const tpl = document.getElementById('as-group-tpl').content.cloneNode(true);
|
|
document.getElementById('as-groups-list').appendChild(tpl);
|
|
});
|
|
document.getElementById('as-groups-list').addEventListener('click', e => {
|
|
if (e.target.classList.contains('as-group-delete')) {
|
|
e.target.closest('.as-group-row').remove();
|
|
}
|
|
});
|
|
|
|
// ── Chargement des pages via le flux RSS XML ──────────────────────────────────
|
|
(function(){
|
|
var container = document.getElementById('stats-pages-container');
|
|
var badge = document.getElementById('stats-pages-count');
|
|
if (!container) return;
|
|
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
fetch('/trending?period=14d')
|
|
.then(function(r){ return r.ok ? r.text() : Promise.reject(); })
|
|
.then(function(xml){
|
|
var doc = new DOMParser().parseFromString(xml, 'application/xml');
|
|
var items = Array.from(doc.querySelectorAll('item'));
|
|
if (!items.length) {
|
|
container.innerHTML = '<p class="text-muted p-3 mb-0">Aucune donnée.</p>';
|
|
return;
|
|
}
|
|
var rows = items.map(function(item){
|
|
var raw = (item.querySelector('title') || {textContent:''}).textContent;
|
|
var link = ((item.querySelector('link') || {}).textContent || '').trim();
|
|
var m = raw.match(/\((\d+)\s+visiteurs?\)$/);
|
|
var vis = m ? parseInt(m[1], 10) : 0;
|
|
var title = raw.replace(/\s*\(\d+\s+visiteurs?\)$/, '');
|
|
var slug = decodeURIComponent(link.replace(/.*\/post\//, ''));
|
|
return {title: title, link: link, slug: slug, vis: vis};
|
|
});
|
|
var maxV = Math.max.apply(null, rows.map(function(r){ return r.vis; })) || 1;
|
|
var html = '<div class="table-responsive"><table class="table table-sm table-hover mb-0 small"><tbody>';
|
|
rows.forEach(function(row, i){
|
|
var pct = Math.round(row.vis / maxV * 100);
|
|
var vis = row.vis.toLocaleString('fr-FR');
|
|
html += '<tr>'
|
|
+ '<td class="text-muted ps-3" style="width:2rem">' + (i+1) + '</td>'
|
|
+ '<td><a href="' + esc(row.link) + '" target="_blank" class="text-decoration-none text-truncate d-block" style="max-width:260px" title="' + esc(row.slug) + '">'
|
|
+ esc(row.title || row.slug) + '</a>'
|
|
+ '<div class="progress mt-1" style="height:3px"><div class="progress-bar" style="width:' + pct + '%"></div></div></td>'
|
|
+ '<td class="text-end fw-semibold pe-3">' + vis + ' <span class="text-muted fw-normal">vis.</span></td>'
|
|
+ '</tr>';
|
|
});
|
|
html += '</tbody></table></div>';
|
|
if (badge) badge.textContent = rows.length + ' URLs';
|
|
container.innerHTML = html;
|
|
})
|
|
.catch(function(){
|
|
container.innerHTML = '<p class="text-muted p-3 mb-0">Impossible de charger le flux.</p>';
|
|
});
|
|
})();
|
|
</script>
|