alpinux-static/app/templates/trash.html
Alpinux a6d7bc2c8a feat: v2.0.0 — 14 tickets implémentés
#53 résumé req/IPs par statut dans Erreurs 404
#54 titre onglet avec compteur non résolus
#2  Tout/Aucun dans resize (tailles + formats)
#7  backup filename affiché dans résultats resize
#8  flash message résumé après upload
#6  renommage inline sur preview_text + preview_other
#23 filtre + tri par nom/taille/date dans corbeille
#20 sélection multiple + batch restore/delete corbeille
#45 /sitemap.xml (assets publics)
#52 ignoreip fail2ban sync sur Ignorer/Retirer une IP
#1  cairosvg>=2.7 dans requirements.txt
#51 ignored_ips.json exclu du rsync --delete
#48 as_cache/ exclu du rsync --delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 21:04:03 +02:00

198 lines
7.8 KiB
HTML

{% extends "base.html" %}
{% block title %}Corbeille{% endblock %}
{% block content %}
<section class="card">
<div class="trash-header">
<h2>Corbeille
{% if entries %}
<span class="trash-count-label">— {{ entries|length }} fichier{{ 's' if entries|length > 1 else '' }}</span>
{% endif %}
</h2>
{% if entries %}
<form method="post" action="{{ url_for('trash_empty') }}"
onsubmit="return confirm('Vider définitivement toute la corbeille ?')">
<button type="submit" class="btn btn-danger">Vider la corbeille</button>
</form>
{% endif %}
</div>
<p class="trash-info">Les fichiers sont supprimés définitivement après 30 jours.</p>
{% if entries %}
<div class="trash-toolbar">
<input type="search" id="trash-filter" class="trash-filter" placeholder="Filtrer par nom…" autocomplete="off">
<button type="button" class="trash-sort-btn active" data-sort="date-desc">Date ▼</button>
<button type="button" class="trash-sort-btn" data-sort="name-asc">Nom ↑</button>
<button type="button" class="trash-sort-btn" data-sort="size-desc">Taille ▼</button>
</div>
<div id="trash-batch-bar" class="trash-batch-bar">
<span class="trash-select-count" id="batch-count">0 sélectionné(s)</span>
<button type="button" id="btn-restore-batch" class="btn btn-sm">♻️ Restaurer</button>
<button type="button" id="btn-delete-batch" class="btn btn-sm btn-danger">🗑 Supprimer</button>
</div>
{% set mode = 'trash' %}
{% include '_file_table.html' %}
{% else %}
<p class="empty">La corbeille est vide.</p>
{% endif %}
</section>
<div id="restore-conflict-panel" class="conflict-panel" style="display:none">
<p class="conflict-title">⚠ Un fichier existe déjà à cet emplacement : <strong id="restore-conflict-name"></strong></p>
<div class="resize-chips resize-chips--radio">
<label class="chip"><input type="radio" name="restore-conflict" value="overwrite" checked><span>Écraser</span></label>
<label class="chip"><input type="radio" name="restore-conflict" value="rename"><span>Renommer</span></label>
</div>
<div class="conflict-actions">
<button type="button" id="restore-confirm" class="btn btn-primary">Confirmer la restauration</button>
<button type="button" id="restore-cancel" class="btn-rename-cancel">Annuler</button>
</div>
</div>
<script>
(function () {
const RESTORE_URL = {{ url_for('trash_restore') | tojson }};
const RESTORE_BATCH_URL = {{ url_for('trash_restore_batch') | tojson }};
const DELETE_BATCH_URL = {{ url_for('trash_delete_batch') | tojson }};
const conflictPanel = document.getElementById('restore-conflict-panel');
const conflictName = document.getElementById('restore-conflict-name');
const confirmBtn = document.getElementById('restore-confirm');
const cancelBtn = document.getElementById('restore-cancel');
let pendingPath = null;
async function doRestore(path, conflict) {
const fd = new FormData();
fd.append('path', path);
if (conflict) fd.append('conflict', conflict);
try {
const resp = await fetch(RESTORE_URL, { method: 'POST', body: fd });
if (resp.status === 409) {
pendingPath = path;
conflictName.textContent = path.split('/').pop();
conflictPanel.style.display = 'block';
conflictPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
return;
}
if (resp.redirected) { window.location.href = resp.url; return; }
if (resp.ok) { window.location.href = resp.url || window.location.href; return; }
alert('Erreur lors de la restauration.');
} catch (_) {
alert('Erreur réseau.');
}
}
document.addEventListener('click', e => {
const btn = e.target.closest('.btn-restore');
if (!btn) return;
doRestore(btn.dataset.path, '');
});
confirmBtn.addEventListener('click', () => {
const strategy = document.querySelector('input[name="restore-conflict"]:checked')?.value || 'overwrite';
conflictPanel.style.display = 'none';
if (pendingPath) doRestore(pendingPath, strategy);
pendingPath = null;
});
cancelBtn.addEventListener('click', () => {
conflictPanel.style.display = 'none';
pendingPath = null;
});
/* ── Filtre ── */
const filterIn = document.getElementById('trash-filter');
const tbody = document.querySelector('#file-table tbody');
filterIn?.addEventListener('input', () => {
const q = filterIn.value.trim().toLowerCase();
tbody?.querySelectorAll('tr').forEach(row => {
row.style.display = (!q || (row.dataset.name || '').includes(q)) ? '' : 'none';
});
updateBatchBar();
});
/* ── Tri ── */
let currentSort = 'date-desc';
document.querySelectorAll('.trash-sort-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.trash-sort-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentSort = btn.dataset.sort;
sortTable(currentSort);
});
});
function sortTable(key) {
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
if (key === 'name-asc') return (a.dataset.name || '').localeCompare(b.dataset.name || '');
if (key === 'size-desc') return parseInt(b.dataset.size || 0, 10) - parseInt(a.dataset.size || 0, 10);
/* date-desc */ return (b.dataset.date || '').localeCompare(a.dataset.date || '');
});
rows.forEach(r => tbody.appendChild(r));
}
/* ── Sélection multiple ── */
const selectAll = document.getElementById('select-all-cb');
const batchBar = document.getElementById('trash-batch-bar');
const batchCount = document.getElementById('batch-count');
function selectedCbs() {
return Array.from(document.querySelectorAll('.row-cb:checked'));
}
function updateBatchBar() {
const sel = selectedCbs().length;
if (batchBar) batchBar.classList.toggle('visible', sel > 0);
if (batchCount) batchCount.textContent = `${sel} sélectionné(s)`;
if (selectAll) {
const all = document.querySelectorAll('.row-cb:not(:disabled)').length;
selectAll.indeterminate = sel > 0 && sel < all;
selectAll.checked = sel > 0 && sel === all;
}
}
selectAll?.addEventListener('change', () => {
document.querySelectorAll('.row-cb').forEach(cb => {
const row = cb.closest('tr');
if (!row || row.style.display !== 'none') cb.checked = selectAll.checked;
});
updateBatchBar();
});
document.addEventListener('change', e => {
if (e.target.classList.contains('row-cb')) updateBatchBar();
});
/* ── Restaurer la sélection ── */
document.getElementById('btn-restore-batch')?.addEventListener('click', async () => {
const paths = selectedCbs().map(cb => cb.dataset.path);
if (!paths.length) return;
const fd = new FormData();
paths.forEach(p => fd.append('paths', p));
try {
const j = await fetch(RESTORE_BATCH_URL, { method: 'POST', body: fd }).then(r => r.json());
const msg = `${j.ok || 0} fichier(s) restauré(s)` + (j.conflict?.length ? `, ${j.conflict.length} conflit(s) ignoré(s)` : '') + (j.error ? `, ${j.error} erreur(s)` : '');
alert(msg);
location.reload();
} catch { alert('Erreur réseau.'); }
});
/* ── Supprimer la sélection ── */
document.getElementById('btn-delete-batch')?.addEventListener('click', async () => {
const cbs = selectedCbs();
if (!cbs.length) return;
const names = cbs.map(cb => cb.dataset.name).join(', ');
if (!confirm(`Supprimer définitivement ${cbs.length} fichier(s) ?\n${names}`)) return;
const fd = new FormData();
cbs.forEach(cb => fd.append('paths', cb.dataset.path));
try {
await fetch(DELETE_BATCH_URL, { method: 'POST', body: fd });
location.reload();
} catch { alert('Erreur réseau.'); }
});
})();
</script>
{% endblock %}