Bannis : blocs AS repliables par défaut (fixes #47)
Chaque AS est replié au chargement (▶). Clic sur la ligne titre pour déplier/replier. Bouton "Tout déplier / Tout replier". La recherche dynamique déplie automatiquement les blocs qui contiennent un résultat. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
259e8d5f3f
commit
a01976cbe8
2 changed files with 37 additions and 5 deletions
|
|
@ -381,6 +381,9 @@ footer { background: var(--blue-dark); color: rgba(255,255,255,.6); margin-top:
|
||||||
.err-ignored-chip { display: inline-flex; align-items: center; gap: .35rem; background: #f3f4f6; border: 1px solid #e5e7eb; border-radius: 20px; padding: .2rem .6rem .2rem .8rem; font-size: .82rem; font-family: monospace; }
|
.err-ignored-chip { display: inline-flex; align-items: center; gap: .35rem; background: #f3f4f6; border: 1px solid #e5e7eb; border-radius: 20px; padding: .2rem .6rem .2rem .8rem; font-size: .82rem; font-family: monospace; }
|
||||||
.err-ignored-remove { background: none; border: none; cursor: pointer; font-size: .75rem; color: #9ca3af; line-height: 1; padding: 0; transition: color .15s; }
|
.err-ignored-remove { background: none; border: none; cursor: pointer; font-size: .75rem; color: #9ca3af; line-height: 1; padding: 0; transition: color .15s; }
|
||||||
.err-ignored-remove:hover { color: #ef4444; }
|
.err-ignored-remove:hover { color: #ef4444; }
|
||||||
|
.banned-as-row td { cursor: pointer; }
|
||||||
|
.banned-as-row td:last-child { cursor: default; }
|
||||||
|
.banned-toggle { display: inline-block; width: 1rem; font-size: .8rem; color: var(--muted); user-select: none; }
|
||||||
.banned-table .col-asn { width: 7rem; }
|
.banned-table .col-asn { width: 7rem; }
|
||||||
.banned-table .col-country { width: 4rem; text-align: center; }
|
.banned-table .col-country { width: 4rem; text-align: center; }
|
||||||
.banned-as-row td { background: var(--blue-light); border-top: 2px solid var(--border); }
|
.banned-as-row td { background: var(--blue-light); border-top: 2px solid var(--border); }
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@
|
||||||
<div class="err-search-wrap" style="margin-top:.5rem">
|
<div class="err-search-wrap" style="margin-top:.5rem">
|
||||||
<input type="search" id="ban-search" class="err-search" placeholder="Filtrer par IP, CIDR ou nom d'AS…" autocomplete="off">
|
<input type="search" id="ban-search" class="err-search" placeholder="Filtrer par IP, CIDR ou nom d'AS…" autocomplete="off">
|
||||||
<span class="err-search-count" id="ban-search-count"></span>
|
<span class="err-search-count" id="ban-search-count"></span>
|
||||||
|
<button id="btn-ban-expand-all" type="button" class="btn btn-sm">▼ Tout déplier</button>
|
||||||
</div>
|
</div>
|
||||||
<table class="file-table banned-table">
|
<table class="file-table banned-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -307,8 +308,9 @@
|
||||||
for (const g of data.groups) {
|
for (const g of data.groups) {
|
||||||
const asnLabel = g.asn ? `<strong>AS${escHtml(g.asn)}</strong>` : '<strong>AS inconnu</strong>';
|
const asnLabel = g.asn ? `<strong>AS${escHtml(g.asn)}</strong>` : '<strong>AS inconnu</strong>';
|
||||||
const n = g.entries.length;
|
const n = g.entries.length;
|
||||||
html += `<tr class="banned-as-row" data-asn="${escHtml(g.asn)}" data-name="${escHtml((g.name||'').toLowerCase())}">
|
html += `<tr class="banned-as-row" data-asn="${escHtml(g.asn)}" data-name="${escHtml((g.name||'').toLowerCase())}" data-open="false">
|
||||||
<td colspan="4" class="banned-as-cell">
|
<td colspan="4" class="banned-as-cell">
|
||||||
|
<span class="banned-toggle">▶</span>
|
||||||
<span class="banned-as-label">${asnLabel}${g.name ? ' · '+escHtml(g.name) : ''}${g.country ? ' <span class="banned-country">['+escHtml(g.country)+']</span>' : ''}</span>
|
<span class="banned-as-label">${asnLabel}${g.name ? ' · '+escHtml(g.name) : ''}${g.country ? ' <span class="banned-country">['+escHtml(g.country)+']</span>' : ''}</span>
|
||||||
<span class="err-total-badge" style="margin-left:.5rem">${n} entrée${n!==1?'s':''}</span>
|
<span class="err-total-badge" style="margin-left:.5rem">${n} entrée${n!==1?'s':''}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -317,7 +319,7 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
for (const entry of g.entries) {
|
for (const entry of g.entries) {
|
||||||
html += `<tr class="banned-entry-row" data-asn="${escHtml(g.asn)}" data-entry="${escHtml(entry)}">
|
html += `<tr class="banned-entry-row" data-asn="${escHtml(g.asn)}" data-entry="${escHtml(entry)}" style="display:none">
|
||||||
<td colspan="4"><code class="err-ip">${escHtml(entry)}</code></td>
|
<td colspan="4"><code class="err-ip">${escHtml(entry)}</code></td>
|
||||||
<td class="col-actions">
|
<td class="col-actions">
|
||||||
<button type="button" class="btn-unban btn btn-sm" data-entry="${escHtml(entry)}" title="Débloquer ${escHtml(entry)}">🔓</button>
|
<button type="button" class="btn-unban btn btn-sm" data-entry="${escHtml(entry)}" title="Débloquer ${escHtml(entry)}">🔓</button>
|
||||||
|
|
@ -329,6 +331,31 @@
|
||||||
document.getElementById('ban-content').style.display = '';
|
document.getElementById('ban-content').style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Replier/déplier un bloc AS ── */
|
||||||
|
function toggleAsBlock(asRow, forceOpen) {
|
||||||
|
const open = forceOpen !== undefined ? !forceOpen : asRow.dataset.open === 'true';
|
||||||
|
const nowOpen = !open;
|
||||||
|
asRow.dataset.open = nowOpen;
|
||||||
|
asRow.querySelector('.banned-toggle').textContent = nowOpen ? '▼' : '▶';
|
||||||
|
document.querySelectorAll(`#ban-tbody .banned-entry-row[data-asn="${asRow.dataset.asn}"]`).forEach(r => {
|
||||||
|
r.style.display = nowOpen ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
if (e.target.closest('button')) return;
|
||||||
|
const asRow = e.target.closest('#ban-tbody .banned-as-row');
|
||||||
|
if (asRow) toggleAsBlock(asRow);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Tout déplier / Tout replier ── */
|
||||||
|
let allExpanded = false;
|
||||||
|
document.getElementById('btn-ban-expand-all')?.addEventListener('click', () => {
|
||||||
|
allExpanded = !allExpanded;
|
||||||
|
document.querySelectorAll('#ban-tbody .banned-as-row').forEach(r => toggleAsBlock(r, !allExpanded));
|
||||||
|
document.getElementById('btn-ban-expand-all').textContent = allExpanded ? '▲ Tout replier' : '▼ Tout déplier';
|
||||||
|
});
|
||||||
|
|
||||||
/* ── Filtre bannis ── */
|
/* ── Filtre bannis ── */
|
||||||
const banSearch = document.getElementById('ban-search');
|
const banSearch = document.getElementById('ban-search');
|
||||||
const banCount = document.getElementById('ban-search-count');
|
const banCount = document.getElementById('ban-search-count');
|
||||||
|
|
@ -336,14 +363,16 @@
|
||||||
function applyBanFilter() {
|
function applyBanFilter() {
|
||||||
const q = banSearch?.value.trim().toLowerCase() || '';
|
const q = banSearch?.value.trim().toLowerCase() || '';
|
||||||
let vis = 0;
|
let vis = 0;
|
||||||
document.querySelectorAll('#tab-banned .banned-as-row').forEach(asRow => {
|
document.querySelectorAll('#ban-tbody .banned-as-row').forEach(asRow => {
|
||||||
const asn = (asRow.dataset.asn || '').toLowerCase();
|
const asn = (asRow.dataset.asn || '').toLowerCase();
|
||||||
const name = (asRow.dataset.name || '').toLowerCase();
|
const name = (asRow.dataset.name || '').toLowerCase();
|
||||||
const rows = [...document.querySelectorAll(`#tab-banned .banned-entry-row[data-asn="${asRow.dataset.asn}"]`)];
|
const open = asRow.dataset.open === 'true';
|
||||||
|
const rows = [...document.querySelectorAll(`#ban-tbody .banned-entry-row[data-asn="${asRow.dataset.asn}"]`)];
|
||||||
let any = false;
|
let any = false;
|
||||||
rows.forEach(er => {
|
rows.forEach(er => {
|
||||||
const match = !q || er.dataset.entry.toLowerCase().includes(q) || asn.includes(q) || name.includes(q);
|
const match = !q || er.dataset.entry.toLowerCase().includes(q) || asn.includes(q) || name.includes(q);
|
||||||
er.style.display = match ? '' : 'none';
|
// avec filtre : afficher les matchs même si replié ; sans filtre : respecter l'état
|
||||||
|
er.style.display = (q ? match : open) ? '' : 'none';
|
||||||
if (match) { any = true; vis++; }
|
if (match) { any = true; vis++; }
|
||||||
});
|
});
|
||||||
asRow.style.display = (!q || any) ? '' : 'none';
|
asRow.style.display = (!q || any) ? '' : 'none';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue