diff --git a/app/public/admin/actions/_cleanup-stats-data.php b/app/public/admin/actions/_cleanup-stats-data.php
new file mode 100644
index 0000000..288eb3c
--- /dev/null
+++ b/app/public/admin/actions/_cleanup-stats-data.php
@@ -0,0 +1,197 @@
+ 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';
+}
+
+function getCleanupStats(): array
+{
+ $storageRoot = STORAGE_ROOT;
+ $filepondDir = $storageRoot . '/tmp/filepond';
+ $trashDir = $storageRoot . '/tmp/_trash';
+ $maxAgeFilepond = 7200; // 2h
+ $maxAgeTrash = 2592000; // 30d
+
+ $sessionSavePath = session_save_path();
+ if (!$sessionSavePath || $sessionSavePath === '') {
+ $sessionSavePath = sys_get_temp_dir();
+ }
+
+ $now = time();
+
+ // ── FilePond stats ───────────────────────────────────────────────────
+ $fpStaleCount = 0;
+ $fpStaleSize = 0;
+ $fpStaleFiles = [];
+ $fpActiveCount = 0;
+ $fpActiveSize = 0;
+
+ if (is_dir($filepondDir)) {
+ $items = @scandir($filepondDir);
+ if ($items !== false) {
+ foreach ($items as $item) {
+ if ($item === '.' || $item === '..' || $item === '.gitkeep') {
+ continue;
+ }
+ $dirPath = $filepondDir . '/' . $item;
+ if (!is_dir($dirPath)) {
+ continue;
+ }
+
+ $size = dirSizeRecursive($dirPath);
+ $mtime = filemtime($dirPath);
+ $ageMinutes = (int)(($now - $mtime) / 60);
+
+ $stale = false;
+
+ // Session-based detection
+ $manifestPath = $dirPath . '/manifest.json';
+ 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)) {
+ $stale = true;
+ }
+ }
+ }
+
+ // Time-based fallback
+ if (!$stale && $ageMinutes > ($maxAgeFilepond / 60)) {
+ $stale = true;
+ }
+
+ if ($stale) {
+ $fpStaleCount++;
+ $fpStaleSize += $size;
+ $fpStaleFiles[] = [
+ 'name' => $item,
+ 'size' => $size,
+ 'human' => humanBytes($size),
+ 'age_minutes' => $ageMinutes,
+ ];
+ } 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;
+ $trStaleFiles = [];
+ $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;
+ $trStaleFiles[] = [
+ 'name' => $item,
+ 'size' => $size,
+ 'human' => humanBytes($size),
+ 'age_days' => $ageDays,
+ ];
+ } else {
+ $trActiveCount++;
+ $trActiveSize += $size;
+ }
+ }
+ }
+ }
+
+ return [
+ 'filepond_stale_count' => $fpStaleCount,
+ 'filepond_stale_size' => $fpStaleSize,
+ 'filepond_stale_human' => humanBytes($fpStaleSize),
+ 'filepond_stale_files' => $fpStaleFiles,
+ '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_stale_files' => $trStaleFiles,
+ 'trash_active_count' => $trActiveCount,
+ 'trash_active_size' => $trActiveSize,
+ 'trash_active_human' => humanBytes($trActiveSize),
+ ];
+}
diff --git a/app/public/admin/actions/_peertube-orphans-data.php b/app/public/admin/actions/_peertube-orphans-data.php
new file mode 100644
index 0000000..52ac35a
--- /dev/null
+++ b/app/public/admin/actions/_peertube-orphans-data.php
@@ -0,0 +1,108 @@
+ false,
+ 'error' => 'PeerTube non configuré.',
+ ];
+ }
+
+ // ── Collect all Peertube UUIDs linked in the DB ──────────────────────
+ $pdo = $db->getPDO();
+ $dbUuids = [];
+ $linkedMap = []; // uuid → [thesis_id, thesis_title, thesis_identifier]
+
+ $stmt = $pdo->query(
+ "SELECT tf.file_path, tf.file_name, t.id AS thesis_id, t.title, t.identifier
+ FROM thesis_files tf
+ JOIN theses t ON t.id = tf.thesis_id
+ WHERE tf.file_path LIKE 'peertube_ids:%'
+ AND t.deleted_at IS NULL"
+ );
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $uuid = substr($row['file_path'], strlen('peertube_ids:'));
+ $dbUuids[$uuid] = true;
+ $linkedMap[$uuid][] = [
+ 'thesis_id' => (int)$row['thesis_id'],
+ 'title' => $row['title'],
+ 'identifier' => $row['identifier'] ?? '',
+ ];
+ }
+
+ // ── List all channel videos ──────────────────────────────────────────
+ try {
+ $channelVideos = PeerTubeService::listChannelVideos($db);
+ } catch (\Throwable $e) {
+ return [
+ 'configured' => true,
+ 'error' => 'Erreur lors du listage des vidéos : ' . $e->getMessage(),
+ ];
+ }
+
+ // ── Find orphans: on channel but not in DB ───────────────────────────
+ $orphans = [];
+ $linked = [];
+ foreach ($channelVideos as $v) {
+ $uuid = $v['shortUUID'] ?: $v['uuid'];
+ if ($uuid === '') {
+ continue;
+ }
+ if (isset($dbUuids[$uuid])) {
+ $linked[] = [
+ 'uuid' => $uuid,
+ 'name' => $v['name'],
+ 'theses' => $linkedMap[$uuid] ?? [],
+ ];
+ } else {
+ $orphans[] = [
+ 'uuid' => $uuid,
+ 'name' => $v['name'],
+ 'createdAt' => $v['createdAt'],
+ ];
+ }
+ }
+
+ // ── Find stale DB entries: in DB but not on channel ──────────────────
+ $stale = [];
+ foreach ($dbUuids as $uuid => $_) {
+ $found = false;
+ foreach ($channelVideos as $v) {
+ if (($v['shortUUID'] ?: $v['uuid']) === $uuid) {
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ $stale[] = [
+ 'uuid' => $uuid,
+ 'theses' => $linkedMap[$uuid] ?? [],
+ ];
+ }
+ }
+
+ $totalOnChannel = count($channelVideos);
+
+ return [
+ 'configured' => true,
+ 'channel_name' => PeerTubeService::getSettings($db)['channel_name'],
+ 'total_on_channel' => $totalOnChannel,
+ 'total_linked' => count($linked),
+ 'orphan_count' => count($orphans),
+ 'orphans' => $orphans,
+ 'stale_count' => count($stale),
+ 'stale_entries' => $stale,
+ ];
+}
diff --git a/app/public/admin/actions/cleanup-stats-fragment.php b/app/public/admin/actions/cleanup-stats-fragment.php
index 461d49c..597d07f 100644
--- a/app/public/admin/actions/cleanup-stats-fragment.php
+++ b/app/public/admin/actions/cleanup-stats-fragment.php
@@ -10,11 +10,8 @@ require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
-// Re-use the existing stats endpoint internally
-ob_start();
-require __DIR__ . '/cleanup-stats.php';
-$json = ob_get_clean();
-$d = json_decode($json, true);
+require_once __DIR__ . '/_cleanup-stats-data.php';
+$d = getCleanupStats();
$fpStale = $d['filepond_stale_count'] ?? 0;
$fpActive = $d['filepond_active_count'] ?? 0;
@@ -41,13 +38,7 @@ if ($trStale > 0) {
}
?>
-
+ ✓ Aucun fichier temporaire.
@@ -72,71 +63,61 @@ if ($trStale > 0) {
0): ?>
-
+ Téléversements abandonnés = htmlspecialchars($fpMeta) ?>
+
0): ?>
-
+ Corbeille = htmlspecialchars($trMeta) ?>
+
0 || $trActive > 0): ?>
diff --git a/app/public/admin/actions/cleanup-stats.php b/app/public/admin/actions/cleanup-stats.php
index b4d4588..692fe39 100644
--- a/app/public/admin/actions/cleanup-stats.php
+++ b/app/public/admin/actions/cleanup-stats.php
@@ -15,194 +15,10 @@ AdminAuth::requireLogin();
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
- exit;
+ return;
}
-$storageRoot = STORAGE_ROOT;
-$filepondDir = $storageRoot . '/tmp/filepond';
-$trashDir = $storageRoot . '/tmp/_trash';
-$maxAgeFilepond = 7200; // 2h
-$maxAgeTrash = 2592000; // 30d
-
-$sessionSavePath = session_save_path();
-if (!$sessionSavePath || $sessionSavePath === '') {
- $sessionSavePath = sys_get_temp_dir();
-}
-
-$now = time();
-
-// ── FilePond stats ───────────────────────────────────────────────────────
-$fpStaleCount = 0;
-$fpStaleSize = 0;
-$fpStaleFiles = [];
-$fpActiveCount = 0;
-$fpActiveSize = 0;
-
-if (is_dir($filepondDir)) {
- $items = @scandir($filepondDir);
- if ($items !== false) {
- foreach ($items as $item) {
- if ($item === '.' || $item === '..' || $item === '.gitkeep') {
- continue;
- }
- $dirPath = $filepondDir . '/' . $item;
- if (!is_dir($dirPath)) {
- continue;
- }
-
- $size = dirSizeRecursive($dirPath);
- $mtime = filemtime($dirPath);
- $ageMinutes = (int)(($now - $mtime) / 60);
-
- $stale = false;
-
- // Session-based detection
- $manifestPath = $dirPath . '/manifest.json';
- 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)) {
- $stale = true;
- }
- }
- }
-
- // Time-based fallback
- if (!$stale && $ageMinutes > ($maxAgeFilepond / 60)) {
- $stale = true;
- }
-
- if ($stale) {
- $fpStaleCount++;
- $fpStaleSize += $size;
- $fpStaleFiles[] = [
- 'name' => $item,
- 'size' => $size,
- 'human' => humanBytes($size),
- 'age_minutes' => $ageMinutes,
- ];
- } 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;
-$trStaleFiles = [];
-$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;
- $trStaleFiles[] = [
- 'name' => $item,
- 'size' => $size,
- 'human' => humanBytes($size),
- 'age_days' => $ageDays,
- ];
- } else {
- $trActiveCount++;
- $trActiveSize += $size;
- }
- }
- }
-}
+require_once __DIR__ . '/_cleanup-stats-data.php';
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_stale_files' => $fpStaleFiles,
- '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_stale_files' => $trStaleFiles,
- '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';
-}
+echo json_encode(getCleanupStats());
diff --git a/app/public/admin/actions/peertube-orphans-fragment.php b/app/public/admin/actions/peertube-orphans-fragment.php
index 29da083..4097d89 100644
--- a/app/public/admin/actions/peertube-orphans-fragment.php
+++ b/app/public/admin/actions/peertube-orphans-fragment.php
@@ -10,94 +10,69 @@ require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
-// Re-use the existing JSON endpoint internally
-ob_start();
-require __DIR__ . '/peertube-orphans.php';
-$json = ob_get_clean();
-$d = json_decode($json, true);
+require_once __DIR__ . '/_peertube-orphans-data.php';
+$d = getPeerTubeOrphansData();
if (!($d['configured'] ?? false)): ?>
-
+ ⚠️ PeerTube non configuré.
-
+ ✗ = htmlspecialchars($d['error']) ?>
-
0): ?>
-
+ Références DB obsolètes = $d['stale_count'] ?>
+ Ces UUID sont référencés en base de données mais n'existent plus sur la chaîne PeerTube. Les TFE liés affichent des liens morts.
+
+ | UUID | TFE(s) |
+
+
+
+ | = htmlspecialchars($s['uuid']) ?> |
+
+
+ = implode(', ', array_map(function($t) {
+ $label = $t['identifier'] ?: '#' . $t['thesis_id'];
+ return '' . htmlspecialchars($label) . '';
+ }, $s['theses'])) ?>
+
+ |
+
+
+
+
diff --git a/app/public/admin/actions/peertube-orphans.php b/app/public/admin/actions/peertube-orphans.php
index c482697..c38b21d 100644
--- a/app/public/admin/actions/peertube-orphans.php
+++ b/app/public/admin/actions/peertube-orphans.php
@@ -14,108 +14,10 @@ AdminAuth::requireLogin();
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
- exit;
+ return;
}
-require_once APP_ROOT . '/src/Database.php';
-require_once APP_ROOT . '/src/PeerTubeService.php';
-
-$db = new Database();
-
-if (!PeerTubeService::isConfigured($db)) {
- header('Content-Type: application/json; charset=utf-8');
- echo json_encode([
- 'configured' => false,
- 'error' => 'PeerTube non configuré.',
- ]);
- exit;
-}
-
-// ── Collect all Peertube UUIDs linked in the DB ──────────────────────────
-$pdo = $db->getPDO();
-$dbUuids = [];
-$linkedMap = []; // uuid → [thesis_id, thesis_title, thesis_identifier]
-
-$stmt = $pdo->query(
- "SELECT tf.file_path, tf.file_name, t.id AS thesis_id, t.title, t.identifier
- FROM thesis_files tf
- JOIN theses t ON t.id = tf.thesis_id
- WHERE tf.file_path LIKE 'peertube_ids:%'
- AND t.deleted_at IS NULL"
-);
-while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
- $uuid = substr($row['file_path'], strlen('peertube_ids:'));
- $dbUuids[$uuid] = true;
- $linkedMap[$uuid][] = [
- 'thesis_id' => (int)$row['thesis_id'],
- 'title' => $row['title'],
- 'identifier' => $row['identifier'] ?? '',
- ];
-}
-
-// ── List all channel videos ──────────────────────────────────────────────
-try {
- $channelVideos = PeerTubeService::listChannelVideos($db);
-} catch (\Throwable $e) {
- header('Content-Type: application/json; charset=utf-8');
- echo json_encode([
- 'configured' => true,
- 'error' => 'Erreur lors du listage des vidéos : ' . $e->getMessage(),
- ]);
- exit;
-}
-
-// ── Find orphans: on channel but not in DB ───────────────────────────────
-$orphans = [];
-$linked = [];
-foreach ($channelVideos as $v) {
- $uuid = $v['shortUUID'] ?: $v['uuid'];
- if ($uuid === '') {
- continue;
- }
- if (isset($dbUuids[$uuid])) {
- $linked[] = [
- 'uuid' => $uuid,
- 'name' => $v['name'],
- 'theses' => $linkedMap[$uuid] ?? [],
- ];
- } else {
- $orphans[] = [
- 'uuid' => $uuid,
- 'name' => $v['name'],
- 'createdAt' => $v['createdAt'],
- ];
- }
-}
-
-// ── Find stale DB entries: in DB but not on channel ──────────────────────
-$stale = [];
-foreach ($dbUuids as $uuid => $_) {
- $found = false;
- foreach ($channelVideos as $v) {
- if (($v['shortUUID'] ?: $v['uuid']) === $uuid) {
- $found = true;
- break;
- }
- }
- if (!$found) {
- $stale[] = [
- 'uuid' => $uuid,
- 'theses' => $linkedMap[$uuid] ?? [],
- ];
- }
-}
-
-$totalOnChannel = count($channelVideos);
+require_once __DIR__ . '/_peertube-orphans-data.php';
header('Content-Type: application/json; charset=utf-8');
-echo json_encode([
- 'configured' => true,
- 'channel_name' => PeerTubeService::getSettings($db)['channel_name'],
- 'total_on_channel' => $totalOnChannel,
- 'total_linked' => count($linked),
- 'orphan_count' => count($orphans),
- 'orphans' => $orphans,
- 'stale_count' => count($stale),
- 'stale_entries' => $stale,
-]);
+echo json_encode(getPeerTubeOrphansData());
diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css
index 167f6b3..34b2e9f 100644
--- a/app/public/assets/css/admin.css
+++ b/app/public/assets/css/admin.css
@@ -2235,6 +2235,9 @@ th.admin-ap-col {
.admin-main--toc > article > section {
margin-bottom: var(--space-xl);
+ border: none;
+ border-radius: 0;
+ padding: 0;
}
.admin-main--toc > article > section > fieldset {
diff --git a/app/templates/admin/cleanup.php b/app/templates/admin/cleanup.php
index e1fdefe..d0e76d1 100644
--- a/app/templates/admin/cleanup.php
+++ b/app/templates/admin/cleanup.php
@@ -1,47 +1,29 @@
-
-
+
+
+
-
-
- Téléversements et corbeille
+
+ Fichiers temporaires
-
+
Chargement…
-
- Vidéos PeerTube
+
+ Vidéos PeerTube
-
+
Chargement…
diff --git a/app/templates/admin/index.php b/app/templates/admin/index.php
index 6b476e3..e6213ff 100644
--- a/app/templates/admin/index.php
+++ b/app/templates/admin/index.php
@@ -28,11 +28,9 @@
Corbeille (= $trashCount ?>)
- 0): ?>
- = icon('trash') ?> Nettoyer (= $tmpTotalCount ?>)
+ = icon('trash') ?> Nettoyer= $tmpTotalCount > 0 ? " ($tmpTotalCount)" : '' ?>
-