Add SQLite indexes for contenus page language/tag queries + WIP: Peertube orphans, dialogs, contact decoupling, context note, finality types

This commit is contained in:
Pontoporeia
2026-06-21 13:33:55 +02:00
parent 0d5e9dac19
commit 03c9c3566f
38 changed files with 1432 additions and 333 deletions

View File

@@ -41,7 +41,7 @@ document.addEventListener('htmx:afterSwap',()=>{document.querySelectorAll('input
<?php endif; ?>
<?php if ($tmpTotalCount > 0): ?>
<button type="button" class="btn btn--sm btn--secondary" id="tmp-cleanup-btn"
onclick="document.getElementById('tmp-cleanup-dialog').showModal(); fetchTmpStats()">
onclick="document.getElementById('tmp-cleanup-dialog').showModal(); htmx.trigger('#tmp-cleanup-stats','loadStats'); htmx.trigger('#peertube-orphans-wrapper','loadPeertube')">
Nettoyer (<?= $tmpTotalCount ?>)
</button>
<?php endif; ?>
@@ -97,289 +97,14 @@ document.addEventListener('htmx:afterSwap',()=>{document.querySelectorAll('input
<?php include APP_ROOT . '/templates/admin/index-table.php'; ?>
</main>
<!-- ══════════════════════════════════════════════════════════════
CONFIRM DIALOGS (replacing browser alert/confirm)
══════════════════════════════════════════════════════════════ -->
<?php include APP_ROOT . '/templates/admin/partials/dialogs/no-selection.php'; ?>
<?php include APP_ROOT . '/templates/admin/partials/dialogs/bulk-confirm.php'; ?>
<?php include APP_ROOT . '/templates/admin/partials/dialogs/bulk-delete.php'; ?>
<?php include APP_ROOT . '/templates/admin/partials/dialogs/delete-thesis.php'; ?>
<!-- No-selection alert -->
<dialog id="no-selection-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="no-sel-title">
<div class="admin-dialog__header">
<h2 id="no-sel-title">Aucune sélection</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Sélectionnez au moins un TFE avant d'effectuer une action groupée.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--primary" onclick="this.closest('dialog').close()">OK</button>
</div>
</dialog>
<?php include APP_ROOT . '/templates/admin/partials/dialogs/import.php'; ?>
<!-- Bulk publish/unpublish confirm -->
<dialog id="bulk-confirm-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="bulk-confirm-title">
<div class="admin-dialog__header">
<h2 id="bulk-confirm-title">Confirmation</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p><span id="bulk-confirm-word"></span> <span id="bulk-confirm-count"></span> TFE(s) ?</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--primary" onclick="this.closest('dialog').close(); execBulk()">Confirmer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>
<!-- Bulk delete confirm -->
<dialog id="bulk-delete-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="bulk-delete-title">
<div class="admin-dialog__header">
<h2 id="bulk-delete-title">Supprimer des TFE</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Supprimer définitivement <strong><span id="bulk-delete-count"></span> TFE(s)</strong> ? Cette action est irréversible.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--danger" onclick="this.closest('dialog').close(); execBulk()">Supprimer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>
<!-- Single thesis delete confirm -->
<dialog id="delete-thesis-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="delete-thesis-title-label">
<div class="admin-dialog__header">
<h2 id="delete-thesis-title-label">Supprimer ce TFE</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Supprimer « <strong id="delete-thesis-title"></strong> » ? Cette action est irréversible.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--danger" id="delete-dialog-confirm" onclick="this.closest('dialog').close()">Supprimer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>
<!-- ══════════════════════════════════════════════════════════════
IMPORT DIALOG
══════════════════════════════════════════════════════════════ -->
<dialog id="import-dialog" class="admin-dialog" aria-labelledby="import-dialog-title">
<div class="admin-dialog__header">
<h2 id="import-dialog-title">Importer une liste de TFE</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="document.getElementById('import-dialog').close()">&#x2715;</button>
</div>
<?php if ($importMessage || !empty($importErrors)): ?>
<div class="admin-import-status-card">
<?php if (!empty($importErrors)): ?>
<div class="toast admin-import-status-card__errors" role="alert" data-type="error">
<strong>⚠ Erreurs :</strong>
<ul class="admin-error-list">
<?php foreach ($importErrors as $err): ?>
<li><?= htmlspecialchars($err) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($importMessage): ?>
<p class="admin-import-status-card__success" role="status">✓ <?= htmlspecialchars($importMessage) ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($importMessage): ?>
<?php if (!empty($importResults)): ?>
<details class="admin-import-log-details">
<summary>Logs d'importation (<?= count($importResults) ?> entrées)</summary>
<ul class="admin-import-log">
<?php foreach ($importResults as $r): ?>
<li class="admin-import-log__item admin-import-log__item--<?= $r['type'] ?>"><?= htmlspecialchars($r['msg']) ?></li>
<?php endforeach; ?>
</ul>
</details>
<?php endif; ?>
<div class="admin-form-footer">
<button type="button" class="btn btn--primary"
onclick="document.getElementById('import-dialog').close(); window.location.href = window.location.pathname">Terminé</button>
</div>
<?php else: ?>
<form method="post" enctype="multipart/form-data" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div>
<label for="csv_file">Fichier CSV</label>
<div class="admin-file-input">
<input type="file" id="csv_file"
name="csv_file"
class="tfe-file-picker"
data-queue-type="csv_import"
required>
<small class="admin-file-hint">
Colonnes : Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU<br>
Quatre premières lignes ignorées — Séparateur : virgule — UTF-8
</small>
</div>
</div>
<div class="admin-form-footer">
<button type="submit" class="btn btn--primary">Importer</button>
<button type="button" class="btn btn--secondary"
onclick="document.getElementById('import-dialog').close()">Annuler</button>
</div>
</form>
<?php if (!empty($importResults)): ?>
<details class="admin-import-log-details">
<summary>Logs d'importation (<?= count($importResults) ?> entrées)</summary>
<ul class="admin-import-log">
<?php foreach ($importResults as $r): ?>
<li class="admin-import-log__item admin-import-log__item--<?= $r['type'] ?>"><?= htmlspecialchars($r['msg']) ?></li>
<?php endforeach; ?>
</ul>
</details>
<?php endif; ?>
<?php endif; ?>
</dialog>
<!-- ══════════════════════════════════════════════════════════════
TMP CLEANUP DIALOG
══════════════════════════════════════════════════════════════ -->
<dialog id="tmp-cleanup-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="tmp-cleanup-title">
<div class="admin-dialog__header">
<h2 id="tmp-cleanup-title">Nettoyer les fichiers temporaires</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="document.getElementById('tmp-cleanup-dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__body">
<p>Les fichiers temporaires s'accumulent lorsque des téléversements sont abandonnés (formulaire fermé avant envoi).</p>
<div id="tmp-cleanup-stats" class="admin-dialog__stats">
Chargement…
</div>
<p class="admin-dialog__hint">
Seuls les fichiers de plus de 2 heures (FilePond) et 30 jours (corbeille) seront supprimés.
Les téléversements récents sont conservés.
</p>
<div id="tmp-cleanup-result" style="display:none"></div>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--danger" id="tmp-cleanup-confirm"
onclick="executeTmpCleanup()">
Nettoyer
</button>
<button type="button" class="btn btn--secondary"
onclick="document.getElementById('tmp-cleanup-dialog').close()">Annuler</button>
</div>
</dialog>
<script>
async function fetchTmpStats() {
const el = document.getElementById('tmp-cleanup-stats');
try {
const resp = await fetch('/admin/actions/cleanup-stats.php');
const data = await resp.json();
let html = '';
const totalStale = data.filepond_stale_count + data.trash_stale_count;
if (totalStale === 0) {
html = '<p style="margin:0;color:var(--accent-green)">✓ Aucun fichier obsolète à nettoyer.</p>';
if ((data.filepond_active_count || 0) + (data.trash_active_count || 0) > 0) {
html += '<p style="margin:var(--space-xs) 0 0 0;font-size:0.85em;color:var(--text-secondary)">';
if (data.filepond_active_count) html += `📁 ${data.filepond_active_count} téléversement(s) actif(s) (session existante) — ${data.filepond_active_human}<br>`;
if (data.trash_active_count) html += `🗑️ ${data.trash_active_count} fichier(s) récent(s) en corbeille — ${data.trash_active_human}`;
html += '</p>';
}
} else {
html = `<p style="margin:0 0 var(--space-xs) 0;font-weight:600">⚠️ ${totalStale} élément(s) obsolète(s) à nettoyer :</p>`;
if (data.filepond_stale_count) {
html += `<details style="margin:0 0 var(--space-xs) 0;font-size:0.9em" open><summary>📁 <strong>Téléversements abandonnés</strong> : ${data.filepond_stale_count} dossier(s) — ${data.filepond_stale_human} <span style="font-size:0.85em;color:var(--text-secondary)">(session expirée ou >2h)</span></summary>`;
if (data.filepond_stale_files) {
html += '<ul style="margin:var(--space-xs) 0 0 var(--space-md);max-height:10em;overflow-y:auto">';
data.filepond_stale_files.forEach(f => { html += `<li>${f.name} <span style="color:var(--text-secondary)">(${f.human}, ~${Math.round(f.age_minutes)}min)</span></li>`; });
html += '</ul>';
}
html += '</details>';
}
if (data.trash_stale_count) {
html += `<details style="margin:0 0 var(--space-xs) 0;font-size:0.9em" open><summary>🗑️ <strong>Fichiers supprimés orphelins</strong> : ${data.trash_stale_count} fichier(s) — ${data.trash_stale_human} <span style="font-size:0.85em;color:var(--text-secondary)">(référence DB disparue ou >30j)</span></summary>`;
if (data.trash_stale_files) {
html += '<ul style="margin:var(--space-xs) 0 0 var(--space-md);max-height:10em;overflow-y:auto">';
data.trash_stale_files.forEach(f => { html += `<li>${f.name} <span style="color:var(--text-secondary)">(${f.human}, ~${Math.round(f.age_days)}j)</span></li>`; });
html += '</ul>';
}
html += '</details>';
}
if (data.filepond_active_count || data.trash_active_count) {
html += '<p style="margin:var(--space-xs) 0 0 0;font-size:0.85em;color:var(--text-secondary)">Conservés : ';
if (data.filepond_active_count) html += `${data.filepond_active_count} téléversement(s) actif(s), `;
if (data.trash_active_count) html += `${data.trash_active_count} fichier(s) récent(s)`;
html += '</p>';
}
}
el.innerHTML = html;
if (totalStale === 0) {
document.getElementById('tmp-cleanup-confirm').disabled = true;
document.getElementById('tmp-cleanup-confirm').textContent = 'Rien à nettoyer';
}
} catch (e) {
el.textContent = 'Erreur lors du chargement des statistiques.';
}
}
async function executeTmpCleanup() {
const btn = document.getElementById('tmp-cleanup-confirm');
const result = document.getElementById('tmp-cleanup-result');
btn.disabled = true;
btn.textContent = 'Nettoyage…';
try {
const resp = await fetch('/admin/actions/cleanup-tmp.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'csrf_token=' + encodeURIComponent('<?= htmlspecialchars($_SESSION['csrf_token']) ?>')
});
const data = await resp.json();
result.style.display = 'block';
if (data.success) {
const total = data.filepond_removed + data.trash_removed;
if (total === 0) {
result.className = 'flash-success';
result.innerHTML = '✓ Aucun fichier obsolète trouvé.';
} else {
result.className = 'flash-success';
let msg = `✓ ${total} élément(s) supprimé(s) : `;
if (data.filepond_removed) msg += `${data.filepond_removed} téléversement(s) abandonné(s), `;
if (data.trash_removed) msg += `${data.trash_removed} fichier(s) orphelin(s)`;
result.innerHTML = msg;
if (data.details && data.details.length > 0) {
result.innerHTML += '<details style="margin-top:var(--space-xs);font-size:0.85em"><summary>Détails (' + data.details.length + ')</summary><ul style="margin:var(--space-xs) 0 0 var(--space-md)">' +
data.details.map(d => '<li>' + d + '</li>').join('') + '</ul></details>';
}
}
setTimeout(() => {
document.getElementById('tmp-cleanup-dialog').close();
window.location.reload();
}, 2000);
} else {
result.className = 'flash-error';
result.textContent = 'Erreur : ' + (data.error || 'inconnue');
btn.disabled = false;
btn.textContent = 'Réessayer';
}
} catch (e) {
result.style.display = 'block';
result.className = 'flash-error';
result.textContent = 'Erreur réseau.';
btn.disabled = false;
btn.textContent = 'Réessayer';
}
}
</script>
<?php include APP_ROOT . '/templates/admin/partials/dialogs/tmp-cleanup.php'; ?>
<?php if ($importMessage || !empty($importErrors)): ?>
<script>document.getElementById('import-dialog').showModal();</script>

View File

@@ -0,0 +1,14 @@
<dialog id="bulk-confirm-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="bulk-confirm-title">
<div class="admin-dialog__header">
<h3 id="bulk-confirm-title">Confirmation</h3>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p><span id="bulk-confirm-word"></span> <span id="bulk-confirm-count"></span> TFE(s)&nbsp;?</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--primary" onclick="this.closest('dialog').close(); execBulk()">Confirmer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>

View File

@@ -0,0 +1,14 @@
<dialog id="bulk-delete-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="bulk-delete-title">
<div class="admin-dialog__header">
<h3 id="bulk-delete-title">Supprimer des TFE</h3>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Supprimer définitivement <strong><span id="bulk-delete-count"></span> TFE(s)</strong>&nbsp;? Cette action est irréversible.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--danger" onclick="this.closest('dialog').close(); execBulk()">Supprimer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>

View File

@@ -0,0 +1,14 @@
<dialog id="delete-thesis-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="delete-thesis-title-label">
<div class="admin-dialog__header">
<h3 id="delete-thesis-title-label">Supprimer ce TFE</h3>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Supprimer &laquo;&nbsp;<strong id="delete-thesis-title"></strong>&nbsp;&raquo;&nbsp;? Cette action est irréversible.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--danger" id="delete-dialog-confirm" onclick="this.closest('dialog').close()">Supprimer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>

View File

@@ -0,0 +1,78 @@
<dialog id="import-dialog" class="admin-dialog" aria-labelledby="import-dialog-title">
<div class="admin-dialog__header">
<h3 id="import-dialog-title">Importer une liste de TFE</h3>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="document.getElementById('import-dialog').close()">&#x2715;</button>
</div>
<?php if ($importMessage || !empty($importErrors)): ?>
<div class="admin-import-status-card">
<?php if (!empty($importErrors)): ?>
<div class="toast admin-import-status-card__errors" role="alert" data-type="error">
<strong>⚠ Erreurs :</strong>
<ul class="admin-error-list">
<?php foreach ($importErrors as $err): ?>
<li><?= htmlspecialchars($err) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($importMessage): ?>
<p class="admin-import-status-card__success" role="status">✓ <?= htmlspecialchars($importMessage) ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($importMessage): ?>
<?php if (!empty($importResults)): ?>
<details class="admin-import-log-details">
<summary>Logs d'importation (<?= count($importResults) ?> entrées)</summary>
<ul class="admin-import-log">
<?php foreach ($importResults as $r): ?>
<li class="admin-import-log__item admin-import-log__item--<?= $r['type'] ?>"><?= htmlspecialchars($r['msg']) ?></li>
<?php endforeach; ?>
</ul>
</details>
<?php endif; ?>
<div class="admin-form-footer">
<button type="button" class="btn btn--primary"
onclick="document.getElementById('import-dialog').close(); window.location.href = window.location.pathname">Terminé</button>
</div>
<?php else: ?>
<form method="post" enctype="multipart/form-data" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div>
<label for="csv_file">Fichier CSV</label>
<div class="admin-file-input">
<input type="file" id="csv_file"
name="csv_file"
class="tfe-file-picker"
data-queue-type="csv_import"
required>
<small class="admin-file-hint">
Colonnes : Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU<br>
Quatre premières lignes ignorées — Séparateur : virgule — UTF-8
</small>
</div>
</div>
<div class="admin-form-footer">
<button type="submit" class="btn btn--primary">Importer</button>
<button type="button" class="btn btn--secondary"
onclick="document.getElementById('import-dialog').close()">Annuler</button>
</div>
</form>
<?php if (!empty($importResults)): ?>
<details class="admin-import-log-details">
<summary>Logs d'importation (<?= count($importResults) ?> entrées)</summary>
<ul class="admin-import-log">
<?php foreach ($importResults as $r): ?>
<li class="admin-import-log__item admin-import-log__item--<?= $r['type'] ?>"><?= htmlspecialchars($r['msg']) ?></li>
<?php endforeach; ?>
</ul>
</details>
<?php endif; ?>
<?php endif; ?>
</dialog>

View File

@@ -0,0 +1,13 @@
<dialog id="no-selection-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="no-sel-title">
<div class="admin-dialog__header">
<h3 id="no-sel-title">Aucune sélection</h3>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Sélectionnez au moins un TFE avant d'effectuer une action groupée.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--primary" onclick="this.closest('dialog').close()">OK</button>
</div>
</dialog>

View File

@@ -0,0 +1,29 @@
<dialog id="tmp-cleanup-dialog" class="admin-dialog admin-dialog--sheet" aria-labelledby="tmp-cleanup-title">
<div class="admin-dialog__header">
<h3 id="tmp-cleanup-title">Nettoyer les fichiers temporaires</h3>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="document.getElementById('tmp-cleanup-dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__body">
<div id="tmp-cleanup-result" style="display:none;margin-bottom:var(--space-sm)"></div>
<div class="n-grid" id="cleanup-grid-parent">
<!-- ═══════ FilePond / Trash ═══════ -->
<details id="tmp-cleanup-stats" class="n-section" open>
<summary>Fichiers temporaires</summary>
<div hx-get="/admin/actions/cleanup-stats-fragment.php"
hx-trigger="loadStats"
hx-swap="innerHTML">
<p style="margin:0;color:var(--text-secondary)">Chargement…</p>
</div>
</details>
<!-- ═══════ PeerTube ═══════ -->
<div id="peertube-orphans-wrapper"
hx-get="/admin/actions/peertube-orphans-fragment.php"
hx-trigger="loadPeertube"
hx-swap="outerHTML"
hx-indicator="#peertube-orphans-wrapper">
<p style="margin:0;color:var(--text-secondary)">Chargement…</p>
</div>
</div>
</div>
</dialog>