feat(erreurs): recherche dynamique par chemin ou IP (#42)

Champ de filtre en temps réel au-dessus du tableau des 404 ;
ferme le panneau de détail des lignes masquées.
Corrige aussi rsync via sudo pour préserver les droits static-cdn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alpinux 2026-05-06 13:21:02 +02:00
parent abf13db0e6
commit 3a6f363e1d
5 changed files with 45 additions and 4 deletions

View file

@ -1,5 +1,12 @@
# Changelog — Alpinux Static # Changelog — Alpinux Static
## [1.5.2] — 2026-05-06
### Ajouté
- Erreurs 404 : champ de recherche dynamique — filtre les lignes par chemin ou adresse IP à chaque frappe, avec compteur de résultats
---
## [1.5.1] — 2026-05-06 ## [1.5.1] — 2026-05-06
### Corrigé ### Corrigé

View file

@ -1 +1 @@
1.5.1 1.5.2

View file

@ -326,6 +326,10 @@ footer { background: var(--blue-dark); color: rgba(255,255,255,.6); margin-top:
.err-header h2 { margin: 0; } .err-header h2 { margin: 0; }
.err-total-badge { font-size: .75rem; font-weight: 400; color: var(--muted); margin-left: .5rem; } .err-total-badge { font-size: .75rem; font-weight: 400; color: var(--muted); margin-left: .5rem; }
.btn-sm { font-size: .8rem; padding: .3rem .7rem; background: var(--blue-light); color: var(--blue); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; } .btn-sm { font-size: .8rem; padding: .3rem .7rem; background: var(--blue-light); color: var(--blue); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; }
.err-search-wrap { display: flex; align-items: center; gap: .75rem; margin-bottom: .75rem; }
.err-search { flex: 1; max-width: 380px; padding: .4rem .75rem; border: 1px solid var(--border); border-radius: 8px; font-size: .9rem; background: var(--bg); color: var(--text); }
.err-search:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 2px var(--blue-light); }
.err-search-count { font-size: .8rem; color: var(--muted); }
.err-table .err-path code { font-size: .82rem; color: var(--text); word-break: break-all; } .err-table .err-path code { font-size: .82rem; color: var(--text); word-break: break-all; }
.col-err-status { width: 1.5rem; text-align: center; } .col-err-status { width: 1.5rem; text-align: center; }

View file

@ -18,6 +18,10 @@
{% if not entries %} {% if not entries %}
<p style="color:var(--muted); margin-top:.5rem">Aucune erreur 404 dans les logs récents.</p> <p style="color:var(--muted); margin-top:.5rem">Aucune erreur 404 dans les logs récents.</p>
{% else %} {% else %}
<div class="err-search-wrap">
<input type="search" id="err-search" class="err-search" placeholder="Filtrer par chemin ou adresse IP…" autocomplete="off">
<span class="err-search-count" id="err-search-count"></span>
</div>
<table class="file-table err-table"> <table class="file-table err-table">
<thead> <thead>
<tr> <tr>
@ -31,7 +35,7 @@
</thead> </thead>
<tbody> <tbody>
{% for path, info in entries %} {% for path, info in entries %}
<tr class="err-row" data-path="{{ path }}"> <tr class="err-row" data-path="{{ path }}" data-ips="{{ info.ips.keys() | join(' ') }}">
<td class="col-err-status"> <td class="col-err-status">
<span class="err-status-dot err-status-dot--{% if not info.last_seen %}unk{% elif (info.ips | length) > 0 %}active{% else %}ok{% endif %}" <span class="err-status-dot err-status-dot--{% if not info.last_seen %}unk{% elif (info.ips | length) > 0 %}active{% else %}ok{% endif %}"
data-path="{{ path }}" data-path="{{ path }}"
@ -80,6 +84,32 @@
const IGNORE_URL = {{ url_for('errors_ignore') | tojson }}; const IGNORE_URL = {{ url_for('errors_ignore') | tojson }};
const BAN_URL = {{ url_for('errors_ban') | tojson }}; const BAN_URL = {{ url_for('errors_ban') | tojson }};
/* ── Search / filter ── */
const searchInput = document.getElementById('err-search');
const searchCount = document.getElementById('err-search-count');
const allRows = [...document.querySelectorAll('.err-row')];
function applyFilter() {
const q = searchInput.value.trim().toLowerCase();
let visible = 0;
allRows.forEach(row => {
const match = !q
|| row.dataset.path.toLowerCase().includes(q)
|| row.dataset.ips.toLowerCase().includes(q);
const detailRow = row.nextElementSibling;
row.style.display = match ? '' : 'none';
if (!match && detailRow && detailRow.classList.contains('err-detail-row')) {
detailRow.style.display = 'none';
row.querySelector('.btn-detail').textContent = '▼';
if (openRow === detailRow) openRow = null;
}
if (match) visible++;
});
searchCount.textContent = q ? `${visible} / ${allRows.length}` : '';
}
if (searchInput) searchInput.addEventListener('input', applyFilter);
/* ── Expand/collapse detail ── */ /* ── Expand/collapse detail ── */
let openRow = null; let openRow = null;

View file

@ -48,8 +48,8 @@ if $DRY_RUN; then
exit 0 exit 0
fi fi
ssh "$REMOTE_HOST" "sudo mkdir -p $REMOTE_DEST && sudo chown static-cdn:static-cdn $REMOTE_DEST" ssh "$REMOTE_HOST" "sudo mkdir -p $REMOTE_DEST"
rsync "${RSYNC_OPTS[@]}" "$APP_DIR/" "$REMOTE_HOST:$REMOTE_DEST/" rsync "${RSYNC_OPTS[@]}" --rsync-path="sudo rsync" "$APP_DIR/" "$REMOTE_HOST:$REMOTE_DEST/"
ssh "$REMOTE_HOST" "sudo chown -R static-cdn:static-cdn $REMOTE_DEST" ssh "$REMOTE_HOST" "sudo chown -R static-cdn:static-cdn $REMOTE_DEST"
echo -e " ${GREEN}✓ Fichiers copiés${RESET}" echo -e " ${GREEN}✓ Fichiers copiés${RESET}"