feat: implement SQLite backup & data integrity plan (Phases 2-4)

This commit is contained in:
Pontoporeia
2026-05-11 01:08:46 +02:00
parent c0163ca4d5
commit 926659087f
18 changed files with 683 additions and 151 deletions

View File

@@ -584,6 +584,32 @@
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: psvklxsu ec511543 "fix: exclude entire var/ from rsync --delete to preserve logs" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: psvklxsu ec511543 "fix: exclude entire var/ from rsync --delete to preserve logs" (rebased revision)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
- $linkName = $link['name'] ?? '';
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: ouqzmwvn fafb3fc6 "feat: implement SQLite backup & data integrity plan (Phases 2-4)" (rebased revision)
$linkName = $link['name'] ?? '';
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
$linkLockedYear = $link['locked_year'] ?? null;
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: ouqzmwvn 97412d13 "feat: implement SQLite backup & data integrity plan (Phases 2-4)" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: ouqzmwvn 97412d13 "feat: implement SQLite backup & data integrity plan (Phases 2-4)" (rebased revision)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
- $linkName = $link['name'] ?? '';
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: wstuyzym f5810c41 "feat: implement SQLite backup & data integrity plan (Phases 2-4)" (rebased revision)
$linkName = $link['name'] ?? '';
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
$linkLockedYear = $link['locked_year'] ?? null;
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: wstuyzym 5886355c "feat: implement SQLite backup & data integrity plan (Phases 2-4)" (rebased revision)
++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
?>
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">

View File

@@ -88,16 +88,17 @@
<fieldset>
<legend>Restrictions d'accès aux fichiers</legend>
<div class="param-form">
<input type="hidden" id="settings-csrf" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div class="param-form" id="param-form-restrictions">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<label class="param-checkbox">
<input type="hidden" name="restricted_files_enabled" value="0">
<input type="checkbox" name="restricted_files_enabled" value="1"
<?= ($siteSettings['restricted_files_enabled'] ?? '0') === '1' ? 'checked' : '' ?>
hx-post="/admin/actions/settings.php"
hx-trigger="change"
hx-swap="none"
hx-include="#settings-csrf"
hx-include="#param-form-restrictions"
hx-vals='{"section":"formulaire"}'>
<span>
<strong>Activer la restriction d'accès</strong><br>
@@ -111,16 +112,17 @@
<legend>Degré d'ouverture</legend>
<p>Options de visibilité disponibles dans le formulaire d'ajout de TFE.</p>
<div class="param-form">
<input type="hidden" id="settings-csrf-acces" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div class="param-form" id="param-form-acces">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<label class="param-checkbox">
<input type="hidden" name="access_type_libre_enabled" value="0">
<input type="checkbox" name="access_type_libre_enabled" value="1"
<?= ($siteSettings['access_type_libre_enabled'] ?? '0') === '1' ? 'checked' : '' ?>
hx-post="/admin/actions/settings.php"
hx-trigger="change"
hx-swap="none"
hx-include="#settings-csrf-acces"
hx-include="#param-form-acces"
hx-vals='{"section":"formulaire"}'>
<span>
<strong>Libre</strong><br>
@@ -129,12 +131,13 @@
</label>
<label class="param-checkbox">
<input type="hidden" name="access_type_interne_enabled" value="0">
<input type="checkbox" name="access_type_interne_enabled" value="1"
<?= ($siteSettings['access_type_interne_enabled'] ?? '1') === '1' ? 'checked' : '' ?>
hx-post="/admin/actions/settings.php"
hx-trigger="change"
hx-swap="none"
hx-include="#settings-csrf-acces"
hx-include="#param-form-acces"
hx-vals='{"section":"formulaire"}'>
<span>
<strong>Interne</strong><br>
@@ -143,12 +146,13 @@
</label>
<label class="param-checkbox">
<input type="hidden" name="access_type_interdit_enabled" value="0">
<input type="checkbox" name="access_type_interdit_enabled" value="1"
<?= ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1' ? 'checked' : '' ?>
hx-post="/admin/actions/settings.php"
hx-trigger="change"
hx-swap="none"
hx-include="#settings-csrf-acces"
hx-include="#param-form-acces"
hx-vals='{"section":"formulaire"}'>
<span>
<strong>Interdit</strong><br>
@@ -163,8 +167,8 @@
<p>Active ou désactive les types de travaux dans les formulaires et la consultation. Un type désactivé ne peut plus être soumis ni affiché sur le site.</p>
<p class="param-note">Le type <strong>TFE</strong> est toujours actif et ne peut pas être désactivé.</p>
<div class="param-form">
<input type="hidden" id="settings-csrf-types" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div class="param-form" id="param-form-types">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<label class="param-checkbox param-checkbox--disabled">
<input type="checkbox" disabled checked>
@@ -175,12 +179,13 @@
</label>
<label class="param-checkbox">
<input type="hidden" name="objet_these_enabled" value="0">
<input type="checkbox" name="objet_these_enabled" value="1"
<?= ($siteSettings['objet_these_enabled'] ?? '1') === '1' ? 'checked' : '' ?>
hx-post="/admin/actions/settings.php"
hx-trigger="change"
hx-swap="none"
hx-include="#settings-csrf-types"
hx-include="#param-form-types"
hx-vals='{"section":"objet_types"}'>
<span>
<strong>Thèse</strong><br>
@@ -189,12 +194,13 @@
</label>
<label class="param-checkbox">
<input type="hidden" name="objet_frart_enabled" value="0">
<input type="checkbox" name="objet_frart_enabled" value="1"
<?= ($siteSettings['objet_frart_enabled'] ?? '1') === '1' ? 'checked' : '' ?>
hx-post="/admin/actions/settings.php"
hx-trigger="change"
hx-swap="none"
hx-include="#settings-csrf-types"
hx-include="#param-form-types"
hx-vals='{"section":"objet_types"}'>
<span>
<strong>Frart</strong><br>

View File

@@ -0,0 +1,82 @@
<main id="main-content" class="admin-main--list">
<div class="admin-list-toolbar admin-list-toolbar--list">
<div class="admin-toolbar-top">
<div class="admin-toolbar-title-row">
<h1><a href="/admin/" class="admin-back-btn" title="Retour à la liste"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168A8,8,0,0,1,176,128Z"></path></svg></a> Corbeille</h1>
<span class="admin-stat admin-stat--inline" style="margin-left:auto"><?= count($trashedTheses) ?> TFE(s)</span>
</div>
</div>
</div>
<?php
$flash = App::consumeFlash();
?>
<?php if ($flash['success']): ?>
<div class="flash-success" role="alert"><?= htmlspecialchars($flash['success']) ?></div>
<?php endif; ?>
<?php if ($flash['error']): ?>
<div class="flash-error" role="alert"><?= htmlspecialchars($flash['error']) ?></div>
<?php endif; ?>
<?php if (empty($trashedTheses)): ?>
<div class="admin-empty">La corbeille est vide.</div>
<?php else: ?>
<form method="post" action="actions/corbeille.php" style="margin-bottom:var(--space-xs)">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="empty_trash">
<button type="submit" class="btn btn--danger btn--sm" onclick="return confirm('Vider toute la corbeille ? Cette action est irréversible.')">
Vider la corbeille
</button>
</form>
<table>
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Titre</th>
<th scope="col">Auteur(s)</th>
<th scope="col">Année</th>
<th scope="col">Supprimé le</th>
<th scope="col" style="width:1%">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($trashedTheses as $t): ?>
<tr>
<td><?= htmlspecialchars($t['identifier'] ?? '#' . $t['id']) ?></td>
<td>
<strong><?= htmlspecialchars($t['title']) ?></strong>
<?php if (!empty($t['subtitle'])): ?>
<br><small><?= htmlspecialchars($t['subtitle']) ?></small>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($t['authors'] ?? '—') ?></td>
<td><?= (int)$t['year'] ?></td>
<td><?= htmlspecialchars($t['deleted_at']) ?></td>
<td class="admin-actions-col">
<div class="admin-actions">
<form method="post" action="actions/corbeille.php" class="admin-inline-form" style="display:inline">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="restore">
<input type="hidden" name="thesis_id" value="<?= (int)$t['id'] ?>">
<button type="submit" class="admin-icon-btn admin-icon-btn--edit" title="Restaurer">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM176,208H80a8,8,0,0,1,0-16h96a8,8,0,0,1,0,16Zm0-32H80a8,8,0,0,1,0-16h96a8,8,0,0,1,0,16Z"/></svg>
</button>
</form>
<form method="post" action="actions/corbeille.php" class="admin-inline-form" style="display:inline"
onsubmit="return confirm('Supprimer définitivement ce TFE ? Cette action est irréversible.')">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="action" value="hard_delete">
<input type="hidden" name="thesis_id" value="<?= (int)$t['id'] ?>">
<button type="submit" class="admin-icon-btn admin-icon-btn--delete" title="Supprimer définitivement">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"/></svg>
</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</main>

View File

@@ -32,6 +32,11 @@ document.addEventListener('htmx:afterSwap',()=>{document.querySelectorAll('input
<div class="admin-btn-group">
<a href="/admin/add.php" class="btn btn--primary btn--sm">+ Ajouter un TFE</a>
<a href="/admin/tags.php" class="btn btn--primary btn--sm">Mots-clés</a>
<?php if ($trashCount > 0): ?>
<a href="/admin/index.php?tab=trash" class="btn btn--sm <?= $tab === 'trash' ? 'btn--primary' : 'btn--secondary' ?>">
Corbeille (<?= $trashCount ?>)
</a>
<?php endif; ?>
<button type="button" class="btn btn--primary btn--sm" id="import-dialog-btn"
onclick="document.getElementById('import-dialog').showModal()">
Importer