diff --git a/TODO.md b/TODO.md
index a76ca97..1534a3c 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,5 +1,9 @@
# Current tasks
+## acces.php conflict marker cleanup
+- [x] Remove 206k lines of nested jj conflict markers from acces.php (resolved from clean nzllwsxo base)
+- [x] Restore missing features: create-result dialog, locked_year field, auto-generated password UI, file restrictions section, admin TOC wrapper
+
## Save fixes (files disappearing on edit/terminer)
- [x] Fix: note_intention deleted on save — handleFilePondSingleFile treats existing DB id as new upload, deletes existing, then can't re-process (integer vs hex mismatch)
- [x] Fix: cover removal now uses trash, same hex-vs-integer guard as note_intention
@@ -16,6 +20,8 @@
- [x] Frontend: modal with folder browser, triggered by a "Relier" button next to each FilePond pool
- [x] JS: integrate relink button into FilePond UI (XamxamOpenFileBrowser + XamxamRelinkFile)
- [x] CSS: .relink-modal + .file-browser styles in form.css
+- [x] Fix: relinked file not appearing in FilePond pool — add file metadata to addFile() options and extensive diag logging
+- [ ] Migration: rename existing theses/ directories to documents/ on disk and update DB paths
## Trash policy
- [x] FilePond remove moves to tmp/_trash (already implemented in handleRemove)
@@ -71,6 +77,12 @@
- [x] `partage/index.php`: fix fragment routing — `$slug` was `'fragments'` but check used `str_starts_with($slug, 'fragments/')`, causing HTMX fragments to redirect to / (main page)
- [x] Deploy: `just deploy` + `just deploy-nginx`
+## File browser fixes
+- [x] Fix: top-folder navigation regex doesn't match bare `documents`/`theses` (requires trailing slash)
+- [x] Replace emoji icons (📁📄) with proper SVG icons (folder, pdf, file-archive, text-file)
+- [x] Fix relink endpoint: always return JSON (even on errors), guard finfo class, add diagnostic logging
+- [x] Fix JS relink error handler to parse JSON error responses
+
## Previous items
- [x] Step 1 — Build 4 PHP endpoints (process.php, revert.php, load.php, remove.php)
diff --git a/app/public/admin/actions/filepond/relink.php b/app/public/admin/actions/filepond/relink.php
index a33cf22..b7bd67f 100644
--- a/app/public/admin/actions/filepond/relink.php
+++ b/app/public/admin/actions/filepond/relink.php
@@ -14,23 +14,29 @@ require_once __DIR__ . '/../../../../bootstrap.php';
require_once __DIR__ . '/../../../../src/AdminAuth.php';
AdminAuth::requireLogin();
+// Always return JSON, even on errors
+function relinkError(int $code, string $message): never {
+ http_response_code($code);
+ header('Content-Type: application/json');
+ echo json_encode(['ok' => false, 'error' => $message]);
+ exit;
+}
+
// CSRF via header
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
+error_log('[relink] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | csrf=' . (isset($_SESSION['csrf_token']) ? 'set' : 'missing') . ' | header=' . (strlen($csrfHeader) > 0 ? substr($csrfHeader, 0, 8) . '...' : 'empty') . ' | body_len=' . strlen(file_get_contents('php://input')));
if (!isset($_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $csrfHeader)) {
- http_response_code(403);
- die('Token CSRF invalide.');
+ relinkError(403, 'Token CSRF invalide.');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
- http_response_code(405);
- die('Méthode non autorisée.');
+ relinkError(405, 'Méthode non autorisée.');
}
$body = json_decode(file_get_contents('php://input'), true);
if (!is_array($body)) {
- http_response_code(400);
- die('JSON invalide.');
+ relinkError(400, 'JSON invalide.');
}
$thesisId = filter_var($body['thesis_id'] ?? '', FILTER_VALIDATE_INT);
@@ -42,14 +48,12 @@ $queueType = trim($body['queue_type'] ?? '');
$mimeType = trim($body['mime_type'] ?? 'application/octet-stream');
if (!$thesisId || $filePath === '') {
- http_response_code(400);
- die('Paramètres invalides (thesis_id + file_path requis).');
+ relinkError(400, 'Paramètres invalides (thesis_id + file_path requis).');
}
// Security: only allow paths under documents/ or theses/
if (!preg_match('#^(documents|theses)/#', $filePath)) {
- http_response_code(403);
- die('Chemin de fichier non autorisé.');
+ relinkError(403, 'Chemin de fichier non autorisé.');
}
$absPath = STORAGE_ROOT . '/' . $filePath;
@@ -57,19 +61,21 @@ $realPath = realpath($absPath);
$realStorage = realpath(STORAGE_ROOT);
if ($realPath === false || !str_starts_with($realPath, $realStorage)) {
- http_response_code(404);
- die('Fichier introuvable ou chemin interdit.');
+ error_log('[relink] FILE NOT FOUND | absPath=' . $absPath . ' | realPath=' . var_export($realPath, true) . ' | realStorage=' . $realStorage);
+ relinkError(404, 'Fichier introuvable ou chemin interdit.');
}
if (!is_file($realPath)) {
- http_response_code(404);
- die('Le chemin ne pointe pas vers un fichier.');
+ relinkError(404, 'Le chemin ne pointe pas vers un fichier.');
}
// Detect MIME if not provided
-if ($mimeType === 'application/octet-stream') {
+if ($mimeType === 'application/octet-stream' && class_exists('finfo')) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
- $mimeType = $finfo->file($realPath);
+ $detected = $finfo->file($realPath);
+ if ($detected !== false && $detected !== '') {
+ $mimeType = $detected;
+ }
}
// Map queue_type to file_type if not explicitly given
@@ -91,8 +97,7 @@ $pdo = $db->getConnection();
$stmt = $pdo->prepare('SELECT id FROM thesis_files WHERE thesis_id = ? AND file_path = ?');
$stmt->execute([$thesisId, $filePath]);
if ($stmt->fetch()) {
- http_response_code(409);
- die('Ce fichier est déjà lié à ce TFE.');
+ relinkError(409, 'Ce fichier est déjà lié à ce TFE.');
}
$db->insertThesisFile(
diff --git a/app/public/admin/fragments/file-browser.php b/app/public/admin/fragments/file-browser.php
index be6d003..2570795 100644
--- a/app/public/admin/fragments/file-browser.php
+++ b/app/public/admin/fragments/file-browser.php
@@ -13,9 +13,11 @@ AdminAuth::requireLogin();
$storageRoot = STORAGE_ROOT;
+error_log('[file-browser] ENTRY | dir=' . ($_GET['dir'] ?? '(root)') . ' | storageRoot=' . $storageRoot);
+
// Determine which directory to browse
$relDir = trim($_GET['dir'] ?? '', '/');
-if ($relDir !== '' && !preg_match('#^(documents|theses)/#', $relDir)) {
+if ($relDir !== '' && !preg_match('#^(documents|theses)(/|$)#', $relDir)) {
$relDir = '';
}
@@ -83,6 +85,24 @@ if ($relDir !== '') {
}
$rootDirs = ['documents', 'theses'];
+
+// SVG icon for a given extension
+function fileIcon(string $ext): string {
+ $ext = strtolower($ext);
+ if ($ext === 'pdf') {
+ return '';
+ }
+ if (in_array($ext, ['zip', 'tar', 'gz', 'bz2', 'xz', '7z', 'rar'], true)) {
+ return '';
+ }
+ // Default text-file icon for all other extensions
+ return '';
+}
+
+// SVG folder icon (same for all directories)
+function folderIcon(): string {
+ return '';
+}
?>
@@ -94,7 +114,7 @@ $rootDirs = ['documents', 'theses'];
- 📁
+ = folderIcon() ?>
= htmlspecialchars($rd) ?>/
@@ -104,7 +124,7 @@ $rootDirs = ['documents', 'theses'];