v1.6.28 : drill-down IP par AS dans stats pays, suppression Répartition par réseau
- Admin stats : clic sur un réseau AS affiche les IPs avec mini sparkline 14 jours + articles/livres consultés - AccessLogParser : calcul ip_data (daily + top paths) inclus dans le cache stats - Suppression du tableau statique "Répartition par réseau" (fusionné dans accordéon pays) - PHP-CS-Fixer appliqué sur l'ensemble des fichiers modifiés Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+15
-13
@@ -230,7 +230,9 @@ function adminStatusBadge(array $a, int $now): string
|
||||
return '/admin/articles?' . http_build_query($p);
|
||||
};
|
||||
$_sortIcon = function (string $col) use ($_sortBy, $_sortDir): string {
|
||||
if ($_sortBy !== $col) { return '<span class="text-muted ms-1" style="font-size:.75em">↕</span>'; }
|
||||
if ($_sortBy !== $col) {
|
||||
return '<span class="text-muted ms-1" style="font-size:.75em">↕</span>';
|
||||
}
|
||||
return '<span class="ms-1" style="font-size:.75em">' . ($_sortDir === 'asc' ? '↑' : '↓') . '</span>';
|
||||
};
|
||||
?>
|
||||
@@ -389,7 +391,7 @@ function adminStatusBadge(array $a, int $now): string
|
||||
'sort' => $_sortBy,
|
||||
'dir' => $_sortDir,
|
||||
], fn ($v) => $v !== ''));
|
||||
foreach ($adminData['articles'] as $_fa):
|
||||
foreach ($adminData['articles'] as $_fa):
|
||||
?>
|
||||
<form id="toggle-featured-<?= htmlspecialchars($_fa['uuid']) ?>" method="post" action="/?action=admin_toggle_featured" hidden>
|
||||
<input type="hidden" name="uuid" value="<?= htmlspecialchars($_fa['uuid']) ?>">
|
||||
@@ -1422,11 +1424,11 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
|
||||
<option value="">— Choisir un article —</option>
|
||||
<?php
|
||||
$alreadyIn = $eb['articles'] ?? [];
|
||||
foreach ($adminData['all_articles'] as $aa):
|
||||
if (in_array($aa['slug'] ?? '', $alreadyIn, true)) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
foreach ($adminData['all_articles'] as $aa):
|
||||
if (in_array($aa['slug'] ?? '', $alreadyIn, true)) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<option value="<?= htmlspecialchars($aa['slug'] ?? '') ?>">
|
||||
<?= htmlspecialchars($aa['title']) ?>
|
||||
<?= !$aa['published'] ? ' (brouillon)' : '' ?>
|
||||
@@ -1495,11 +1497,11 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
|
||||
|
||||
<?php
|
||||
$_aiNotice = $adminData['ai_notice'] ?? '';
|
||||
$_aiProvider = $adminData['ai_provider'] ?? 'anthropic';
|
||||
$_aiModel = $adminData['ai_model'] ?? '';
|
||||
$_anthropicOk = $adminData['anthropic_key_set'] ?? false;
|
||||
$_cliOk = $adminData['claude_cli_found'] ?? false;
|
||||
?>
|
||||
$_aiProvider = $adminData['ai_provider'] ?? 'anthropic';
|
||||
$_aiModel = $adminData['ai_model'] ?? '';
|
||||
$_anthropicOk = $adminData['anthropic_key_set'] ?? false;
|
||||
$_cliOk = $adminData['claude_cli_found'] ?? false;
|
||||
?>
|
||||
|
||||
<?php if ($_aiNotice === 'saved'): ?>
|
||||
<div class="alert alert-success py-2 small">Configuration IA enregistrée.</div>
|
||||
@@ -1619,6 +1621,6 @@ sudo -u www-data HOME=/var/lib/claude-www /usr/local/bin/claude --print "Répond
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$content = ob_get_clean();
|
||||
$title = 'Administration — ' . siteTitle();
|
||||
include __DIR__ . '/layout.php';
|
||||
|
||||
@@ -4,10 +4,8 @@ $_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'] ?? [];
|
||||
$_pagesByDay = $adminData['stats_pages_by_day'] ?? [];
|
||||
$_activeGroup = trim($_GET['group'] ?? '');
|
||||
$_ipData = $adminData['stats_ip_data'] ?? [];
|
||||
?>
|
||||
|
||||
<?php if ($_statsSaved): ?>
|
||||
@@ -27,7 +25,8 @@ $_activeGroup = trim($_GET['group'] ?? '');
|
||||
|
||||
<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_AS_LIST = <?= json_encode($_asList, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
||||
var FOLIO_IP_DATA = <?= json_encode($_ipData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
||||
</script>
|
||||
|
||||
<div class="card mb-4">
|
||||
@@ -98,73 +97,6 @@ var FOLIO_AS_LIST = <?= json_encode($_asList, JSON_UNESCAPED_SLASHES |
|
||||
|
||||
</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?>
|
||||
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ ob_start();
|
||||
$_cover = $_first['cover'] ?? '';
|
||||
$_cat = trim($_first['category'] ?? '');
|
||||
$_coverStyle = $_cover !== ''
|
||||
? "background-image:url('/file?uuid=" . rawurlencode($_first['uuid']) . "&name=" . rawurlencode($_cover) . "')"
|
||||
? "background-image:url('/file?uuid=" . rawurlencode($_first['uuid']) . '&name=' . rawurlencode($_cover) . "')"
|
||||
: 'background:' . coverGradient($_cat !== '' ? $_cat : $_first['uuid'], $allCats ?? []);
|
||||
?>
|
||||
?>
|
||||
<a href="/book/<?= rawurlencode($_book['slug']) ?>" class="book-home-card">
|
||||
<div class="book-home-card-cover" style="<?= $_coverStyle ?>"></div>
|
||||
<div class="book-home-card-body">
|
||||
@@ -42,7 +42,7 @@ ob_start();
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$content = ob_get_clean();
|
||||
$title = 'Livres — ' . siteTitle();
|
||||
$metaRobots = 'index, follow';
|
||||
$canonical = rtrim((string)($_ENV['APP_URL'] ?? getenv('APP_URL') ?: ''), '/') . '/books';
|
||||
|
||||
@@ -48,13 +48,14 @@
|
||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||
<?php
|
||||
$_pub = BASE_PATH . '/public/assets/';
|
||||
if (!function_exists('_av')) {
|
||||
function _av(string $base, string $rel): string {
|
||||
$f = $base . $rel;
|
||||
return '/assets/' . $rel . (is_file($f) ? '?v=' . substr(md5_file($f), 0, 8) : '');
|
||||
}
|
||||
if (!function_exists('_av')) {
|
||||
function _av(string $base, string $rel): string
|
||||
{
|
||||
$f = $base . $rel;
|
||||
return '/assets/' . $rel . (is_file($f) ? '?v=' . substr(md5_file($f), 0, 8) : '');
|
||||
}
|
||||
?>
|
||||
}
|
||||
?>
|
||||
<link rel="stylesheet" href="<?= _av($_pub, 'css/style.css') ?>">
|
||||
</head>
|
||||
|
||||
@@ -65,7 +66,7 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark mb-0" role="navigation" aria-label="Navigation principale">
|
||||
<div class="container-fluid">
|
||||
<?php
|
||||
$_layoutAction = $_GET['action'] ?? 'list';
|
||||
$_layoutAction = $_GET['action'] ?? 'list';
|
||||
$_layoutPrivateCats = isset($articles) ? $articles->getPrivateCategories() : [];
|
||||
$_layoutCats = isset($articles) ? array_filter(
|
||||
$articles->getCategories(),
|
||||
|
||||
@@ -228,9 +228,9 @@ function _renderCard(array $post, array $privateCats, array $allCats, \Parsedown
|
||||
$_cover = $_first['cover'] ?? '';
|
||||
$_cat = trim($_first['category'] ?? '');
|
||||
$_coverStyle = $_cover !== ''
|
||||
? "background-image:url('/file?uuid=" . rawurlencode($_first['uuid']) . "&name=" . rawurlencode($_cover) . "')"
|
||||
? "background-image:url('/file?uuid=" . rawurlencode($_first['uuid']) . '&name=' . rawurlencode($_cover) . "')"
|
||||
: 'background:' . coverGradient($_cat !== '' ? $_cat : $_first['uuid'], $allCats ?? []);
|
||||
?>
|
||||
?>
|
||||
<a href="/book/<?= rawurlencode($_book['slug']) ?>" class="book-home-card">
|
||||
<div class="book-home-card-cover" style="<?= $_coverStyle ?>"></div>
|
||||
<div class="book-home-card-body">
|
||||
@@ -275,7 +275,7 @@ function _renderCard(array $post, array $privateCats, array $allCats, \Parsedown
|
||||
<?php if ($prevCursor !== null || $nextCursor !== null): ?>
|
||||
<nav class="pagination-nav mt-5" aria-label="Navigation">
|
||||
<?php
|
||||
$hasCat = $filterCat !== '';
|
||||
$hasCat = $filterCat !== '';
|
||||
$catBase = $hasCat ? '/categorie/' . rawurlencode($filterCat) : null;
|
||||
?>
|
||||
<?php if ($prevCursor !== null): ?>
|
||||
|
||||
@@ -294,9 +294,9 @@ $hasSources = (!empty($externalLinks) || !empty($files))
|
||||
|
||||
<?php if ($article['published'] ?? false): ?>
|
||||
<?php
|
||||
$_shareUrl = rtrim(defined('APP_URL') ? APP_URL : '', '/') . '/post/' . rawurlencode($article['slug'] ?? '');
|
||||
$_shareTitle = $article['title'] ?? '';
|
||||
?>
|
||||
$_shareUrl = rtrim(defined('APP_URL') ? APP_URL : '', '/') . '/post/' . rawurlencode($article['slug'] ?? '');
|
||||
$_shareTitle = $article['title'] ?? '';
|
||||
?>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 my-3 py-2 border-top"
|
||||
id="share-bar"
|
||||
data-url="<?= htmlspecialchars($_shareUrl) ?>"
|
||||
@@ -442,8 +442,8 @@ $_shareTitle = $article['title'] ?? '';
|
||||
|
||||
<?php
|
||||
$_revisions = array_reverse($article['revisions'] ?? []);
|
||||
if (!empty($_revisions) && isLoggedIn()):
|
||||
?>
|
||||
if (!empty($_revisions) && isLoggedIn()):
|
||||
?>
|
||||
<h6 class="related-sidebar-title mt-3">Historique</h6>
|
||||
<ul class="toc-list small">
|
||||
<?php foreach (array_slice($_revisions, 0, 10) as $_rev): ?>
|
||||
|
||||
@@ -201,5 +201,7 @@ $_hasUuid = $_wizUuid !== '';
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = ($mode === 'create' ? 'Nouvel article' : 'Modifier') . ' — Étape 1/' . $totalSteps;
|
||||
if ($mode === 'edit') { $aiEditor = true; }
|
||||
if ($mode === 'edit') {
|
||||
$aiEditor = true;
|
||||
}
|
||||
include BASE_PATH . '/templates/layout.php';
|
||||
|
||||
Reference in New Issue
Block a user