b0f4814bb0
- Agents détectés : UA affiché sans troncature (drill-down et liste) - Bouton « + bot » pour ajouter un agent aux patterns via AJAX (CSRF) - Section Agents alimentée par all_uas (tous UAs publics, bots inclus) - AccessLogParser : bots exclus des compteurs pages/livres/visiteurs - Caches stats vidés après chaque modification des patterns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
133 lines
6.2 KiB
PHP
133 lines
6.2 KiB
PHP
<?php
|
|
$_statsSaved = isset($_GET['saved']);
|
|
$_statsError = ($_GET['error'] ?? '') === 'write';
|
|
$_readable = $adminData['stats_readable'] ?? false;
|
|
$_books = $adminData['stats_books'] ?? [];
|
|
$_asList = $adminData['stats_as'] ?? [];
|
|
$_pagesByDay = $adminData['stats_pages_by_day'] ?? [];
|
|
$_ipData = $adminData['stats_ip_data'] ?? [];
|
|
$_botPatterns = $adminData['bot_patterns'] ?? [];
|
|
$_allUas = $adminData['stats_all_uas'] ?? [];
|
|
?>
|
|
|
|
<?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 · tous les chemins · flux RSS XML</p>
|
|
|
|
<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) ?>;
|
|
var FOLIO_IP_DATA = <?= json_encode($_ipData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
|
var FOLIO_BOT_PATTERNS = <?= json_encode($_botPatterns, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
|
var FOLIO_ALL_UAS = <?= json_encode($_allUas, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
|
var FOLIO_CSRF = <?= json_encode($_session['csrf'] ?? '', JSON_UNESCAPED_UNICODE) ?>;
|
|
</script>
|
|
|
|
<div class="card mb-4">
|
|
<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 class="card-footer bg-transparent border-top px-3 pt-3 pb-2" id="stats-trend-container"></div>
|
|
<div class="card-footer bg-transparent border-top px-3 pt-3 pb-2" id="stats-multiline-container"></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">
|
|
<!-- 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 -->
|
|
|
|
<!-- Agents détectés -->
|
|
<div class="card mt-4">
|
|
<div class="card-header bg-transparent py-2 small fw-semibold d-flex justify-content-between align-items-center">
|
|
<span>Agents détectés <span class="text-muted fw-normal" id="agents-count"></span></span>
|
|
<button class="btn btn-sm btn-outline-secondary py-0" type="button"
|
|
data-bs-toggle="collapse" data-bs-target="#agents-edit-panel">
|
|
Gérer les patterns
|
|
</button>
|
|
</div>
|
|
<div class="card-body p-0" id="stats-agents-container">
|
|
<p class="text-muted p-3 mb-0">Chargement…</p>
|
|
</div>
|
|
|
|
<!-- Panneau d'édition des patterns bots -->
|
|
<div id="agents-edit-panel" class="collapse">
|
|
<div class="card-footer bg-transparent border-top p-3">
|
|
<p class="small text-muted mb-2">Un pattern par ligne (correspondance insensible à la casse, recherche partielle dans le User-Agent).</p>
|
|
<form method="post" action="/?action=admin_save_bots">
|
|
<input type="hidden" name="_csrf" value="<?= htmlspecialchars($_session['csrf'] ?? '') ?>">
|
|
<textarea name="bot_patterns" class="form-control form-control-sm font-monospace mb-2"
|
|
rows="12" style="font-size:.75rem"><?= htmlspecialchars(implode("\n", $_botPatterns)) ?></textarea>
|
|
<button type="submit" class="btn btn-sm btn-primary">Enregistrer les patterns</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php endif; // readable?>
|
|
|
|
<script src="/assets/js/admin-stats.js" defer></script>
|