feat : graphique visiteurs par pays + réseaux détail + suppression groupes AS

This commit is contained in:
2026-05-19 19:44:27 +02:00
parent be8a95ac4f
commit d6a7033e9e
2 changed files with 88 additions and 50 deletions
+77 -10
View File
@@ -1,20 +1,87 @@
/* Admin stats : groupes AS + chargement pages via flux RSS XML /trending?period=14d */ /* Admin stats : groupes AS + chargement pages via flux RSS XML /trending?period=14d */
// ── Groupes de réseaux ────────────────────────────────────────────────────────
// ── Visiteurs par pays ────────────────────────────────────────────────────────
(function () { (function () {
var addBtn = document.getElementById('as-group-add'); var el = document.getElementById('stats-country-container');
if (!addBtn) { return; } var asList = (typeof FOLIO_AS_LIST !== 'undefined') ? FOLIO_AS_LIST : [];
if (!el || !asList.length) { return; }
addBtn.addEventListener('click', function () { // Noms de pays en français via l'API Intl
var tpl = document.getElementById('as-group-tpl').content.cloneNode(true); var dispNames = null;
document.getElementById('as-groups-list').appendChild(tpl); try { dispNames = new Intl.DisplayNames(['fr'], { type: 'region' }); } catch (e) {}
function countryName(code) {
if (!code || code === '??') { return 'Inconnu'; }
try { return dispNames ? dispNames.of(code) : code; } catch (e) { return code; }
}
// Drapeau emoji depuis le code ISO-2
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]) + ' ';
}
// Agréger par pays
var byCountry = {};
var asByCountry = {}; // country → [{name, asn, hits}]
asList.forEach(function (as) {
var c = as.country || '??';
byCountry[c] = (byCountry[c] || 0) + as.hits;
if (!asByCountry[c]) { asByCountry[c] = []; }
asByCountry[c].push(as);
}); });
document.getElementById('as-groups-list').addEventListener('click', function (e) { var countries = Object.keys(byCountry).map(function (c) {
if (e.target.classList.contains('as-group-delete')) { return { code: c, hits: byCountry[c], networks: asByCountry[c] };
e.target.closest('.as-group-row').remove(); }).sort(function (a, b) { return b.hits - a.hits; }).slice(0, 20);
}
if (!countries.length) { el.innerHTML = '<p class="text-muted mb-0">Aucune donnée.</p>'; return; }
var maxH = countries[0].hits || 1;
var html = '<div class="accordion accordion-flush" id="acc-countries">';
countries.forEach(function (c, i) {
var pct = Math.round(c.hits / maxH * 100);
var cname = flag(c.code) + countryName(c.code);
var vis = c.hits.toLocaleString('fr-FR');
var accId = 'acc-country-' + i;
var nets = c.networks.slice().sort(function (a, b) { return b.hits - a.hits; });
var maxN = nets[0] ? nets[0].hits : 1;
var netRows = nets.map(function (n) {
var npct = Math.round(n.hits / maxN * 100);
return '<div class="d-flex align-items-center gap-2 py-1">'
+ '<div class="text-muted small" style="width:9rem;flex-shrink:0">'
+ (n.name || '?') + (n.asn ? ' <span class="opacity-50">AS' + n.asn + '</span>' : '') + '</div>'
+ '<div class="flex-grow-1"><div class="progress" style="height:4px">'
+ '<div class="progress-bar bg-info" style="width:' + npct + '%"></div>'
+ '</div></div>'
+ '<div class="text-end text-muted small" style="width:4rem;flex-shrink:0">'
+ n.hits.toLocaleString('fr-FR') + '</div>'
+ '</div>';
}).join('');
html += '<div class="accordion-item border-0">'
+ '<div class="d-flex align-items-center gap-2 py-2 px-0" data-bs-toggle="collapse"'
+ ' data-bs-target="#' + accId + '" role="button" aria-expanded="false">'
+ '<div class="fw-medium" style="width:10rem;flex-shrink:0">' + cname + '</div>'
+ '<div class="flex-grow-1"><div class="progress" style="height:6px">'
+ '<div class="progress-bar" style="width:' + pct + '%"></div>'
+ '</div></div>'
+ '<div class="text-end fw-semibold" style="width:5rem;flex-shrink:0">'
+ vis + ' <span class="text-muted fw-normal small">vis.</span></div>'
+ '<span class="text-muted" style="width:1rem;flex-shrink:0;font-size:.7rem">▾</span>'
+ '</div>'
+ '<div id="' + accId + '" class="collapse">'
+ '<div class="ps-2 pb-2 border-start ms-3">' + netRows + '</div>'
+ '</div>'
+ '</div>';
}); });
html += '</div>';
el.innerHTML = html;
}()); }());
// ── Pages les plus visitées (RSS XML + sparklines) ─────────────────────────── // ── Pages les plus visitées (RSS XML + sparklines) ───────────────────────────
+11 -40
View File
@@ -25,7 +25,10 @@ $_activeGroup = trim($_GET['group'] ?? '');
<p class="text-muted small mb-4">14 derniers jours · visiteurs uniques · flux RSS XML</p> <p class="text-muted small mb-4">14 derniers jours · visiteurs uniques · flux RSS XML</p>
<script>var FOLIO_PAGES_BY_DAY = <?= json_encode($_pagesByDay, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;</script> <script>
var FOLIO_PAGES_BY_DAY = <?= json_encode($_pagesByDay, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
var FOLIO_AS_LIST = <?= json_encode($_asList, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
</script>
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header bg-transparent py-2 small fw-semibold d-flex justify-content-between"> <div class="card-header bg-transparent py-2 small fw-semibold d-flex justify-content-between">
@@ -39,6 +42,13 @@ $_activeGroup = trim($_GET['group'] ?? '');
<div class="card-footer bg-transparent border-top px-3 pt-3 pb-2" id="stats-multiline-container"></div> <div class="card-footer bg-transparent border-top px-3 pt-3 pb-2" id="stats-multiline-container"></div>
</div> </div>
<div class="card mb-4">
<div class="card-header bg-transparent py-2 small fw-semibold">Visiteurs par pays</div>
<div class="card-body p-3" id="stats-country-container">
<p class="text-muted mb-0">Chargement…</p>
</div>
</div>
<div class="row g-4"> <div class="row g-4">
<!-- Livres --> <!-- Livres -->
@@ -157,44 +167,5 @@ $_activeGroup = trim($_GET['group'] ?? '');
<?php endif; // readable?> <?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&#10;ex : Free SAS&#10;Orange&#10;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&#10;ex : Googlebot&#10;Bingbot"></textarea>
</div>
</template>
<script src="/assets/js/admin-stats.js" defer></script> <script src="/assets/js/admin-stats.js" defer></script>