diff --git a/.gitignore b/.gitignore index 6caaac1..ae4cd10 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,11 @@ app/storage/logs/admin.log app/storage/cache/rate_limit/* !app/storage/cache/rate_limit/.gitkeep +# FilePond tmp uploads + trash (keep .gitkeep) +app/storage/tmp/filepond/* +!app/storage/tmp/filepond/.gitkeep +app/storage/tmp/_trash/* + # Thesis storage (keep .gitkeep) app/storage/theses/* !app/storage/theses/.gitkeep diff --git a/TODO.md b/TODO.md index 7bec0d6..38b103b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,22 @@ # Current tasks +## Tmp file cleanup (stale filepond + _trash) +- [x] Session-based detection: check manifest session_id against PHP session files +- [x] DB-based detection for _trash: check thesis_files row still exists +- [x] Time-based fallback: >2h filepond, >30d trash +- [x] admin cleanup-stats.php: stale vs active breakdown with sizes +- [x] admin cleanup-tmp.php: smart cleanup with detailed JSON response +- [x] admin index: Nettoyer button + dialog with stats and cleanup trigger +- [x] .gitignore: exclude tmp/filepond/* and tmp/_trash/* +- [ ] Deploy: just deploy + +## Dialog & trash page margins +- [x] Add admin-dialog__body CSS rule with padding + margin resets +- [x] Add admin-dialog__stats + admin-dialog__hint classes +- [x] Fix admin-dialog__alert p margins (not-last-child gets bottom margin) +- [x] Add horizontal margins to .admin-main--list > direct children (trash page forms, tables, flash msgs) +- [x] Clean up tmp-cleanup dialog inline styles → CSS classes + ## Deploy exclusions - [x] Exclude storage/tmp/ (not just filepond/*) to skip _trash dirs with bad perms - [x] Exclude storage/documents/ and storage/theses/ from rsync deploy diff --git a/app/public/admin/actions/cleanup-stats.php b/app/public/admin/actions/cleanup-stats.php new file mode 100644 index 0000000..bb0bd3a --- /dev/null +++ b/app/public/admin/actions/cleanup-stats.php @@ -0,0 +1,192 @@ + ($maxAgeFilepond / 60)) { + $stale = true; + } + + if ($stale) { + $fpStaleCount++; + $fpStaleSize += $size; + } else { + $fpActiveCount++; + $fpActiveSize += $size; + } + } + } +} + +// ── Trash stats ────────────────────────────────────────────────────────── +require_once __DIR__ . '/../../../src/Database.php'; +$db = new Database(); +$pdo = $db->getPDO(); + +$existingFileIds = []; +$stmt = $pdo->query('SELECT id FROM thesis_files'); +while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $existingFileIds[(int)$row['id']] = true; +} + +$trStaleCount = 0; +$trStaleSize = 0; +$trActiveCount = 0; +$trActiveSize = 0; + +if (is_dir($trashDir)) { + $items = @scandir($trashDir); + if ($items !== false) { + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + $filePath = $trashDir . '/' . $item; + if (!is_file($filePath)) { + continue; + } + + $size = filesize($filePath); + $mtime = filemtime($filePath); + $ageDays = (int)(($now - $mtime) / 86400); + + $stale = false; + + if (preg_match('/^(\d+)_/', $item, $m)) { + $dbId = (int)$m[1]; + if (!isset($existingFileIds[$dbId])) { + $stale = true; + } + } + + if (!$stale && $ageDays > ($maxAgeTrash / 86400)) { + $stale = true; + } + + if ($stale) { + $trStaleCount++; + $trStaleSize += $size; + } else { + $trActiveCount++; + $trActiveSize += $size; + } + } + } +} + +header('Content-Type: application/json; charset=utf-8'); +echo json_encode([ + 'filepond_stale_count' => $fpStaleCount, + 'filepond_stale_size' => $fpStaleSize, + 'filepond_stale_human' => humanBytes($fpStaleSize), + 'filepond_active_count' => $fpActiveCount, + 'filepond_active_size' => $fpActiveSize, + 'filepond_active_human' => humanBytes($fpActiveSize), + 'trash_stale_count' => $trStaleCount, + 'trash_stale_size' => $trStaleSize, + 'trash_stale_human' => humanBytes($trStaleSize), + 'trash_active_count' => $trActiveCount, + 'trash_active_size' => $trActiveSize, + 'trash_active_human' => humanBytes($trActiveSize), +]); + +// ── Helpers ─────────────────────────────────────────────────────────────── +function dirSizeRecursive(string $dir): int +{ + $size = 0; + if (!is_dir($dir)) { + return 0; + } + $items = @scandir($dir); + if ($items === false) { + return 0; + } + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + $path = $dir . '/' . $item; + if (is_dir($path)) { + $size += dirSizeRecursive($path); + } else { + $size += filesize($path); + } + } + return $size; +} + +function humanBytes(int $bytes): string +{ + if ($bytes > 1073741824) { + return number_format($bytes / 1073741824, 1) . ' GB'; + } + if ($bytes > 1048576) { + return number_format($bytes / 1048576, 1) . ' MB'; + } + return number_format($bytes / 1024, 1) . ' KB'; +} diff --git a/app/public/admin/actions/cleanup-tmp.php b/app/public/admin/actions/cleanup-tmp.php new file mode 100644 index 0000000..df0e443 --- /dev/null +++ b/app/public/admin/actions/cleanup-tmp.php @@ -0,0 +1,194 @@ +2h filepond, >30d trash) as safety net. + * Returns JSON with counts of removed items. + */ + +require_once __DIR__ . '/../../../bootstrap.php'; +require_once __DIR__ . '/../../../src/AdminAuth.php'; + +AdminAuth::requireLogin(); + +if ($_SERVER['REQUEST_METHOD'] !== 'POST' + || !isset($_POST['csrf_token'], $_SESSION['csrf_token']) + || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { + http_response_code(403); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['error' => 'CSRF invalide.']); + exit; +} + +require_once __DIR__ . '/../../../src/Database.php'; + +$storageRoot = STORAGE_ROOT; +$filepondDir = $storageRoot . '/tmp/filepond'; +$trashDir = $storageRoot . '/tmp/_trash'; +$maxAgeFilepond = 7200; // 2 hours (fallback) +$maxAgeTrash = 2592000; // 30 days (fallback) + +$now = time(); +$filepondRemoved = 0; +$trashRemoved = 0; +$errors = []; +$details = []; + +$db = new Database(); +$pdo = $db->getPDO(); + +// ── Determine PHP session save path ────────────────────────────────────── +$sessionSavePath = session_save_path(); +if (!$sessionSavePath || $sessionSavePath === '') { + $sessionSavePath = sys_get_temp_dir(); +} + +// ── Cleanup stale filepond dirs ────────────────────────────────────────── +if (is_dir($filepondDir)) { + $items = @scandir($filepondDir); + if ($items === false) { + $errors[] = 'Impossible de lire tmp/filepond/.'; + } else { + foreach ($items as $item) { + if ($item === '.' || $item === '..' || $item === '.gitkeep') { + continue; + } + $dirPath = $filepondDir . '/' . $item; + if (!is_dir($dirPath)) { + continue; + } + + $shouldDelete = false; + $reason = ''; + + $manifestPath = $dirPath . '/manifest.json'; + $mtime = filemtime($dirPath); + $ageMinutes = (int)(($now - $mtime) / 60); + + // Strategy 1: session-based (preferred) + if (file_exists($manifestPath)) { + $manifest = json_decode(file_get_contents($manifestPath), true); + if (is_array($manifest) && !empty($manifest['session_id'])) { + $sessionFile = $sessionSavePath . '/sess_' . $manifest['session_id']; + if (!file_exists($sessionFile)) { + $shouldDelete = true; + $reason = "session expirée (" . $manifest['session_id'] . ")"; + } + } + } + + // Strategy 2: time-based fallback (no manifest or session still exists but very old) + if (!$shouldDelete && $ageMinutes > ($maxAgeFilepond / 60)) { + $shouldDelete = true; + $reason = "plus de " . ($maxAgeFilepond / 3600) . "h"; + } + + if ($shouldDelete) { + rmdirRecursive($dirPath); + $filepondRemoved++; + $details[] = "filepond/$item: $reason"; + } + } + } +} + +// ── Cleanup orphaned _trash files ──────────────────────────────────────── +if (is_dir($trashDir)) { + // Build a set of existing thesis_files IDs for fast lookup + $existingFileIds = []; + $stmt = $pdo->query('SELECT id FROM thesis_files'); + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $existingFileIds[(int)$row['id']] = true; + } + + // Also collect thesis_ids from theses table (a file might refer to a hard-deleted thesis) + // — but thesis_files rows are CASCADE-deleted when a thesis is hard-deleted, so if the + // thesis_files row is gone, the trash file is truly orphaned. + + $items = @scandir($trashDir); + if ($items === false) { + $errors[] = 'Impossible de lire tmp/_trash/.'; + } else { + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + $filePath = $trashDir . '/' . $item; + if (!is_file($filePath)) { + continue; + } + + $shouldDelete = false; + $reason = ''; + + $mtime = filemtime($filePath); + $ageDays = (int)(($now - $mtime) / 86400); + + // Strategy 1: DB-based — file was trashed by deleteThesisFile(id=N), + // file is named "N_basename". If thesis_files row N no longer exists, it's orphaned. + if (preg_match('/^(\d+)_/', $item, $m)) { + $dbId = (int)$m[1]; + if (!isset($existingFileIds[$dbId])) { + $shouldDelete = true; + $reason = "ligne DB thesis_files#$dbId supprimée"; + } + } + + // Strategy 2: time-based fallback + if (!$shouldDelete && $ageDays > ($maxAgeTrash / 86400)) { + $shouldDelete = true; + $reason = "plus de " . ($maxAgeTrash / 86400) . "j"; + } + + if ($shouldDelete) { + if (!@unlink($filePath)) { + $errors[] = "Impossible de supprimer: $item"; + } else { + $trashRemoved++; + $details[] = "_trash/$item: $reason"; + } + } + } + } +} + +$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + +header('Content-Type: application/json; charset=utf-8'); +echo json_encode([ + 'success' => true, + 'filepond_removed' => $filepondRemoved, + 'trash_removed' => $trashRemoved, + 'errors' => $errors, + 'details' => $details, +]); + +// ── Helper ──────────────────────────────────────────────────────────────── +function rmdirRecursive(string $dir): void +{ + if (!is_dir($dir)) { + return; + } + $items = @scandir($dir); + if ($items === false) { + return; + } + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + $path = $dir . '/' . $item; + if (is_dir($path)) { + rmdirRecursive($path); + } else { + @unlink($path); + } + } + @rmdir($dir); +} diff --git a/app/public/admin/index.php b/app/public/admin/index.php index dd776d5..273b30b 100644 --- a/app/public/admin/index.php +++ b/app/public/admin/index.php @@ -473,6 +473,34 @@ try { $trashCount = $db->countTrashedTheses(); $tab = $_GET['tab'] ?? 'list'; $trashedTheses = ($tab === 'trash') ? $db->getTrashedTheses() : []; + + // ── Tmp file stats ─────────────────────────────────────────────────── + $filepondDir = STORAGE_ROOT . '/tmp/filepond'; + $trashDir = STORAGE_ROOT . '/tmp/_trash'; + $tmpFilepondCount = 0; + $tmpTrashCount = 0; + $tmpTotalCount = 0; + if (is_dir($filepondDir)) { + $items = @scandir($filepondDir); + if ($items !== false) { + foreach ($items as $item) { + if ($item !== '.' && $item !== '..' && $item !== '.gitkeep') { + $tmpFilepondCount++; + } + } + } + } + if (is_dir($trashDir)) { + $items = @scandir($trashDir); + if ($items !== false) { + foreach ($items as $item) { + if ($item !== '.' && $item !== '..') { + $tmpTrashCount++; + } + } + } + } + $tmpTotalCount = $tmpFilepondCount + $tmpTrashCount; } catch (Exception $e) { error_log("Error loading theses list: " . $e->getMessage()); die("Erreur lors du chargement de la liste."); diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index af8792c..721bafe 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -61,6 +61,15 @@ padding: 0 !important; } +.admin-main--list > form, +.admin-main--list > table, +.admin-main--list > .flash-success, +.admin-main--list > .flash-error, +.admin-main--list > .admin-empty { + margin-left: var(--space-m); + margin-right: var(--space-m); +} + #admin-table-wrap { padding: 0 0 var(--space-2xl); } @@ -996,10 +1005,43 @@ th.admin-ap-col { line-height: 1.6; } -.admin-dialog__alert p { +.admin-dialog__alert p:not(:last-child) { + margin: 0 0 var(--space-xs) 0; +} + +.admin-dialog__alert p:last-child { margin: 0; } +.admin-dialog__body { + padding: var(--space-m) var(--space-l); + font-size: var(--step--1); + line-height: 1.6; +} + +.admin-dialog__body > *:first-child { + margin-top: 0; +} + +.admin-dialog__body > *:last-child { + margin-bottom: 0; +} + +.admin-dialog__stats { + margin: var(--space-sm) 0; + padding: var(--space-sm); + background: var(--bg-secondary); + border-radius: 8px; + font-size: var(--step--1); + line-height: 1.6; +} + +.admin-dialog__hint { + font-size: var(--step--2); + color: var(--text-secondary); + margin: var(--space-xs) 0 0 0; +} + .admin-dialog__footer { display: flex; gap: var(--space-xs); diff --git a/app/storage/tmp/filepond/12e0bfc40a62179cc3ada9f38b8e2614/2026-03-30-112553_hyprshot.png b/app/storage/tmp/filepond/12e0bfc40a62179cc3ada9f38b8e2614/2026-03-30-112553_hyprshot.png deleted file mode 100644 index 259023f..0000000 Binary files a/app/storage/tmp/filepond/12e0bfc40a62179cc3ada9f38b8e2614/2026-03-30-112553_hyprshot.png and /dev/null differ diff --git a/app/storage/tmp/filepond/12e0bfc40a62179cc3ada9f38b8e2614/manifest.json b/app/storage/tmp/filepond/12e0bfc40a62179cc3ada9f38b8e2614/manifest.json deleted file mode 100644 index 4385821..0000000 --- a/app/storage/tmp/filepond/12e0bfc40a62179cc3ada9f38b8e2614/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"queue_type":"cover","original_name":"2026-03-30-112553_hyprshot.png","mime":"image/png","ext":"png","size":790551,"session_id":"ca4d45dbfb0b3fc4f87f725826df9d35","uploaded_at":"2026-05-12T11:03:34+00:00"} \ No newline at end of file diff --git a/app/storage/tmp/filepond/4dcd1b78c9f5e136c878e9e9e829c4cc/2026-03-30-112553_hyprshot.png b/app/storage/tmp/filepond/4dcd1b78c9f5e136c878e9e9e829c4cc/2026-03-30-112553_hyprshot.png deleted file mode 100644 index 259023f..0000000 Binary files a/app/storage/tmp/filepond/4dcd1b78c9f5e136c878e9e9e829c4cc/2026-03-30-112553_hyprshot.png and /dev/null differ diff --git a/app/storage/tmp/filepond/4dcd1b78c9f5e136c878e9e9e829c4cc/manifest.json b/app/storage/tmp/filepond/4dcd1b78c9f5e136c878e9e9e829c4cc/manifest.json deleted file mode 100644 index 98fb814..0000000 --- a/app/storage/tmp/filepond/4dcd1b78c9f5e136c878e9e9e829c4cc/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"queue_type":"cover","original_name":"2026-03-30-112553_hyprshot.png","mime":"image/png","ext":"png","size":790551,"session_id":"ca4d45dbfb0b3fc4f87f725826df9d35","uploaded_at":"2026-05-12T10:50:23+00:00"} \ No newline at end of file diff --git a/app/storage/tmp/filepond/58ac477065ed363153824f34bc21fe2d/NA - Guide pour répondre au #NotAllMen.mp3 b/app/storage/tmp/filepond/58ac477065ed363153824f34bc21fe2d/NA - Guide pour répondre au #NotAllMen.mp3 deleted file mode 100644 index 31b35de..0000000 Binary files a/app/storage/tmp/filepond/58ac477065ed363153824f34bc21fe2d/NA - Guide pour répondre au #NotAllMen.mp3 and /dev/null differ diff --git a/app/storage/tmp/filepond/6ae2b2848899757706424ebfe383236e/manifest.json b/app/storage/tmp/filepond/6ae2b2848899757706424ebfe383236e/manifest.json deleted file mode 100644 index e7edda1..0000000 --- a/app/storage/tmp/filepond/6ae2b2848899757706424ebfe383236e/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"queue_type":"note_intention","original_name":"nixing_the_fix_report_final_5521_630pm-508_002.pdf","mime":"application/pdf","ext":"pdf","size":1296086,"session_id":"ca4d45dbfb0b3fc4f87f725826df9d35","uploaded_at":"2026-05-12T11:03:37+00:00"} \ No newline at end of file diff --git a/app/storage/tmp/filepond/6ae2b2848899757706424ebfe383236e/nixing_the_fix_report_final_5521_630pm-508_002.pdf b/app/storage/tmp/filepond/6ae2b2848899757706424ebfe383236e/nixing_the_fix_report_final_5521_630pm-508_002.pdf deleted file mode 100644 index 054ef1b..0000000 Binary files a/app/storage/tmp/filepond/6ae2b2848899757706424ebfe383236e/nixing_the_fix_report_final_5521_630pm-508_002.pdf and /dev/null differ diff --git a/app/storage/tmp/filepond/a74918292b80d2407b7a460152167a48/Nixing the Fix_ An FTC Report to Congress on Repair Restrictions - nixing_the_fix_report_final_5521_630pm-508_002.pdf b/app/storage/tmp/filepond/a74918292b80d2407b7a460152167a48/Nixing the Fix_ An FTC Report to Congress on Repair Restrictions - nixing_the_fix_report_final_5521_630pm-508_002.pdf deleted file mode 100644 index c640130..0000000 Binary files a/app/storage/tmp/filepond/a74918292b80d2407b7a460152167a48/Nixing the Fix_ An FTC Report to Congress on Repair Restrictions - nixing_the_fix_report_final_5521_630pm-508_002.pdf and /dev/null differ diff --git a/app/storage/tmp/filepond/a74918292b80d2407b7a460152167a48/manifest.json b/app/storage/tmp/filepond/a74918292b80d2407b7a460152167a48/manifest.json deleted file mode 100644 index a7f071f..0000000 --- a/app/storage/tmp/filepond/a74918292b80d2407b7a460152167a48/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"queue_type":"annexe","original_name":"Nixing the Fix_ An FTC Report to Congress on Repair Restrictions - nixing_the_fix_report_final_5521_630pm-508_002.pdf","mime":"application/pdf","ext":"pdf","size":5861686,"session_id":"ca4d45dbfb0b3fc4f87f725826df9d35","uploaded_at":"2026-05-12T10:50:16+00:00"} \ No newline at end of file diff --git a/app/storage/tmp/filepond/af9a22fc91fece874a0349e2597b136f/2026-05-06-105104_hyprshot.png b/app/storage/tmp/filepond/af9a22fc91fece874a0349e2597b136f/2026-05-06-105104_hyprshot.png deleted file mode 100644 index a0824bd..0000000 Binary files a/app/storage/tmp/filepond/af9a22fc91fece874a0349e2597b136f/2026-05-06-105104_hyprshot.png and /dev/null differ diff --git a/app/storage/tmp/filepond/af9a22fc91fece874a0349e2597b136f/manifest.json b/app/storage/tmp/filepond/af9a22fc91fece874a0349e2597b136f/manifest.json deleted file mode 100644 index 334641a..0000000 --- a/app/storage/tmp/filepond/af9a22fc91fece874a0349e2597b136f/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"queue_type":"cover","original_name":"2026-05-06-105104_hyprshot.png","mime":"image/png","ext":"png","size":170350,"session_id":"60cb0e3107c8795cc3c0d2b8c222953e","uploaded_at":"2026-05-13T12:18:50+00:00"} \ No newline at end of file diff --git a/app/storage/tmp/filepond/aff4c36c7e233e24914ccae418b0c190/invoice_25-12-01_251200002_les-iles-mardi.pdf b/app/storage/tmp/filepond/aff4c36c7e233e24914ccae418b0c190/invoice_25-12-01_251200002_les-iles-mardi.pdf deleted file mode 100644 index 13be225..0000000 Binary files a/app/storage/tmp/filepond/aff4c36c7e233e24914ccae418b0c190/invoice_25-12-01_251200002_les-iles-mardi.pdf and /dev/null differ diff --git a/app/storage/tmp/filepond/aff4c36c7e233e24914ccae418b0c190/manifest.json b/app/storage/tmp/filepond/aff4c36c7e233e24914ccae418b0c190/manifest.json deleted file mode 100644 index 49843a5..0000000 --- a/app/storage/tmp/filepond/aff4c36c7e233e24914ccae418b0c190/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"queue_type":"note_intention","original_name":"invoice_25-12-01_251200002_les-iles-mardi.pdf","mime":"application/pdf","ext":"pdf","size":349083,"session_id":"ca4d45dbfb0b3fc4f87f725826df9d35","uploaded_at":"2026-05-12T10:30:01+00:00"} \ No newline at end of file diff --git a/app/storage/tmp/filepond/cb63cb9e87000016b73d6f1a2413cce6/2026-03-30-112553_hyprshot.png b/app/storage/tmp/filepond/cb63cb9e87000016b73d6f1a2413cce6/2026-03-30-112553_hyprshot.png deleted file mode 100644 index 259023f..0000000 Binary files a/app/storage/tmp/filepond/cb63cb9e87000016b73d6f1a2413cce6/2026-03-30-112553_hyprshot.png and /dev/null differ diff --git a/app/storage/tmp/filepond/cb63cb9e87000016b73d6f1a2413cce6/manifest.json b/app/storage/tmp/filepond/cb63cb9e87000016b73d6f1a2413cce6/manifest.json deleted file mode 100644 index 0e166e3..0000000 --- a/app/storage/tmp/filepond/cb63cb9e87000016b73d6f1a2413cce6/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"queue_type":"cover","original_name":"2026-03-30-112553_hyprshot.png","mime":"image/png","ext":"png","size":790551,"session_id":"ca4d45dbfb0b3fc4f87f725826df9d35","uploaded_at":"2026-05-12T10:46:44+00:00"} \ No newline at end of file diff --git a/app/storage/tmp/filepond/d2f11b313134b1f2686e6238f46e9430/Screenshot_2026-05-08_at_11-25-04_DepNum.png b/app/storage/tmp/filepond/d2f11b313134b1f2686e6238f46e9430/Screenshot_2026-05-08_at_11-25-04_DepNum.png deleted file mode 100644 index d9212f5..0000000 Binary files a/app/storage/tmp/filepond/d2f11b313134b1f2686e6238f46e9430/Screenshot_2026-05-08_at_11-25-04_DepNum.png and /dev/null differ diff --git a/app/storage/tmp/filepond/d2f11b313134b1f2686e6238f46e9430/manifest.json b/app/storage/tmp/filepond/d2f11b313134b1f2686e6238f46e9430/manifest.json deleted file mode 100644 index aa0bfbe..0000000 --- a/app/storage/tmp/filepond/d2f11b313134b1f2686e6238f46e9430/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"queue_type":"cover","original_name":"Screenshot_2026-05-08_at_11-25-04_DepNum.png","mime":"image/png","ext":"png","size":134916,"session_id":"ca4d45dbfb0b3fc4f87f725826df9d35","uploaded_at":"2026-05-12T10:30:08+00:00"} \ No newline at end of file diff --git a/app/storage/tmp/filepond/d3242080d85f2076ae2d8c657fcea6d3/Nixing the Fix_ An FTC Report to Congress on Repair Restrictions - nixing_the_fix_report_final_5521_630pm-508_002.pdf b/app/storage/tmp/filepond/d3242080d85f2076ae2d8c657fcea6d3/Nixing the Fix_ An FTC Report to Congress on Repair Restrictions - nixing_the_fix_report_final_5521_630pm-508_002.pdf deleted file mode 100644 index c640130..0000000 Binary files a/app/storage/tmp/filepond/d3242080d85f2076ae2d8c657fcea6d3/Nixing the Fix_ An FTC Report to Congress on Repair Restrictions - nixing_the_fix_report_final_5521_630pm-508_002.pdf and /dev/null differ diff --git a/app/storage/tmp/filepond/d3242080d85f2076ae2d8c657fcea6d3/manifest.json b/app/storage/tmp/filepond/d3242080d85f2076ae2d8c657fcea6d3/manifest.json deleted file mode 100644 index 509fdaf..0000000 --- a/app/storage/tmp/filepond/d3242080d85f2076ae2d8c657fcea6d3/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"queue_type":"note_intention","original_name":"Nixing the Fix_ An FTC Report to Congress on Repair Restrictions - nixing_the_fix_report_final_5521_630pm-508_002.pdf","mime":"application/pdf","ext":"pdf","size":5861686,"session_id":"ca4d45dbfb0b3fc4f87f725826df9d35","uploaded_at":"2026-05-12T10:46:38+00:00"} \ No newline at end of file diff --git a/app/storage/tmp/filepond/f1af4e988d9557278b98fa87946be69b/Proposition_procédure_licences_V2.pdf b/app/storage/tmp/filepond/f1af4e988d9557278b98fa87946be69b/Proposition_procédure_licences_V2.pdf deleted file mode 100644 index d447e1b..0000000 Binary files a/app/storage/tmp/filepond/f1af4e988d9557278b98fa87946be69b/Proposition_procédure_licences_V2.pdf and /dev/null differ diff --git a/app/storage/tmp/filepond/f1af4e988d9557278b98fa87946be69b/manifest.json b/app/storage/tmp/filepond/f1af4e988d9557278b98fa87946be69b/manifest.json deleted file mode 100644 index 983255b..0000000 --- a/app/storage/tmp/filepond/f1af4e988d9557278b98fa87946be69b/manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"queue_type":"note_intention","original_name":"Proposition_proce\u0301dure_licences_V2.pdf","mime":"application/pdf","ext":"pdf","size":32119,"session_id":"ca4d45dbfb0b3fc4f87f725826df9d35","uploaded_at":"2026-05-12T10:50:27+00:00"} \ No newline at end of file diff --git a/app/templates/admin/index.php b/app/templates/admin/index.php index a870d4f..d5274d0 100644 --- a/app/templates/admin/index.php +++ b/app/templates/admin/index.php @@ -37,6 +37,12 @@ document.addEventListener('htmx:afterSwap',()=>{document.querySelectorAll('input Corbeille () + 0): ?> + + + +
+

Les fichiers temporaires s'accumulent lorsque des téléversements sont abandonnés (formulaire fermé avant envoi).

+
+ Chargement… +
+

+ 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. +

+ + +
+ + + + + diff --git a/justfile b/justfile index c01a871..05ed4d2 100644 --- a/justfile +++ b/justfile @@ -318,6 +318,10 @@ trigger-backup: # Manually trigger the backup script on the server now (doesn't wait for cron). ssh -t xamxam "sudo -u www-data /usr/local/bin/backup-sqlite.sh" +[group('deploy')] +deploy-all-first: deploy deploy-backup + # One-shot: full initial deploy including backup cron. + # ============================================================================ # Testing # ============================================================================