feat: fix file deletion on save + trash policy + documents/ prefix + relink browser

1. note_intention: Delete old file only when a genuinely new upload arrives
   (32-char hex file_id), not when the FilePond pool preserves an existing
   file by sending its DB integer ID.  Previously the DB integer ID
   triggered $hasNewNote=true, which deleted the existing note_intention
   from disk+DB, then handleFilePondSingleFile couldn't re-process it
   because the regex requires a hex pattern.  Same fix applied to cover.

2. All file deletions now use deleteThesisFileToTrash() which renames
   files to tmp/_trash/ instead of unlinking.  The trash preserves
   original filenames prefixed with DB id for traceability.  Skips
   website URLs and PeerTube refs (no disk file).

3. Storage prefix changed from theses/ to documents/ to reflect that
   the folder holds all document types (determined by file_type in DB).
   MediaController visibility gate supports both prefixes for backward
   compat with existing files.

4. File browser + relink feature for orphaned files:
   - /admin/fragments/file-browser.php — HTMX tree browser for
     storage/documents/ and storage/theses/
   - /admin/actions/filepond/relink.php — POST endpoint that inserts
     a thesis_files row pointing to existing on-disk file
   - Per-pool "📂 Relier" buttons (edit mode only)
   - JS: XamxamOpenFileBrowser / XamxamRelinkFile with FilePond integration
   - CSS: .relink-modal dialog + .file-browser tree styles
This commit is contained in:
Pontoporeia
2026-05-13 14:58:15 +02:00
parent 6f7a02244f
commit 79eddf5d5a
30 changed files with 191580 additions and 187 deletions

64
TODO.md
View File

@@ -1,7 +1,32 @@
# Current tasks # Current tasks
## 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
- [x] Fix: all file deletions now route through deleteThesisFileToTrash (renames to tmp/_trash instead of unlinking)
## Storage restructure
- [x] Move storage root from theses/ to documents/ (ThesisFileHandler, ThesisEditController, ThesisCreateController, MediaController)
- [x] MediaController: support both theses/ and documents/ prefixes for visibility gate
- [ ] Migration: rename existing theses/ directories to documents/ on disk and update DB paths
## Relink feature
- [x] Backend: endpoint to browse documents/ directory (file-browser.php with HTMX tree)
- [x] Backend: endpoint to relink an existing file to a thesis (relink.php inserts thesis_files row)
- [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
## Trash policy
- [x] FilePond remove moves to tmp/_trash (already implemented in handleRemove)
- [x] Fix: partage FilePond asks admin password — shared handler + separate partage endpoints with share_active session gate - [x] Fix: partage FilePond asks admin password — shared handler + separate partage endpoints with share_active session gate
- [ ] Deploy: just deploy (includes new partage/actions/filepond/ + FilepondHandler.php) - [x] Fix: mots-clé HTMX search — restored tag-search-fragment.php logic lost during fragment architecture refactor
- [x] Generalize pill-search: single fragment endpoint (type=tag|language|supervisor), deduplicate tag & language backends, add jury autocomplete (promoteur·ice interne/externe ULB, lecteur·ice interne/externe)
- [x] Deploy: just deploy (includes new partage/actions/filepond/ + FilepondHandler.php)
- [x] Fix: language pill-search showing mots-clé results — form field name collision; replaced hidden inputs with scoped hx-vals; fixed exclude logic per type
- [x] Add Créer button to jury supervisor autocomplete (removed guard in pill-search-fragment.php)
- [x] Fix: UNIQUE constraint on authors.email — findOrCreateAuthor now checks for existing author by email before inserting; prevents crash when two authors share an email
# Current tasks # Current tasks
@@ -44,7 +69,7 @@
- [x] `admin/parametres.php`: always-visible accessibility table (Normal vs Maintenance) - [x] `admin/parametres.php`: always-visible accessibility table (Normal vs Maintenance)
- [x] `admin.css`: `.param-access-table` styles (border-radius via overflow:hidden, green/secondary colours) - [x] `admin.css`: `.param-access-table` styles (border-radius via overflow:hidden, green/secondary colours)
- [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] `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)
- [ ] Deploy: `just deploy` + `just deploy-nginx` - [x] Deploy: `just deploy` + `just deploy-nginx`
## Previous items ## Previous items
@@ -62,3 +87,38 @@
- [x] Add `script-src 'self' 'unsafe-inline'` to main CSP header (public pages use inline scripts + onclick handlers) - [x] Add `script-src 'self' 'unsafe-inline'` to main CSP header (public pages use inline scripts + onclick handlers)
- [x] Add `storage/tmp/filepond/*` to .gitignore + rsync exclude, with .gitkeep - [x] Add `storage/tmp/filepond/*` to .gitignore + rsync exclude, with .gitkeep
- [ ] Deploy: `just deploy` to sync vendor JS files + updated CSP + .gitkeep to server - [ ] Deploy: `just deploy` to sync vendor JS files + updated CSP + .gitkeep to server
# improvements_postlaunch — Année verrouillable dans partage + correction ID
## Implémentation
### 1. Schema: ajouter locked_year aux share_links
- [ ] `Database::runMigrations()`: ALTER TABLE share_links ADD COLUMN locked_year INTEGER
- [ ] `app/storage/schema.sql`: ajouter la colonne
### 2. ShareLink model: lire/écrire locked_year
- [ ] `ShareLink::create()`: accepter et stocker locked_year
- [ ] `ShareLink::update()`: accepter et stocker locked_year
- [ ] `findBySlug()` retourne déjà SELECT *, donc locked_year remonte automatiquement
### 3. Admin UI — Dialog de création de lien
- [ ] Ajouter champ "Année académique verrouillée" dans create-dialog (acces.php)
- [ ] Ajouter champ dans edit-dialog (acces.php)
### 4. Admin UI — Liste des liens
- [ ] Afficher colonne "Année" dans le tableau des liens (acces.php)
### 5. Admin actions (acces-etudiante.php)
- [ ] Lire locked_year depuis $_POST dans action 'create' et 'update'
- [ ] Passer au ShareLink model
### 6. Partage — Formulaire
- [ ] `partage/index.php` (`renderShareLinkForm`): lire locked_year depuis le lien
- [ ] `fieldset-academic.php`: quand $lockedYear est défini → hidden input + span "Année académique verrouillée : YYYY" + explication; quand null → comportement actuel
- [ ] `ThesisCreateController::validateAndSanitise()`: respecter locked_year si présent dans POST (priorité sur $_POST['année'])
### 7. Admin edit.php — Forcer l'identifiant
- [ ] Ajouter un champ "Identifiant" en lecture seule mais avec un bouton "Regénérer"
- [ ] `ThesisEditController`: ajouter méthode `regenerateIdentifier()` qui reconstruit YYYY-NNN avec MAX+1 sur la nouvelle année
- [ ] `Database`: méthode `regenerateThesisIdentifier(int $thesisId, int $year)` — met à jour identifier basé sur l'année dans un SELECT FOR UPDATE
- [ ] Attention: renommer les dossiers de fichiers sur disque si l'identifiant change

View File

@@ -38,7 +38,15 @@ switch ($action) {
$validObjet = ['tfe', 'thèse', 'frart']; $validObjet = ['tfe', 'thèse', 'frart'];
$selected = is_array($objetRaw) ? array_intersect($objetRaw, $validObjet) : []; $selected = is_array($objetRaw) ? array_intersect($objetRaw, $validObjet) : [];
$objetRestriction = !empty($selected) ? implode(',', $selected) : 'tfe'; $objetRestriction = !empty($selected) ? implode(',', $selected) : 'tfe';
$link = $shareLink->create(1, $expiresAt, $objetRestriction, $name); $lockedYearRaw = $_POST['locked_year'] ?? null;
$lockedYear = null;
if ($lockedYearRaw !== null && $lockedYearRaw !== '') {
$lockedYear = filter_var($lockedYearRaw, FILTER_VALIDATE_INT);
if ($lockedYear === false || $lockedYear < 2000 || $lockedYear > ((int)date('Y') + 3)) {
$lockedYear = null;
}
}
$link = $shareLink->create(1, $expiresAt, $objetRestriction, $name, $lockedYear);
$logger->logLinkCreate( $logger->logLinkCreate(
$link['slug'] ?? '', $link['slug'] ?? '',
true, // Always has password true, // Always has password
@@ -86,7 +94,13 @@ switch ($action) {
if ($id > 0) { if ($id > 0) {
$name = isset($_POST['name']) ? trim($_POST['name']) : null; $name = isset($_POST['name']) ? trim($_POST['name']) : null;
$expiresRaw = isset($_POST['expires_at']) ? trim($_POST['expires_at']) : null; $expiresRaw = isset($_POST['expires_at']) ? trim($_POST['expires_at']) : null;
$shareLink->update($id, $name, $expiresRaw); // locked_year: null=not sent (keep), ""=clear, otherwise year string
$lockedYearRaw = $_POST['locked_year'] ?? null;
$lockedYear = null; // default: not sent → don't change
if ($lockedYearRaw !== null) {
$lockedYear = $lockedYearRaw; // pass through: "" for clear, "2026" for set
}
$shareLink->update($id, $name, $expiresRaw, $lockedYear);
App::redirect('/admin/acces.php', success: 'Lien mis à jour.'); App::redirect('/admin/acces.php', success: 'Lien mis à jour.');
} else { } else {
App::redirect('/admin/acces.php', error: 'Lien introuvable.'); App::redirect('/admin/acces.php', error: 'Lien introuvable.');

View File

@@ -0,0 +1,120 @@
<?php
/**
* Relink endpoint — attach an orphaned on-disk file to a thesis.
*
* POST /admin/actions/filepond/relink.php
* Body: JSON { thesis_id: 123, file_path: "documents/2025/FOLDER/file.pdf",
* file_name: "file.pdf", file_size: 12345, mime_type: "application/pdf",
* queue_type: "tfe", file_type: "main" }
*
* Inserts a thesis_files row pointing to the existing on-disk file.
* The file is NOT copied or moved — it stays where it is.
*/
require_once __DIR__ . '/../../../../bootstrap.php';
require_once __DIR__ . '/../../../../src/AdminAuth.php';
AdminAuth::requireLogin();
// CSRF via header
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!isset($_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $csrfHeader)) {
http_response_code(403);
die('Token CSRF invalide.');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
die('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.');
}
$thesisId = filter_var($body['thesis_id'] ?? '', FILTER_VALIDATE_INT);
$filePath = trim($body['file_path'] ?? '');
$fileName = trim($body['file_name'] ?? basename($filePath));
$fileSize = (int)($body['file_size'] ?? 0);
$fileType = trim($body['file_type'] ?? '');
$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).');
}
// Security: only allow paths under documents/ or theses/
if (!preg_match('#^(documents|theses)/#', $filePath)) {
http_response_code(403);
die('Chemin de fichier non autorisé.');
}
$absPath = STORAGE_ROOT . '/' . $filePath;
$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.');
}
if (!is_file($realPath)) {
http_response_code(404);
die('Le chemin ne pointe pas vers un fichier.');
}
// Detect MIME if not provided
if ($mimeType === 'application/octet-stream') {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($realPath);
}
// Map queue_type to file_type if not explicitly given
if ($fileType === '') {
$fileTypeMap = [
'cover' => 'cover',
'note_intention' => 'note_intention',
'tfe' => 'main',
'annexe' => 'annex',
];
$fileType = $fileTypeMap[$queueType] ?? 'main';
}
require_once APP_ROOT . '/src/Database.php';
$db = Database::getInstance();
// Check if this file is already linked to this thesis (avoid duplicate)
$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.');
}
$db->insertThesisFile(
$thesisId,
$fileType,
$filePath,
$fileName,
$fileSize,
$mimeType,
null,
null
);
// Get the new file's ID
$newId = $pdo->lastInsertId();
error_log("[relink] thesis_id=$thesisId file_path=$filePath file_type=$fileType new_id=$newId");
header('Content-Type: application/json');
echo json_encode([
'ok' => true,
'id' => (int)$newId,
'message' => 'Fichier relié avec succès.',
]);
exit;

View File

@@ -55,7 +55,7 @@ function wasSelected($key, $value) {
$isAdmin = true; $isAdmin = true;
$bodyClass = 'admin-body'; $bodyClass = 'admin-body';
$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; $extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css'];
$extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js', '/assets/js/app/beforeunload-guard.js', '/assets/js/app/pill-search.js']; $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js', '/assets/js/app/beforeunload-guard.js', '/assets/js/app/pill-search.js', '/assets/js/app/jury-autocomplete.js'];
require_once APP_ROOT . '/templates/head.php'; require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/header.php';
include APP_ROOT . '/templates/admin/add.php'; include APP_ROOT . '/templates/admin/add.php';

View File

@@ -40,7 +40,7 @@ try {
$isAdmin = true; $bodyClass = 'admin-body'; $isAdmin = true; $bodyClass = 'admin-body';
$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; $extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css'];
$extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js', '/assets/js/app/beforeunload-guard.js', '/assets/js/app/pill-search.js']; $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js', '/assets/js/app/beforeunload-guard.js', '/assets/js/app/pill-search.js', '/assets/js/app/jury-autocomplete.js'];
require_once APP_ROOT . '/templates/head.php'; require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/header.php';
include APP_ROOT . '/templates/admin/edit.php'; include APP_ROOT . '/templates/admin/edit.php';

View File

@@ -0,0 +1,146 @@
<?php
/**
* File browser fragment — returns a clickable directory tree of documents/ + theses/.
*
* GET /admin/fragments/file-browser.php?dir=documents/2025
*
* Used by the relink modal to let admins pick an orphaned file and reattach
* it to a thesis entry.
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
$storageRoot = STORAGE_ROOT;
// Determine which directory to browse
$relDir = trim($_GET['dir'] ?? '', '/');
if ($relDir !== '' && !preg_match('#^(documents|theses)/#', $relDir)) {
$relDir = '';
}
$currentAbs = $storageRoot . '/' . ($relDir !== '' ? $relDir . '/' : '');
// Security: prevent escaping STORAGE_ROOT
$realCurrent = realpath($currentAbs);
$realStorage = realpath($storageRoot);
if ($realCurrent === false || !str_starts_with($realCurrent, $realStorage)) {
http_response_code(403);
die('Chemin interdit.');
}
// Build breadcrumb
$breadcrumb = [];
if ($relDir !== '') {
$parts = explode('/', $relDir);
$accum = '';
foreach ($parts as $p) {
$accum = ltrim($accum . '/' . $p, '/');
$breadcrumb[] = ['label' => $p, 'dir' => $accum];
}
}
// Gather entries
$entries = [];
if (is_dir($realCurrent)) {
$dh = opendir($realCurrent);
if ($dh) {
while (($name = readdir($dh)) !== false) {
if ($name === '.' || $name === '..') continue;
$full = $realCurrent . '/' . $name;
$isDir = is_dir($full);
$entries[] = [
'name' => $name,
'is_dir' => $isDir,
'size' => $isDir ? null : filesize($full),
'ext' => $isDir ? null : strtolower(pathinfo($name, PATHINFO_EXTENSION)),
];
}
closedir($dh);
}
}
// Sort: dirs first, then files, alphabetical
usort($entries, function ($a, $b) {
if ($a['is_dir'] !== $b['is_dir']) return $a['is_dir'] ? -1 : 1;
return strnatcasecmp($a['name'], $b['name']);
});
// Human-readable filesize
function fmtSize(?int $bytes): string {
if ($bytes === null) return '';
if ($bytes >= 1_000_000_000) return round($bytes / 1_000_000_000, 1) . ' GB';
if ($bytes >= 1_000_000) return round($bytes / 1_000_000, 1) . ' MB';
if ($bytes >= 1_000) return round($bytes / 1_000, 1) . ' KB';
return $bytes . ' B';
}
// Determine parent dir
$parentDir = '';
if ($relDir !== '') {
$parentParts = explode('/', $relDir);
array_pop($parentParts);
$parentDir = implode('/', $parentParts);
}
$rootDirs = ['documents', 'theses'];
?>
<div id="file-browser-container" class="file-browser">
<?php if ($relDir === ''): ?>
<!-- Root: show top-level directories -->
<p class="file-browser-hint">Sélectionnez un dossier pour parcourir les fichiers orphelins.</p>
<ul class="file-browser-list">
<?php foreach ($rootDirs as $rd): ?>
<?php $rdAbs = $storageRoot . '/' . $rd; ?>
<li class="file-browser-entry file-browser-dir">
<a href="#" hx-get="/admin/fragments/file-browser.php?dir=<?= urlencode($rd) ?>"
hx-target="#file-browser-container" hx-swap="outerHTML">
<span class="file-browser-icon">📁</span>
<span class="file-browser-name"><?= htmlspecialchars($rd) ?>/</span>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<!-- Subdirectory: breadcrumb + entries -->
<nav class="file-browser-breadcrumb">
<a href="#" hx-get="/admin/fragments/file-browser.php"
hx-target="#file-browser-container" hx-swap="outerHTML">📂 racine</a>
<?php foreach ($breadcrumb as $i => $bc): ?>
<span class="file-browser-sep">/</span>
<a href="#" hx-get="/admin/fragments/file-browser.php?dir=<?= urlencode($bc['dir']) ?>"
hx-target="#file-browser-container" hx-swap="outerHTML"><?= htmlspecialchars($bc['label']) ?></a>
<?php endforeach; ?>
</nav>
<?php if (empty($entries)): ?>
<p class="file-browser-empty">Ce dossier est vide.</p>
<?php else: ?>
<ul class="file-browser-list">
<?php foreach ($entries as $e): ?>
<?php if ($e['is_dir']): ?>
<li class="file-browser-entry file-browser-dir">
<a href="#" hx-get="/admin/fragments/file-browser.php?dir=<?= urlencode($relDir . '/' . $e['name']) ?>"
hx-target="#file-browser-container" hx-swap="outerHTML">
<span class="file-browser-icon">📁</span>
<span class="file-browser-name"><?= htmlspecialchars($e['name']) ?>/</span>
</a>
</li>
<?php else: ?>
<li class="file-browser-entry file-browser-file"
data-file-path="<?= htmlspecialchars($relDir . '/' . $e['name']) ?>"
data-file-name="<?= htmlspecialchars($e['name']) ?>"
data-file-ext="<?= htmlspecialchars($e['ext'] ?? '') ?>"
data-file-size="<?= (int)($e['size'] ?? 0) ?>">
<button type="button" class="file-browser-select-btn"
onclick="XamxamRelinkFile(this)">
<span class="file-browser-icon">📄</span>
<span class="file-browser-name"><?= htmlspecialchars($e['name']) ?></span>
<span class="file-browser-size"><?= htmlspecialchars(fmtSize($e['size'])) ?></span>
</button>
</li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,10 @@
<?php
/**
* Admin fragment: generic pill-search (HTMX partial).
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
App::boot();
require_once APP_ROOT . '/public/partage/pill-search-fragment.php';

View File

@@ -1023,6 +1023,19 @@
pointer-events: none; pointer-events: none;
} }
/* ── Jury autocomplete dropdown (positioned within .admin-jury-lecteurs) ──── */
.admin-jury-lecteurs[data-jury-autocomplete] {
position: relative;
}
.admin-jury-lecteurs .jury-suggestions {
position: absolute;
top: auto;
left: 0;
right: 0;
margin-top: 2px;
}
/* Individual suggestion item */ /* Individual suggestion item */
.tag-search-item { .tag-search-item {
display: flex; display: flex;
@@ -1340,3 +1353,134 @@ legend {
color: var(--text-secondary); color: var(--text-secondary);
font-style: italic; font-style: italic;
} }
/* ── File browser (relink) ──────────────────────────────────────── */
.file-browser-trigger {
margin-top: var(--space-2xs);
font-size: var(--step--2);
}
.relink-modal {
width: min(90vw, 700px);
max-height: 80vh;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-primary);
padding: 0;
overflow: hidden;
color: var(--text-primary);
}
.relink-modal::backdrop {
background: rgba(0,0,0,0.5);
}
.relink-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-s) var(--space-m);
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
}
.relink-modal-header h3 {
margin: 0;
font-size: var(--step-0);
}
.relink-modal-footer {
padding: var(--space-xs) var(--space-m);
border-top: 1px solid var(--border);
background: var(--bg-secondary);
}
#relink-modal-body {
padding: var(--space-s) var(--space-m);
overflow-y: auto;
max-height: 60vh;
}
/* ── File browser tree ─────────────────────────────────────────── */
.file-browser {
font-size: var(--step--1);
}
.file-browser-hint,
.file-browser-empty,
.file-browser-loading,
.file-browser-error {
color: var(--text-secondary);
text-align: center;
padding: var(--space-m);
}
.file-browser-error {
color: var(--error);
}
.file-browser-breadcrumb {
display: flex;
align-items: center;
gap: var(--space-2xs);
flex-wrap: wrap;
padding-bottom: var(--space-s);
margin-bottom: var(--space-s);
border-bottom: 1px solid var(--border);
font-size: var(--step--2);
}
.file-browser-breadcrumb a {
color: var(--accent-blue, var(--link));
text-decoration: none;
}
.file-browser-breadcrumb a:hover {
text-decoration: underline;
}
.file-browser-sep {
color: var(--text-tertiary);
}
.file-browser-list {
list-style: none;
margin: 0;
padding: 0;
}
.file-browser-entry {
border-bottom: 1px solid var(--border-subtle);
}
.file-browser-entry a,
.file-browser-select-btn {
display: flex;
align-items: center;
gap: var(--space-xs);
width: 100%;
padding: var(--space-xs) var(--space-2xs);
text-decoration: none;
color: var(--text-primary);
background: none;
border: none;
cursor: pointer;
font-size: var(--step--1);
text-align: left;
transition: background 0.15s;
}
.file-browser-entry a:hover,
.file-browser-select-btn:hover {
background: var(--bg-secondary);
}
.file-browser-icon {
flex-shrink: 0;
font-size: var(--step-0);
}
.file-browser-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-browser-size {
flex-shrink: 0;
color: var(--text-tertiary);
font-size: var(--step--2);
font-variant-numeric: tabular-nums;
}
.file-browser-file .file-browser-select-btn {
color: var(--accent-green, var(--success));
}
.file-browser-file .file-browser-select-btn:hover {
background: var(--bg-secondary);
}

View File

@@ -550,4 +550,131 @@
window.__xamxamDirty = false; window.__xamxamDirty = false;
} }
}); });
// ── Relink file browser ──────────────────────────────────────────
/**
* Open the file browser modal for a specific queue type.
* Triggered by the "📂 Relier un fichier" button.
*/
window.XamxamOpenFileBrowser = (btn) => {
var queueType = btn.dataset.queueType;
var thesisId = btn.dataset.thesisId;
// Store context for the relink callback
window.__xamxamRelinkCtx = {
queueType: queueType,
thesisId: thesisId,
};
var modal = document.getElementById('relink-modal');
if (!modal) {
console.error('[relink] modal #relink-modal not found');
return;
}
var body = document.getElementById('relink-modal-body');
body.innerHTML = '<p class="file-browser-loading">Chargement…</p>';
modal.showModal();
// Load the file browser via HTMX (or fetch if htmx not available)
if (window.htmx) {
window.htmx.ajax('GET', '/admin/fragments/file-browser.php', {
target: '#relink-modal-body',
swap: 'innerHTML',
});
} else {
fetch('/admin/fragments/file-browser.php')
.then(r => r.text())
.then(html => { body.innerHTML = html; })
.catch(() => { body.innerHTML = '<p class="file-browser-error">Erreur de chargement.</p>'; });
}
};
/**
* Relink a selected file to the thesis.
* Triggered when a file is clicked in the file browser.
*/
window.XamxamRelinkFile = (el) => {
var li = el.closest('.file-browser-entry');
if (!li) return;
var ctx = window.__xamxamRelinkCtx || {};
var thesisId = ctx.thesisId;
var queueType = ctx.queueType;
var filePath = li.dataset.filePath;
var fileName = li.dataset.fileName;
var fileSize = parseInt(li.dataset.fileSize, 10) || 0;
var ext = li.dataset.fileExt || '';
if (!filePath || !thesisId || !queueType) {
console.error('[relink] missing data', { filePath, thesisId, queueType });
return;
}
// Determine MIME from extension
var mimeMap = {
pdf: 'application/pdf',
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', gif: 'image/gif',
mp4: 'video/mp4', webm: 'video/webm', ogv: 'video/ogg', mov: 'video/quicktime',
mp3: 'audio/mpeg', ogg: 'audio/ogg', wav: 'audio/wav', flac: 'audio/flac', aac: 'audio/aac', m4a: 'audio/mp4',
vtt: 'text/vtt',
zip: 'application/zip', tar: 'application/x-tar', gz: 'application/gzip',
};
var mimeType = mimeMap[ext] || 'application/octet-stream';
var csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
var bodyEl = document.getElementById('relink-modal-body');
if (bodyEl) bodyEl.innerHTML = '<p class="file-browser-loading">Reliage en cours…</p>';
fetch('/admin/actions/filepond/relink.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify({
thesis_id: parseInt(thesisId, 10),
file_path: filePath,
file_name: fileName,
file_size: fileSize,
mime_type: mimeType,
queue_type: queueType,
}),
})
.then(r => r.json().then(data => ({ ok: r.ok, status: r.status, data })))
.then(({ ok, status, data }) => {
if (!ok) {
if (bodyEl) bodyEl.innerHTML = `<p class="file-browser-error">Erreur : ${data}</p>`;
return;
}
console.log('[relink] success | new_id=' + data.id);
// Add the new file to the FilePond pool
var input = document.querySelector(`.tfe-file-picker[data-queue-type="${queueType}"]`);
if (input) {
var pond = FilePond.find(input);
if (pond) {
pond.addFile({
source: String(data.id),
options: { type: 'local' },
});
}
}
// Close modal
var modal = document.getElementById('relink-modal');
if (modal) modal.close();
// Mark form dirty
window.__xamxamDirty = true;
})
.catch(err => {
console.error('[relink] fetch error', err);
if (bodyEl) bodyEl.innerHTML = '<p class="file-browser-error">Erreur réseau.</p>';
});
};
})(); })();

View File

@@ -0,0 +1,140 @@
/**
* jury-autocomplete.js — inlines autocomplete for jury member text inputs.
*
* Each jury sub-fieldset (<fieldset class="admin-jury-lecteurs">) gains a shared
* suggestion dropdown positioned below the last input. Typing in any input within
* that fieldset triggers an HTMX POST to the pill-search fragment (type=supervisor).
* Clicking a suggestion fills the input that triggered the search.
*
* Data attributes on the fieldset:
* data-jury-autocomplete — marks the fieldset for initialisation
* data-jury-hx-post — HTMX endpoint URL (required)
* data-jury-hx-target — CSS selector for the shared dropdown (optional)
*/
(function () {
'use strict';
function initAll() {
document.querySelectorAll('[data-jury-autocomplete]:not([data-jury-autocomplete-initialized])').forEach(function (fieldset) {
fieldset.setAttribute('data-jury-autocomplete-initialized', '1');
initFieldset(fieldset);
});
}
document.addEventListener('DOMContentLoaded', initAll);
document.body.addEventListener('htmx:afterSwap', initAll);
function initFieldset(fieldset) {
var hxPost = fieldset.getAttribute('data-jury-hx-post') || '/admin/fragments/pill-search.php';
var role = fieldset.getAttribute('data-jury-role') || '';
var dropdown = fieldset.querySelector('.jury-suggestions');
if (!dropdown) {
dropdown = document.createElement('div');
dropdown.className = 'jury-suggestions tag-search-suggestions';
dropdown.setAttribute('role', 'listbox');
// Insert after the list container
var list = fieldset.querySelector('.admin-jury-list');
if (list) {
list.insertAdjacentElement('afterend', dropdown);
} else {
fieldset.appendChild(dropdown);
}
}
var activeInput = null;
var selectedIdx = -1;
var debounceTimer = null;
// Click on suggestion → fill the active input
dropdown.addEventListener('click', function (e) {
var btn = e.target.closest('.tag-search-item');
if (!btn) return;
var name = (btn.getAttribute('data-tag-name') || '').trim();
if (!name || !activeInput) return;
activeInput.value = btn.classList.contains('tag-search-item--create')
? activeInput.value.trim()
: name;
dropdown.innerHTML = '';
selectedIdx = -1;
activeInput.focus();
});
// Highlighting helper
function highlight(idx) {
var items = dropdown.querySelectorAll('.tag-search-item');
for (var i = 0; i < items.length; i++) {
items[i].classList.toggle('tag-search-item--highlight', i === idx);
}
}
fieldset.addEventListener('input', function (e) {
var inp = e.target.closest('input[type="text"]');
if (!inp) return;
activeInput = inp;
var q = inp.value.trim();
// Build the hx-include query — include hidden type=supervisor
var typeInput = fieldset.querySelector('input[name="type"][value="supervisor"]');
var includeSelector = typeInput ? '[name="type"][value="supervisor"]' : '';
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(function () {
if (q === '') {
dropdown.innerHTML = '';
selectedIdx = -1;
return;
}
// Manual HTMX POST
var xhr = new XMLHttpRequest();
xhr.open('POST', hxPost);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.setRequestHeader('HX-Request', 'true');
xhr.onload = function () {
if (xhr.status === 200) {
dropdown.innerHTML = xhr.responseText;
selectedIdx = -1;
}
};
var params = 'type=supervisor&q=' + encodeURIComponent(q) + (role ? '&role=' + encodeURIComponent(role) : '');
xhr.send(params);
}, 200);
});
// Keyboard navigation
fieldset.addEventListener('keydown', function (e) {
var items = dropdown.querySelectorAll('.tag-search-item');
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
if (items.length === 0) return;
e.preventDefault();
if (e.key === 'ArrowDown') {
selectedIdx = (selectedIdx + 1) % items.length;
} else {
selectedIdx = selectedIdx <= 0 ? items.length - 1 : selectedIdx - 1;
}
highlight(selectedIdx);
} else if (e.key === 'Enter') {
if (items.length > 0 && dropdown.innerHTML !== '') {
e.preventDefault();
if (selectedIdx >= 0 && selectedIdx < items.length) {
items[selectedIdx].click();
} else {
items[0].click();
}
}
} else if (e.key === 'Escape') {
dropdown.innerHTML = '';
selectedIdx = -1;
}
});
// Close dropdown on outside click
document.addEventListener('click', function (e) {
if (!fieldset.contains(e.target)) {
dropdown.innerHTML = '';
selectedIdx = -1;
}
});
}
})();

View File

@@ -0,0 +1,8 @@
<?php
/**
* Partagé fragment: generic pill-search (HTMX partial).
*/
require_once __DIR__ . '/../../../bootstrap.php';
App::boot();
require_once APP_ROOT . '/public/partage/pill-search-fragment.php';

View File

@@ -79,6 +79,12 @@ if ($slug === 'validate-file-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST')
exit; exit;
} }
if ($slug === 'pill-search-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
App::boot();
require_once __DIR__ . '/fragments/pill-search.php';
exit;
}
// Special route: /partage/recapitulatif?id=N // Special route: /partage/recapitulatif?id=N
if ($slug === 'recapitulatif' || $slug === 'recapitulatif.php') { if ($slug === 'recapitulatif' || $slug === 'recapitulatif.php') {
App::boot(); App::boot();
@@ -434,6 +440,7 @@ function renderShareLinkForm(string $slug, array $link): void
<script src="<?= App::assetV('/assets/js/app/file-upload-filepond.js') ?>" defer></script> <script src="<?= App::assetV('/assets/js/app/file-upload-filepond.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/js/app/beforeunload-guard.js') ?>" defer></script> <script src="<?= App::assetV('/assets/js/app/beforeunload-guard.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/js/app/pill-search.js') ?>" defer></script> <script src="<?= App::assetV('/assets/js/app/pill-search.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/js/app/jury-autocomplete.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/js/vendor/htmx.min.js') ?>" defer></script> <script src="<?= App::assetV('/assets/js/vendor/htmx.min.js') ?>" defer></script>
</head> </head>
<body class="student-body"> <body class="student-body">
@@ -612,7 +619,7 @@ function handleShareLinkSubmission(string $slug): void
header('Location: /partage/' . urlencode($slug)); header('Location: /partage/' . urlencode($slug));
exit(); exit();
} catch (Exception $e) { } catch (\Throwable $e) {
$logger->logError('partage', $e->getMessage(), [ $logger->logError('partage', $e->getMessage(), [
'share_slug' => $slug, 'share_slug' => $slug,
'author' => $authorName, 'author' => $authorName,

View File

@@ -1,72 +1,10 @@
<?php <?php
/** /**
* language-search-fragment.php * language-search-fragment.php
* * @deprecated Use /partage/fragments/pill-search.php with type=language instead.
* Shared HTMX fragment: returns matching language suggestions for the * Kept for backward compatibility with existing hx-post URLs.
* "Autre(s) langue(s)" interactive search input.
*/ */
require_once __DIR__ . '/../../src/Database.php'; $_POST['type'] = 'language';
$_POST['q'] = $_POST['language_search_q'] ?? '';
$query = trim(preg_replace('/\s+/', ' ', strtolower($_POST['language_search_q'] ?? ''))); $_POST['exclude'] = $_POST['language_autre'] ?? [];
require_once __DIR__ . '/pill-search-fragment.php';
$currentLanguages = isset($_POST['language_autre']) && is_array($_POST['language_autre'])
? array_map(function($l) { return trim(preg_replace('/\s+/', ' ', strtolower($l))); }, $_POST['language_autre'])
: [];
error_log("[lang-search] q=" . var_export($query, true) . " cur=" . json_encode($currentLanguages));
$db = Database::getInstance();
$results = $db->searchLanguages($query);
error_log("[lang-search] raw results count=" . count($results) . " rows=" . json_encode(array_slice($results, 0, 5)));
// Deduplicate
$seen = [];
$results = array_values(array_filter($results, function($lang) use (&$seen) {
$key = strtolower($lang['name']);
if (isset($seen[$key])) return false;
$seen[$key] = true;
return true;
}));
// Exclude the main languages (handled by the checkbox list above)
$excludedLanguages = ['français', 'anglais', 'néerlandais'];
// Filter out already-selected and excluded main languages
$results = array_values(array_filter($results, function($lang) use ($currentLanguages, $excludedLanguages) {
$lower = strtolower($lang['name']);
return !in_array($lower, $currentLanguages, true)
&& !in_array($lower, $excludedLanguages, true);
}));
// Exact match check
$exactExists = false;
foreach ($results as $lang) {
if (strcasecmp($lang['name'], $query) === 0) {
$exactExists = true;
break;
}
}
$inCurrent = in_array($query, $currentLanguages, true);
$isExcluded = in_array($query, $excludedLanguages, true);
$canCreate = ($query !== '' && !$exactExists && !$inCurrent && !$isExcluded);
error_log("[lang-search] exactExists=" . var_export($exactExists, true) . " inCurrent=" . var_export($inCurrent, true) . " canCreate=" . var_export($canCreate, true));
?>
<?php if (empty($results) && !$canCreate): ?>
<div class="tag-search-empty">Aucune langue trouvée.</div>
<?php endif; ?>
<?php foreach ($results as $lang): ?>
<button type="button" class="tag-search-item" data-tag-id="<?= (int)$lang['id'] ?>" data-tag-name="<?= htmlspecialchars($lang['name']) ?>">
<span class="tag-search-item-name"><?= htmlspecialchars($lang['name']) ?></span>
<span class="tag-search-item-count">(<?= (int)$lang['thesis_count'] ?>)</span>
</button>
<?php endforeach; ?>
<?php if ($canCreate): ?>
<button type="button" class="tag-search-item tag-search-item--create" data-tag-name="<?= htmlspecialchars($query) ?>">
<span class="tag-search-item-name">Créer «&nbsp;<?= htmlspecialchars($query) ?>&nbsp;»</span>
</button>
<?php endif; ?>

View File

@@ -0,0 +1,123 @@
<?php
/**
* pill-search-fragment.php
*
* Generic HTMX fragment: returns matching suggestions for search inputs.
*
* Supports three entity types:
* - tag → used by mot-clé pill-search component
* - language → used by "Autre(s) langue(s)" pill-search component
* - supervisor → used by jury member text inputs (inline autocomplete, no pills)
*
* Included by:
* - /admin/fragments/pill-search.php (AdminAuth gated)
* - /partage/fragments/pill-search.php (public, session already booted)
*
* Expected POST:
* type — 'tag' | 'language' | 'supervisor'
* q — search query string (partial name)
* exclude[] — already-selected names to exclude from suggestions
*/
require_once __DIR__ . '/../../src/Database.php';
$db = Database::getInstance();
$type = trim($_POST['pill_type'] ?? $_POST['type'] ?? 'tag');
// Accept both generic 'q' and type-specific field names (tag_search_q, language_search_q)
// Use the type-specific field first to avoid collision when both are present (same <form>).
$rawQ = $_POST['q']
?? ($type === 'language' ? ($_POST['language_search_q'] ?? '') : ($_POST['tag_search_q'] ?? ''))
?? '';
$q = trim(preg_replace('/\s+/', ' ', strtolower((string)$rawQ)));
// Accept both generic 'exclude[]' and type-specific field names (tag[], language_autre[])
$exclude = [];
$rawExclude = $_POST['exclude']
?? ($type === 'language' ? ($_POST['language_autre'] ?? null) : ($_POST['tag'] ?? null))
?? null;
if (isset($rawExclude) && is_array($rawExclude)) {
$exclude = array_map(function($v) {
return trim(preg_replace('/\s+/', ' ', strtolower($v)));
}, $rawExclude);
}
$results = [];
$emptyMessage = 'Aucun résultat.';
$createLabel = 'Créer';
switch ($type) {
case 'language':
$results = $db->searchLanguages($q);
$emptyMessage = 'Aucune langue trouvée.';
$createLabel = 'Créer';
break;
case 'supervisor':
$role = trim($_POST['role'] ?? '');
$results = $db->searchSupervisors($q, $role);
$emptyMessage = 'Aucun·e membre trouvé·e.';
break;
default: // tag
$results = $db->searchTags($q);
$emptyMessage = 'Aucun mot-clé trouvé.';
$createLabel = 'Créer';
break;
}
// Deduplicate results by lowercase name
$seen = [];
$results = array_values(array_filter($results, function($item) use (&$seen) {
$key = strtolower($item['name']);
if (isset($seen[$key])) return false;
$seen[$key] = true;
return true;
}));
// Exclude already-selected items and, for languages, the main three
if ($type === 'language') {
$excludedMain = ['français', 'anglais', 'néerlandais'];
$results = array_values(array_filter($results, function($lang) use ($excludedMain, $exclude) {
$lower = strtolower($lang['name']);
return !in_array($lower, $excludedMain, true)
&& !in_array($lower, $exclude, true);
}));
} elseif (!empty($exclude)) {
$results = array_values(array_filter($results, function($item) use ($exclude) {
return !in_array(strtolower($item['name']), $exclude, true);
}));
}
// "Créer" button logic
$exactExists = false;
foreach ($results as $item) {
if (strcasecmp($item['name'], $q) === 0) {
$exactExists = true;
break;
}
}
$inExcluded = in_array($q, $exclude, true);
$canCreate = ($q !== '' && !$exactExists && !$inExcluded);
if ($type === 'language') {
$excludedMain = ['français', 'anglais', 'néerlandais'];
if (in_array($q, $excludedMain, true)) {
$canCreate = false;
}
}
?>
<?php if (empty($results) && !$canCreate): ?>
<div class="tag-search-empty"><?= htmlspecialchars($emptyMessage) ?></div>
<?php endif; ?>
<?php foreach ($results as $item): ?>
<button type="button" class="tag-search-item" data-tag-name="<?= htmlspecialchars($item['name']) ?>">
<span class="tag-search-item-name"><?= htmlspecialchars($item['name']) ?></span>
<span class="tag-search-item-count">(<?= (int)$item['thesis_count'] ?>)</span>
</button>
<?php endforeach; ?>
<?php if ($canCreate): ?>
<button type="button" class="tag-search-item tag-search-item--create" data-tag-name="<?= htmlspecialchars($q) ?>">
<span class="tag-search-item-name"><?= htmlspecialchars($createLabel) ?> «&nbsp;<?= htmlspecialchars($q) ?>&nbsp;»</span>
</button>
<?php endif; ?>

View File

@@ -1,6 +1,10 @@
<?php <?php
/** /**
* Partagé fragment: tag search (HTMX partial). * tag-search-fragment.php
* @deprecated Use /partage/fragments/tag-search.php instead. * @deprecated Use /partage/fragments/pill-search.php with type=tag instead.
* Kept for backward compatibility with existing hx-post URLs.
*/ */
require_once __DIR__ . '/fragments/tag-search.php'; $_POST['type'] = 'tag';
$_POST['q'] = $_POST['tag_search_q'] ?? '';
$_POST['exclude'] = $_POST['tag'] ?? [];
require_once __DIR__ . '/pill-search-fragment.php';

View File

@@ -50,8 +50,8 @@ class MediaController
exit; exit;
} }
// 3. Visibility gate for thesis files // 3. Visibility gate for thesis files (both legacy theses/ and new documents/ paths)
if (preg_match('#^theses/#', $requestedPath)) { if (preg_match('#^(theses|documents)/#', $requestedPath)) {
require_once APP_ROOT . '/src/Database.php'; require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/ErrorHandler.php'; require_once APP_ROOT . '/src/ErrorHandler.php';
try { try {

View File

@@ -191,7 +191,7 @@ class ThesisCreateController
$tf = $this->buildThesisFolder($data['annee'], $allAuthorsStr, $data['titre']); $tf = $this->buildThesisFolder($data['annee'], $allAuthorsStr, $data['titre']);
$folderName = $this->ensureUniqueFolder($tf['folderPath']); $folderName = $this->ensureUniqueFolder($tf['folderPath']);
// Rebuild path with potentially modified folder name // Rebuild path with potentially modified folder name
$folderPath = 'theses/' . $data['annee'] . '/' . $folderName . '/'; $folderPath = 'documents/' . $data['annee'] . '/' . $folderName . '/';
$filePrefix = $folderName; $filePrefix = $folderName;
if (!empty($post['filepond_mode'])) { if (!empty($post['filepond_mode'])) {

View File

@@ -322,11 +322,11 @@ class ThesisEditController
$existingFiles = $this->db->getThesisFiles($thesisId); $existingFiles = $this->db->getThesisFiles($thesisId);
foreach ($existingFiles as $f) { foreach ($existingFiles as $f) {
$fp = $f['file_path'] ?? ''; $fp = $f['file_path'] ?? '';
if (str_starts_with($fp, 'theses/')) { if (str_starts_with($fp, 'documents/') || str_starts_with($fp, 'theses/')) {
$parts = explode('/', $fp); $parts = explode('/', $fp);
if (count($parts) >= 3) { if (count($parts) >= 3) {
$folderName = $parts[2]; $folderName = $parts[2];
$folderPath = 'theses/' . $year . '/' . $folderName . '/'; $folderPath = 'documents/' . $year . '/' . $folderName . '/';
$filePrefix = $folderName; $filePrefix = $folderName;
break; break;
} }
@@ -341,17 +341,15 @@ class ThesisEditController
// ── Cover image (outside transaction — filesystem op) ───────────────── // ── Cover image (outside transaction — filesystem op) ─────────────────
if (!empty($post['filepond_mode'])) { if (!empty($post['filepond_mode'])) {
// Async path: cover file_id arrives in post, not $_FILES // Delete old cover only if a genuinely new cover was uploaded (hex file_id).
if (!empty($post['remove_cover'])) { // Existing cover preserved in FilePond sends its DB integer ID — skip.
$coverIdRaw = ($post['queue_file']['cover'] ?? null);
$coverId = is_array($coverIdRaw) ? ($coverIdRaw[0] ?? null) : $coverIdRaw;
$isNewCover = $coverId !== null && $coverId !== '' && preg_match('/^[a-f0-9]{32}$/', (string)$coverId);
if ($isNewCover) {
foreach ($existingFiles as $f) { foreach ($existingFiles as $f) {
if ($f['file_type'] === 'cover') { if ($f['file_type'] === 'cover') {
$this->db->deleteThesisFile((int)$f['id'], $thesisId); $this->deleteThesisFileToTrash((int)$f['id'], $thesisId);
if (!empty($f['file_path']) && defined('STORAGE_ROOT')) {
$abs = STORAGE_ROOT . '/' . $f['file_path'];
if (file_exists($abs)) {
@unlink($abs);
}
}
break; break;
} }
} }
@@ -360,13 +358,7 @@ class ThesisEditController
} elseif (!empty($post['remove_cover'])) { } elseif (!empty($post['remove_cover'])) {
foreach ($existingFiles as $f) { foreach ($existingFiles as $f) {
if ($f['file_type'] === 'cover') { if ($f['file_type'] === 'cover') {
$this->db->deleteThesisFile((int)$f['id'], $thesisId); $this->deleteThesisFileToTrash((int)$f['id'], $thesisId);
if (!empty($f['file_path']) && defined('STORAGE_ROOT')) {
$abs = STORAGE_ROOT . '/' . $f['file_path'];
if (file_exists($abs)) {
@unlink($abs);
}
}
break; break;
} }
} }
@@ -376,19 +368,16 @@ class ThesisEditController
// ── Note d'intention (replace if uploaded) ──────────────────────────── // ── Note d'intention (replace if uploaded) ────────────────────────────
if (!empty($post['filepond_mode'])) { if (!empty($post['filepond_mode'])) {
// Remove old note_intention if new one is uploaded via async path // Only delete + replace if a genuinely new file was uploaded (hex file_id).
$newNoteId = ($post['queue_file']['note_intention'] ?? null); // Existing files preserved in the FilePond pool send their DB integer ID;
$hasNewNote = $newNoteId !== null && (is_array($newNoteId) ? !empty($newNoteId) : $newNoteId !== ''); // we must NOT delete them — they're already stored.
if ($hasNewNote) { $noteIdRaw = ($post['queue_file']['note_intention'] ?? null);
$noteId = is_array($noteIdRaw) ? ($noteIdRaw[0] ?? null) : $noteIdRaw;
$isNewNote = $noteId !== null && $noteId !== '' && preg_match('/^[a-f0-9]{32}$/', (string)$noteId);
if ($isNewNote) {
foreach ($existingFiles as $f) { foreach ($existingFiles as $f) {
if ($f['file_type'] === 'note_intention') { if ($f['file_type'] === 'note_intention') {
$this->db->deleteThesisFile((int)$f['id'], $thesisId); $this->deleteThesisFileToTrash((int)$f['id'], $thesisId);
if (!empty($f['file_path']) && defined('STORAGE_ROOT')) {
$abs = STORAGE_ROOT . '/' . $f['file_path'];
if (file_exists($abs)) {
@unlink($abs);
}
}
break; break;
} }
} }
@@ -399,13 +388,7 @@ class ThesisEditController
if (!empty($files['note_intention']['tmp_name'] ?? null) && ($files['note_intention']['error'] ?? -1) === UPLOAD_ERR_OK) { if (!empty($files['note_intention']['tmp_name'] ?? null) && ($files['note_intention']['error'] ?? -1) === UPLOAD_ERR_OK) {
foreach ($existingFiles as $f) { foreach ($existingFiles as $f) {
if ($f['file_type'] === 'note_intention') { if ($f['file_type'] === 'note_intention') {
$this->db->deleteThesisFile((int)$f['id'], $thesisId); $this->deleteThesisFileToTrash((int)$f['id'], $thesisId);
if (!empty($f['file_path']) && defined('STORAGE_ROOT')) {
$abs = STORAGE_ROOT . '/' . $f['file_path'];
if (file_exists($abs)) {
@unlink($abs);
}
}
break; break;
} }
} }
@@ -421,16 +404,7 @@ class ThesisEditController
if ($fileId <= 0) { if ($fileId <= 0) {
continue; continue;
} }
$filePath = $this->db->deleteThesisFile($fileId, $thesisId); $this->deleteThesisFileToTrash($fileId, $thesisId);
if ($filePath && defined('STORAGE_ROOT')) {
// Skip filesystem deletion for website URLs (not real files)
if (!str_starts_with($filePath, 'http://') && !str_starts_with($filePath, 'https://')) {
$abs = STORAGE_ROOT . '/' . $filePath;
if (file_exists($abs)) {
@unlink($abs);
}
}
}
} }
// ── Reorder existing files ──────────────────────────────────────────── // ── Reorder existing files ────────────────────────────────────────────
@@ -681,7 +655,7 @@ class ThesisEditController
{ {
$websiteUrl = trim($post['website_url'] ?? ''); $websiteUrl = trim($post['website_url'] ?? '');
// Remove existing website rows // Remove existing website rows (website URLs have no disk file)
$existingFiles = $this->db->getThesisFiles($thesisId); $existingFiles = $this->db->getThesisFiles($thesisId);
foreach ($existingFiles as $f) { foreach ($existingFiles as $f) {
if ($f['file_type'] === 'website') { if ($f['file_type'] === 'website') {

View File

@@ -6,7 +6,7 @@
* Shared file-upload logic used by ThesisCreateController and * Shared file-upload logic used by ThesisCreateController and
* ThesisEditController. All on-disk files are stored under: * ThesisEditController. All on-disk files are stored under:
* *
* theses/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/ * documents/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
* *
* Filenames follow: * Filenames follow:
* *
@@ -88,7 +88,7 @@ trait ThesisFileHandler
* *
* @param int $thesisId * @param int $thesisId
* @param array|null $upload Single-file $_FILES entry. * @param array|null $upload Single-file $_FILES entry.
* @param string $folderPath Relative path from STORAGE_ROOT to the thesis folder (e.g. "theses/2025/2025_SMITH_Mon_Titre/"). * @param string $folderPath Relative path from STORAGE_ROOT to the thesis folder (e.g. "documents/2025/2025_SMITH_Mon_Titre/").
* @param string $filePrefix The prefix shared by all files in this folder (e.g. "2025_SMITH_Mon_Titre"). * @param string $filePrefix The prefix shared by all files in this folder (e.g. "2025_SMITH_Mon_Titre").
*/ */
protected function handleCoverUpload(int $thesisId, ?array $upload, string $folderPath, string $filePrefix): void protected function handleCoverUpload(int $thesisId, ?array $upload, string $folderPath, string $filePrefix): void
@@ -802,7 +802,7 @@ trait ThesisFileHandler
/** /**
* Build the folder path and file prefix for a thesis. * Build the folder path and file prefix for a thesis.
* *
* Folder: theses/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/ * Folder: documents/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
* Prefix: {YYYY}_{AUTHORS}_{TITLE_SLUG} * Prefix: {YYYY}_{AUTHORS}_{TITLE_SLUG}
* *
* @return array{folderPath: string, filePrefix: string, folderName: string} * @return array{folderPath: string, filePrefix: string, folderName: string}
@@ -814,7 +814,7 @@ trait ThesisFileHandler
$folderName = $year . '_' . $authorSlug . '_' . $titleSlug; $folderName = $year . '_' . $authorSlug . '_' . $titleSlug;
$filePrefix = $folderName; $filePrefix = $folderName;
$folderPath = 'theses/' . $year . '/' . $folderName . '/'; $folderPath = 'documents/' . $year . '/' . $folderName . '/';
return [ return [
'folderPath' => $folderPath, 'folderPath' => $folderPath,
@@ -1203,4 +1203,55 @@ trait ThesisFileHandler
} }
@rmdir($tmpDir); @rmdir($tmpDir);
} }
/**
* Delete a thesis_files row, moving the on-disk file to the trash directory
* instead of unlinking it. Website URLs and PeerTube references are only
* removed from the database (no disk file).
*/
protected function deleteThesisFileToTrash(int $fileId, int $thesisId): void
{
if ($fileId <= 0) {
return;
}
// Fetch the file row to get the path before deleting the DB record
$existingFiles = $this->db->getThesisFiles($thesisId);
$fileRow = null;
foreach ($existingFiles as $f) {
if ((int)$f['id'] === $fileId) {
$fileRow = $f;
break;
}
}
$filePath = $this->db->deleteThesisFile($fileId, $thesisId);
if ($filePath && defined('STORAGE_ROOT')) {
// Skip filesystem for website URLs and PeerTube IDs (not real files)
if (str_starts_with($filePath, 'http://') || str_starts_with($filePath, 'https://')) {
return;
}
if (str_starts_with($filePath, 'peertube_ids:')) {
return;
}
$abs = STORAGE_ROOT . '/' . $filePath;
if (file_exists($abs)) {
$trashDir = STORAGE_ROOT . '/tmp/_trash';
if (!is_dir($trashDir)) {
mkdir($trashDir, 0755, true);
}
// Keep original filename structure for traceability
$trashName = $fileId . '_' . basename($filePath);
$trashPath = $trashDir . '/' . $trashName;
if (!rename($abs, $trashPath)) {
// Fallback to unlink if rename fails (cross-device)
@copy($abs, $trashPath);
@unlink($abs);
}
error_log("ThesisFileHandler: file \$fileId moved to trash → \$trashName");
}
}
}
} }

View File

@@ -47,6 +47,12 @@ class Database
} catch (\PDOException $e) { } catch (\PDOException $e) {
// Column already exists — ignore // Column already exists — ignore
} }
// Add 'locked_year' column to share_links if missing
try {
$this->pdo->exec("ALTER TABLE share_links ADD COLUMN locked_year INTEGER");
} catch (\PDOException $e) {
// Column already exists — ignore
}
} }
/** /**
@@ -1013,19 +1019,39 @@ class Database
$email = null; $email = null;
} }
$stmt = $this->pdo->prepare('SELECT id FROM authors WHERE name = ?'); $cleanEmail = ($email !== null && $email !== '') ? $email : null;
// Try to find by name first
$stmt = $this->pdo->prepare('SELECT id, email FROM authors WHERE name = ?');
$stmt->execute([$name]); $stmt->execute([$name]);
$author = $stmt->fetch(); $author = $stmt->fetch();
if ($author) { if ($author) {
// Always update email (may be null to clear) and show_contact. // Update email and show_contact unless that email belongs to another author.
if ($cleanEmail !== null) {
$dup = $this->pdo->prepare('SELECT id FROM authors WHERE email = ? AND id != ?');
$dup->execute([$cleanEmail, $author['id']]);
if ($dup->fetch()) {
$cleanEmail = null; // don't steal another author's email
}
}
$updateStmt = $this->pdo->prepare('UPDATE authors SET email = ?, show_contact = ? WHERE id = ?'); $updateStmt = $this->pdo->prepare('UPDATE authors SET email = ?, show_contact = ? WHERE id = ?');
$updateStmt->execute([$email && $email !== '' ? $email : null, $showContact ? 1 : 0, $author['id']]); $updateStmt->execute([$cleanEmail, $showContact ? 1 : 0, $author['id']]);
return $author['id']; return $author['id'];
} }
// If an author with this email already exists (different name), reuse that record.
if ($cleanEmail !== null) {
$byEmail = $this->pdo->prepare('SELECT id FROM authors WHERE email = ?');
$byEmail->execute([$cleanEmail]);
$existing = $byEmail->fetch();
if ($existing) {
return $existing['id'];
}
}
$stmt = $this->pdo->prepare('INSERT INTO authors (name, email, show_contact) VALUES (?, ?, ?)'); $stmt = $this->pdo->prepare('INSERT INTO authors (name, email, show_contact) VALUES (?, ?, ?)');
$stmt->execute([$name, $email, $showContact ? 1 : 0]); $stmt->execute([$name, $cleanEmail, $showContact ? 1 : 0]);
return $this->pdo->lastInsertId(); return $this->pdo->lastInsertId();
} }
@@ -1194,6 +1220,67 @@ class Database
// TAG MANAGEMENT (admin) // TAG MANAGEMENT (admin)
// ======================================================================== // ========================================================================
/**
* Search supervisors by name prefix. Returns up to 10 matching supervisors.
* If $query is empty, returns the most-used ones (up to 10).
*
* @param string $role Optional role filter: 'promoteur_interne' (is_external=0, role=promoteur),
* 'promoteur_externe' (is_external=1, role=promoteur),
* 'lecteur_interne' (is_external=0, role=lecteur),
* 'lecteur_externe' (is_external=1, role=lecteur)
*/
public function searchSupervisors(string $query = '', string $role = ''): array
{
$query = trim($query);
// Map role to WHERE conditions
$roleWhere = '';
$roleParams = [];
switch ($role) {
case 'promoteur_interne':
$roleWhere = 'AND ts.is_external = 0 AND ts.role = \'promoteur\'';
break;
case 'promoteur_externe':
$roleWhere = 'AND ts.is_external = 1 AND ts.role = \'promoteur\'';
break;
case 'lecteur_interne':
$roleWhere = 'AND ts.is_external = 0 AND ts.role = \'lecteur\'';
break;
case 'lecteur_externe':
$roleWhere = 'AND ts.is_external = 1 AND ts.role = \'lecteur\'';
break;
default:
break;
}
if ($query === '') {
$stmt = $this->pdo->query('
SELECT s.id, s.name,
COUNT(DISTINCT ts.thesis_id) as thesis_count
FROM supervisors s
LEFT JOIN thesis_supervisors ts ON s.id = ts.supervisor_id
' . $roleWhere . '
GROUP BY s.id
ORDER BY thesis_count DESC, s.name COLLATE NOCASE
LIMIT 10
');
} else {
$stmt = $this->pdo->prepare('
SELECT s.id, s.name,
COUNT(DISTINCT ts.thesis_id) as thesis_count
FROM supervisors s
LEFT JOIN thesis_supervisors ts ON s.id = ts.supervisor_id
WHERE s.name LIKE ?
' . $roleWhere . '
GROUP BY s.id
ORDER BY s.name = ? DESC, thesis_count DESC, s.name COLLATE NOCASE
LIMIT 10
');
$stmt->execute([$query . '%', $query]);
}
return $stmt->fetchAll();
}
/** /**
* Search tags by name prefix. Returns up to 10 matching tags. * Search tags by name prefix. Returns up to 10 matching tags.
* If $query is empty, returns the most-used tags (up to 10). * If $query is empty, returns the most-used tags (up to 10).
@@ -1794,13 +1881,11 @@ class Database
} }
// 2. Accent-tolerant fallback: strip accents and re-compare. // 2. Accent-tolerant fallback: strip accents and re-compare.
// iconv 'ASCII//TRANSLIT' turns é→e, ç→c, etc. $asciiName = self::stripAccents($name);
$asciiName = @iconv('UTF-8', 'ASCII//TRANSLIT', $name); if ($asciiName !== $name) {
if ($asciiName !== false && $asciiName !== $name) {
$all = $this->pdo->query('SELECT id, name FROM languages WHERE deleted_at IS NULL')->fetchAll(); $all = $this->pdo->query('SELECT id, name FROM languages WHERE deleted_at IS NULL')->fetchAll();
foreach ($all as $row) { foreach ($all as $row) {
$rowAscii = @iconv('UTF-8', 'ASCII//TRANSLIT', strtolower($row['name'])); if (self::stripAccents(strtolower($row['name'])) === $asciiName) {
if ($rowAscii !== false && $rowAscii === $asciiName) {
return (int)$row['id']; return (int)$row['id'];
} }
} }
@@ -1952,7 +2037,7 @@ class Database
} }
/** /**
* Check visibility for a file path under theses/. * Check visibility for a file path under documents/ or theses/.
* Returns the access_type_id of the owning thesis, or null if the file * Returns the access_type_id of the owning thesis, or null if the file
* is not found or the path does not belong to a thesis file. * is not found or the path does not belong to a thesis file.
* *
@@ -3071,4 +3156,50 @@ class Database
return $result ?: null; return $result ?: null;
} }
/**
* Strip accents from a UTF-8 string (é→e, ç→c, etc.).
* Pure PHP fallback when iconv extension is not available.
*/
private static function stripAccents(string $str): string
{
if (function_exists('iconv')) {
$result = @iconv('UTF-8', 'ASCII//TRANSLIT', $str);
if ($result !== false) {
return strtolower($result);
}
}
// Manual transliteration table for common accented chars
static $map = null;
if ($map === null) {
$utf8 = [
'À','Á','Â','Ã','Ä','Å','Æ','à','á','â','ã','ä','å','æ',
'Ç','ç',
'È','É','Ê','Ë','è','é','ê','ë',
'Ì','Í','Î','Ï','ì','í','î','ï',
'Ð','ð',
'Ñ','ñ',
'Ò','Ó','Ô','Õ','Ö','Ø','ò','ó','ô','õ','ö','ø',
'Ù','Ú','Û','Ü','ù','ú','û','ü',
'Ý','ý','ÿ',
'Š','š','Ž','ž','Þ','þ','Œ','œ','ß',
];
$ascii = [
'A','A','A','A','A','A','AE','a','a','a','a','a','a','ae',
'C','c',
'E','E','E','E','e','e','e','e',
'I','I','I','I','i','i','i','i',
'D','d',
'N','n',
'O','O','O','O','O','O','o','o','o','o','o','o',
'U','U','U','U','u','u','u','u',
'Y','y','y',
'S','s','Z','z','TH','th','OE','oe','ss',
];
$map = array_combine($utf8, $ascii);
}
return strtolower(strtr($str, $map));
}
} }

View File

@@ -61,7 +61,7 @@ class ShareLink
* @param string|null $expiresAt ISO-8601 expiration date, null = never expires * @param string|null $expiresAt ISO-8601 expiration date, null = never expires
* @return array|null The created link row with _plain_password attached * @return array|null The created link row with _plain_password attached
*/ */
public function create(int $createdBy, ?string $expiresAt = null, ?string $objetRestriction = null, ?string $name = null): ?array public function create(int $createdBy, ?string $expiresAt = null, ?string $objetRestriction = null, ?string $name = null, ?int $lockedYear = null): ?array
{ {
$slug = self::generateSlug(); $slug = self::generateSlug();
$plainPassword = self::generatePassword(); $plainPassword = self::generatePassword();
@@ -74,11 +74,16 @@ class ShareLink
$objetRestriction = 'tfe'; $objetRestriction = 'tfe';
} }
// Validate locked_year: must be a plausible academic year (2000..current+3)
if ($lockedYear !== null && ($lockedYear < 2000 || $lockedYear > ((int)date('Y') + 3))) {
$lockedYear = null;
}
$stmt = $this->db->getConnection()->prepare( $stmt = $this->db->getConnection()->prepare(
'INSERT INTO share_links (slug, name, objet_restriction, password_hash, encrypted_password, is_active, created_by, expires_at) 'INSERT INTO share_links (slug, name, objet_restriction, password_hash, encrypted_password, is_active, created_by, expires_at, locked_year)
VALUES (?, ?, ?, ?, ?, 1, ?, ?)' VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)'
); );
$stmt->execute([$slug, $name, $objetRestriction, $passwordHash, Crypto::encrypt($plainPassword), $createdBy, $expiresAt]); $stmt->execute([$slug, $name, $objetRestriction, $passwordHash, Crypto::encrypt($plainPassword), $createdBy, $expiresAt, $lockedYear]);
$link = $this->findBySlug($slug); $link = $this->findBySlug($slug);
if ($link) { if ($link) {
@@ -235,7 +240,15 @@ class ShareLink
/** /**
* Update a share link (name, expiration). * Update a share link (name, expiration).
*/ */
public function update(int $id, ?string $name = null, ?string $expiresAt = null): void /**
* Update a share link's name, expiration, and/or locked year.
*
* $lockedYear:
* - null → leave unchanged (not present in POST)
* - "" → clear (set to NULL in DB)
* - non-empty string → parse as int, validate, and set
*/
public function update(int $id, ?string $name = null, ?string $expiresAt = null, mixed $lockedYear = null): void
{ {
$pdo = $this->db->getConnection(); $pdo = $this->db->getConnection();
$fields = []; $fields = [];
@@ -250,6 +263,17 @@ class ShareLink
$fields[] = 'expires_at = ?'; $fields[] = 'expires_at = ?';
$params[] = $expiresAtVal; $params[] = $expiresAtVal;
} }
if ($lockedYear !== null) {
if ($lockedYear === '' || $lockedYear === false) {
$fields[] = 'locked_year = NULL';
} else {
$year = filter_var($lockedYear, FILTER_VALIDATE_INT);
if ($year !== false && $year >= 2000 && $year <= ((int)date('Y') + 3)) {
$fields[] = 'locked_year = ?';
$params[] = $year;
}
}
}
if (!empty($fields)) { if (!empty($fields)) {
$params[] = $id; $params[] = $id;

View File

@@ -0,0 +1,364 @@
{"timestamp":"2026-05-04T15:36:30+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"maintenance_on","status":"success"}
{"timestamp":"2026-05-04T15:53:38+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"maintenance_off","status":"success"}
{"timestamp":"2026-05-04T15:53:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"maintenance_on","status":"success"}
{"timestamp":"2026-05-04T16:11:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"maintenance_off","status":"success"}
{"timestamp":"2026-05-05T09:06:21+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"add","status":"success","context":{"thesis_id":38,"identifier":"2025-013","author":"Lila Dubois, Karim Nassar"}}
{"timestamp":"2026-05-05T09:08:03+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[38]}}
{"timestamp":"2026-05-05T09:19:46+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":38,"title":"Jouer l'espace : dispositifs scénographiques pour l'expérience participative"}}
{"timestamp":"2026-05-05T09:33:13+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"csv_export","status":"success"}
{"timestamp":"2026-05-05T09:33:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1","restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-05T16:40:13+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"delete_all_theses","status":"success","context":{"count":13}}
{"timestamp":"2026-05-05T16:57:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":15,"ids":[53,52,51,50,49,48,47,46,45,44,43,42,41,40,39]}}
{"timestamp":"2026-05-05T16:58:02+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":25,"ids":[178,177,176,175,174,173,172,171,170,169,168,167,166,165,164,163,162,161,160,159,158,157,156,155,154]}}
{"timestamp":"2026-05-07T16:15:27+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"files_export","status":"success","context":{"file_count":0,"byte_size":248}}
{"timestamp":"2026-05-07T16:56:50+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"apropos","action":"edit","status":"success","context":{"key":"credits"}}
{"timestamp":"2026-05-07T18:06:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"page","action":"edit","status":"success","context":{"slug":"about"}}
{"timestamp":"2026-05-08T08:55:54+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":179,"title":"Systèmes d'identité visuelle générative : entre contrainte algorithmique et expression singulière"}}
{"timestamp":"2026-05-08T08:59:47+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":179,"title":"Systèmes d'identité visuelle générative : entre contrainte algorithmique et expression singulière"}}
{"timestamp":"2026-05-08T09:02:15+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"delete_all_theses","status":"success","context":{"count":141}}
{"timestamp":"2026-05-08T17:48:45+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"partage_intro"}}
{"timestamp":"2026-05-08T17:49:10+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"partage_intro"}}
{"timestamp":"2026-05-08T17:52:04+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"partage_intro"}}
{"timestamp":"2026-05-08T18:30:36+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_languages"}}
{"timestamp":"2026-05-08T18:30:49+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_keywords"}}
{"timestamp":"2026-05-08T18:31:09+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_keywords"}}
{"timestamp":"2026-05-08T18:40:00+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}}
{"timestamp":"2026-05-08T19:22:22+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}}
{"timestamp":"2026-05-08T20:33:47+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}}
{"timestamp":"2026-05-08T20:35:30+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}}
{"timestamp":"2026-05-08T20:35:35+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}}
{"timestamp":"2026-05-08T20:37:31+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}}
{"timestamp":"2026-05-08T20:44:48+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"page","action":"edit","status":"success","context":{"slug":"about"}}
{"timestamp":"2026-05-09T14:12:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260509-2VBLLICG","has_password":false,"expires_at":"2026-05-09 16:30:00","objet_restriction":"tfe"}}
{"timestamp":"2026-05-09T16:53:01+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"archive","status":"success","context":{"link_id":108}}
{"timestamp":"2026-05-09T16:53:05+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"archive","status":"success","context":{"link_id":1}}
{"timestamp":"2026-05-09T16:53:10+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260509-6UWYB2ZJ","has_password":false,"expires_at":null,"objet_restriction":"tfe"}}
{"timestamp":"2026-05-09T17:04:39+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[369]}}
{"timestamp":"2026-05-09T17:35:26+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"unpublish","status":"success","context":{"count":1,"ids":[369]}}
{"timestamp":"2026-05-09T18:09:32+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[369]}}
{"timestamp":"2026-05-09T18:28:04+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"deactivate","status":"success","context":{"link_id":137}}
{"timestamp":"2026-05-09T18:28:06+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"activate","status":"success","context":{"link_id":137}}
{"timestamp":"2026-05-09T18:42:48+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[369]}}
{"timestamp":"2026-05-09T18:43:05+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"unpublish","status":"success","context":{"count":1,"ids":[190]}}
{"timestamp":"2026-05-09T18:43:09+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[190]}}
{"timestamp":"2026-05-09T18:43:11+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"unpublish","status":"success","context":{"count":1,"ids":[190]}}
{"timestamp":"2026-05-09T18:43:33+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":11,"ids":[190,189,188,187,186,185,184,183,182,181,180]}}
{"timestamp":"2026-05-10T00:34:35+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":511,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-10T00:34:47+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":511,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-10T00:35:59+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":511,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-10T00:44:19+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":511,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-10T00:44:38+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":511,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-10T00:44:59+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":505,"title":"Notes on a truthful & engaged cinema"}}
{"timestamp":"2026-05-10T01:33:38+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":511,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-10T08:17:26+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":142,"ids":[511,510,509,508,507,506,505,504,503,502,501,500,499,498,497,496,495,494,493,492,491,490,489,488,487,486,485,484,483,482,481,480,479,478,477,476,475,474,473,472,471,470,469,468,467,466,465,464,463,462,461,460,459,458,457,456,455,454,453,452,451,450,449,448,447,446,445,444,443,442,441,440,439,438,437,436,435,434,433,432,431,430,429,428,427,426,425,424,423,422,421,420,419,418,417,416,415,414,413,412,411,410,409,408,407,406,405,404,403,402,401,400,399,398,397,396,395,394,393,392,391,390,389,388,387,386,385,384,383,382,381,380,379,378,377,376,375,374,373,372,371,370]}}
{"timestamp":"2026-05-10T08:20:25+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":665,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-10T08:20:49+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":142,"ids":[665,664,663,662,661,660,659,658,657,656,655,654,653,652,651,650,649,648,647,646,645,644,643,642,641,640,639,638,637,636,635,634,633,632,631,630,629,628,627,626,625,624,623,622,621,620,619,618,617,616,615,614,613,612,611,610,609,608,607,606,605,604,603,602,601,600,599,598,597,596,595,594,593,592,591,590,589,588,587,586,585,584,583,582,581,580,579,578,577,576,575,574,573,572,571,570,569,568,567,566,565,564,563,562,561,560,559,558,557,556,555,554,553,552,551,550,549,548,547,546,545,544,543,542,541,540,539,538,537,536,535,534,533,532,531,530,529,528,527,526,525,524]}}
{"timestamp":"2026-05-10T08:44:11+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":142,"ids":[665,664,663,662,661,660,659,658,657,656,655,654,653,652,651,650,649,648,647,646,645,644,643,642,641,640,639,638,637,636,635,634,633,632,631,630,629,628,627,626,625,624,623,622,621,620,619,618,617,616,615,614,613,612,611,610,609,608,607,606,605,604,603,602,601,600,599,598,597,596,595,594,593,592,591,590,589,588,587,586,585,584,583,582,581,580,579,578,577,576,575,574,573,572,571,570,569,568,567,566,565,564,563,562,561,560,559,558,557,556,555,554,553,552,551,550,549,548,547,546,545,544,543,542,541,540,539,538,537,536,535,534,533,532,531,530,529,528,527,526,525,524]}}
{"timestamp":"2026-05-10T09:22:51+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":142,"ids":[807,806,805,804,803,802,801,800,799,798,797,796,795,794,793,792,791,790,789,788,787,786,785,784,783,782,781,780,779,778,777,776,775,774,773,772,771,770,769,768,767,766,765,764,763,762,761,760,759,758,757,756,755,754,753,752,751,750,749,748,747,746,745,744,743,742,741,740,739,738,737,736,735,734,733,732,731,730,729,728,727,726,725,724,723,722,721,720,719,718,717,716,715,714,713,712,711,710,709,708,707,706,705,704,703,702,701,700,699,698,697,696,695,694,693,692,691,690,689,688,687,686,685,684,683,682,681,680,679,678,677,676,675,674,673,672,671,670,669,668,667,666]}}
{"timestamp":"2026-05-10T09:58:02+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"tag","action":"rename","status":"success","context":{"tag_id":351,"new_name":"absurdité"}}
{"timestamp":"2026-05-10T13:03:12+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":90,"ids":[963,900,899,898,897,896,895,894,893,892,891,890,889,888,887,886,885,884,883,882,881,880,879,878,877,876,875,874,873,872,871,870,869,868,867,866,865,864,863,862,861,860,859,858,857,856,855,854,853,852,851,850,849,848,847,846,845,844,843,842,841,840,839,838,837,836,835,834,833,832,831,830,829,828,827,826,825,824,823,822,821,820,819,818,817,816,815,814,813,812]}}
{"timestamp":"2026-05-10T17:59:52+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[966]}}
{"timestamp":"2026-05-10T19:53:00+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[975]}}
{"timestamp":"2026-05-10T19:58:40+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[976]}}
{"timestamp":"2026-05-10T20:04:53+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":976,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-10T20:05:08+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[976]}}
{"timestamp":"2026-05-10T20:05:40+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":976,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-10T20:05:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[976]}}
{"timestamp":"2026-05-10T20:06:25+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":976,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-10T20:06:34+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":976,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-10T20:07:06+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":976,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-10T20:18:25+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":976,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-10T20:18:33+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":976,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-10T20:20:46+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":976,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-10T20:37:27+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":3,"ids":[976,975,966]}}
{"timestamp":"2026-05-10T20:51:17+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[1118]}}
{"timestamp":"2026-05-10T21:08:09+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[1119]}}
{"timestamp":"2026-05-10T21:40:54+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"0","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T22:41:50+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"0","restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T22:58:05+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"0","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T22:58:06+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T22:58:07+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"0","access_type_interdit_enabled":"0","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T22:58:19+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"0","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T22:58:22+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T22:58:39+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"0","restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T23:41:08+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1"}}}
{"timestamp":"2026-05-10T23:41:09+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_interne_enabled":"1"}}}
{"timestamp":"2026-05-10T23:41:09+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-10T23:46:32+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"0","restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T23:46:40+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"0","restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T23:46:49+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"0","restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T23:46:59+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"0","restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T23:47:00+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T23:47:00+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T23:47:01+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T23:47:12+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T23:47:29+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"0","restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T23:52:16+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"0","restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T23:52:21+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T23:52:42+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"objet_types_update","status":"success","context":{"values":{"objet_these_enabled":"0","objet_frart_enabled":"1"}}}
{"timestamp":"2026-05-10T23:52:47+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"objet_types_update","status":"success","context":{"values":{"objet_these_enabled":"0","objet_frart_enabled":"0"}}}
{"timestamp":"2026-05-10T23:52:50+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T23:53:41+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1","restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T23:53:56+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"0","restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T23:54:22+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-10T23:54:33+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-10T23:54:37+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-10T23:59:02+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T23:59:07+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-10T23:59:10+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-10T23:59:12+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-10T23:59:13+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-10T23:59:17+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-10T23:59:20+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-10T23:59:25+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"objet_types_update","status":"success","context":{"values":{"objet_these_enabled":"1","objet_frart_enabled":"1"}}}
{"timestamp":"2026-05-10T23:59:28+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"objet_types_update","status":"success","context":{"values":{"objet_these_enabled":"1","objet_frart_enabled":"1"}}}
{"timestamp":"2026-05-11T00:02:04+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T00:02:10+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T00:02:15+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T00:02:18+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T00:02:21+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T00:02:24+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T00:08:01+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T00:19:35+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T00:19:38+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:10:41+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:10:47+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:10:53+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:10:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"objet_types_update","status":"success","context":{"values":{"objet_these_enabled":"0","objet_frart_enabled":"1"}}}
{"timestamp":"2026-05-11T01:10:58+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"objet_types_update","status":"success","context":{"values":{"objet_these_enabled":"0","objet_frart_enabled":"0"}}}
{"timestamp":"2026-05-11T01:11:10+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:11:21+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:12:10+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:12:16+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:12:20+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:12:23+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:15:41+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:24:21+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:24:26+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:25:22+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:25:25+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:25:36+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:25:38+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:25:54+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:36:14+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:36:33+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:37:26+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:37:30+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:39:30+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:39:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:44:05+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-11T01:44:09+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:44:13+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-11T01:44:17+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:44:20+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:44:45+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T01:44:50+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-11T01:44:59+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:45:06+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T01:45:14+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"objet_types_update","status":"success","context":{"values":{"objet_these_enabled":"1","objet_frart_enabled":"0"}}}
{"timestamp":"2026-05-11T01:45:30+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"objet_types_update","status":"success","context":{"values":{"objet_these_enabled":"0","objet_frart_enabled":"0"}}}
{"timestamp":"2026-05-11T08:31:45+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"peertube_update","status":"success","context":{"enabled":true}}
{"timestamp":"2026-05-11T09:19:45+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"peertube_update","status":"success","context":{"enabled":true}}
{"timestamp":"2026-05-11T09:20:03+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"peertube_update","status":"success","context":{"enabled":true}}
{"timestamp":"2026-05-11T09:21:22+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T09:23:16+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"peertube_update","status":"success","context":{"enabled":true}}
{"timestamp":"2026-05-11T09:23:30+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"peertube_update","status":"success","context":{"enabled":true}}
{"timestamp":"2026-05-11T09:31:16+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T09:36:38+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T09:37:05+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T09:37:39+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T09:37:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-11T09:37:46+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-11T09:56:47+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"peertube_update","status":"success","context":{"enabled":true}}
{"timestamp":"2026-05-11T09:57:04+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"smtp_test","status":"success","context":{"to":"theophile.gervreau-mercier@erg.school","error":null}}
{"timestamp":"2026-05-11T09:59:05+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T10:17:46+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T10:31:36+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"1","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T10:31:37+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"1","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-11T10:31:46+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T10:37:48+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T10:40:59+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T10:47:51+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T10:55:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T11:03:58+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T11:05:36+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T11:18:46+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T11:26:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T11:52:41+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T12:36:25+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T12:58:48+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T13:00:02+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T13:00:15+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T13:01:19+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T13:02:52+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1119,"title":"Generative Poetics: Algorithms as Artistic Agents"}}
{"timestamp":"2026-05-11T13:05:28+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"csv_export","status":"success"}
{"timestamp":"2026-05-11T13:05:28+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"files_export","status":"success","context":{"file_count":4,"byte_size":3402264}}
{"timestamp":"2026-05-11T13:05:39+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"files_export","status":"success","context":{"file_count":4,"byte_size":3402264}}
{"timestamp":"2026-05-11T14:40:46+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1119]}}
{"timestamp":"2026-05-11T14:40:48+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"unpublish","status":"success","context":{"count":1,"ids":[1118]}}
{"timestamp":"2026-05-11T14:40:58+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1119]}}
{"timestamp":"2026-05-11T14:40:58+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1120]}}
{"timestamp":"2026-05-11T14:41:20+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"1"}}}
{"timestamp":"2026-05-11T17:37:54+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"access_type_libre_enabled":"0","access_type_interne_enabled":"0","access_type_interdit_enabled":"1"}}}
{"timestamp":"2026-05-12T10:45:39+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":144,"ids":[1122,1121,1118,1117,1116,1115,1114,1113,1112,1111,1110,1109,1108,1107,1106,1105,1104,1103,1102,1101,1100,1099,1098,1097,1096,1095,1094,1093,1092,1091,1090,1089,1088,1087,1086,1085,1084,1083,1082,1081,1080,1079,1078,1077,1076,1075,1074,1073,1072,1071,1070,1069,1068,1067,1066,1065,1064,1063,1062,1061,1060,1059,1058,1057,1056,1055,1054,1053,1052,1051,1050,1049,1048,1047,1046,1045,1044,1043,1042,1041,1040,1039,1038,1037,1036,1035,1034,1033,1032,1031,1030,1029,1028,1027,1026,1025,1024,1023,1022,1021,1020,1019,1018,1017,1016,1015,1014,1013,1012,1011,1010,1009,1008,1007,1006,1005,1004,1003,1002,1001,1000,999,998,997,996,995,994,993,992,991,990,989,988,987,986,985,984,983,982,981,980,979,978,977]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[977]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[978]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[979]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[980]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[981]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[982]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[983]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[984]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[985]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[986]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[987]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[988]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[989]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[990]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[991]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[992]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[993]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[994]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[995]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[996]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[997]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[998]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[999]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1000]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1001]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1002]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1003]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1004]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1005]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1006]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1007]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1008]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1009]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1010]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1011]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1012]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1013]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1014]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1015]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1016]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1017]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1018]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1019]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1020]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1021]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1022]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1023]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1024]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1025]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1026]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1027]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1028]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1029]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1030]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1031]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1032]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1033]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1034]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1035]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1036]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1037]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1038]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1039]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1040]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1041]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1042]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1043]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1044]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1045]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1046]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1047]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1048]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1049]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1050]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1051]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1052]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1053]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1054]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1055]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1056]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1057]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1058]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1059]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1060]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1061]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1062]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1063]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1064]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1065]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1066]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1067]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1068]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1069]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1070]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1071]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1072]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1073]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1074]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1075]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1076]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1077]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1078]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1079]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1080]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1081]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1082]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1083]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1084]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1085]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1086]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1087]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1088]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1089]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1090]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1091]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1092]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1093]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1094]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1095]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1096]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1097]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1098]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1099]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1100]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1101]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1102]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1103]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1104]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1105]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1106]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1107]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1108]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1109]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1110]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1111]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1112]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1113]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1114]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1115]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1116]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1117]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1118]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1121]}}
{"timestamp":"2026-05-12T10:45:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"delete","status":"success","context":{"delete_all":false,"count":1,"ids":[1122]}}
{"timestamp":"2026-05-12T10:48:25+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1263,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-12T11:03:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1263,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-12T11:04:01+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1263,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-12T11:05:06+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}}
{"timestamp":"2026-05-12T11:17:52+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1263,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}
{"timestamp":"2026-05-12T11:18:37+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[1263]}}
{"timestamp":"2026-05-12T11:58:23+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"archive","status":"success","context":{"link_id":137}}
{"timestamp":"2026-05-12T11:58:38+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260512-LHEPMFNV","has_password":true,"expires_at":null,"objet_restriction":"tfe"}}
{"timestamp":"2026-05-12T11:59:32+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260512-F4DLHEFN","has_password":true,"expires_at":null,"objet_restriction":"tfe"}}
{"timestamp":"2026-05-12T12:07:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260512-RJJ3JYYS","has_password":true,"expires_at":null,"objet_restriction":"tfe"}}
{"timestamp":"2026-05-12T12:08:08+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"set_password","status":"success","context":{"link_id":210,"removed":false}}
{"timestamp":"2026-05-12T12:10:34+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"set_password","status":"success","context":{"link_id":210,"removed":false}}
{"timestamp":"2026-05-12T12:22:30+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"archive","status":"success","context":{"link_id":210}}
{"timestamp":"2026-05-12T12:22:32+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"archive","status":"success","context":{"link_id":209}}
{"timestamp":"2026-05-12T12:22:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"archive","status":"success","context":{"link_id":208}}
{"timestamp":"2026-05-12T12:23:22+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260512-CL5LC53W","has_password":true,"expires_at":null,"objet_restriction":"tfe"}}
{"timestamp":"2026-05-18T15:20:07+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":1263,"title":"Pourquoi les artistes sont-ils encore sur Instagram alors que jai vu une story disant quil fallait quitter META"}}

View File

@@ -52,3 +52,5 @@
{"source":"admin","action":"submit","status":"error","error":"Le champ 'Auteur·ice(s)' est requis.","author":"","post_keys":["progress_token","csrf_token","titre","subtitle","auteurice","mail","synopsis","language_search_q","tag_search_q","année","orientation","ap","finality","jury_promoteur","jury_promoteur_ulb_name","jury_lecteur_interne","jury_lecteur_externe","admin_mode","edit_mode","has_annexes","website_url","access_type_id","context_note","jury_points","remarks","lien","contact_interne","go"],"timestamp":"2026-05-11T15:09:54+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} {"source":"admin","action":"submit","status":"error","error":"Le champ 'Auteur·ice(s)' est requis.","author":"","post_keys":["progress_token","csrf_token","titre","subtitle","auteurice","mail","synopsis","language_search_q","tag_search_q","année","orientation","ap","finality","jury_promoteur","jury_promoteur_ulb_name","jury_lecteur_interne","jury_lecteur_externe","admin_mode","edit_mode","has_annexes","website_url","access_type_id","context_note","jury_points","remarks","lien","contact_interne","go"],"timestamp":"2026-05-11T15:09:54+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
{"source":"admin","action":"submit","status":"error","error":"Année invalide. Veuillez entrer une année valide.","author":"dqlskjflkjdsqf","post_keys":["progress_token","csrf_token","titre","subtitle","auteurice","mail","synopsis","language_search_q","tag_search_q","année","orientation","ap","finality","jury_promoteur","jury_promoteur_ulb_name","jury_lecteur_interne","jury_lecteur_externe","admin_mode","edit_mode","has_annexes","website_url","access_type_id","context_note","jury_points","remarks","lien","contact_interne","go"],"timestamp":"2026-05-11T15:10:00+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} {"source":"admin","action":"submit","status":"error","error":"Année invalide. Veuillez entrer une année valide.","author":"dqlskjflkjdsqf","post_keys":["progress_token","csrf_token","titre","subtitle","auteurice","mail","synopsis","language_search_q","tag_search_q","année","orientation","ap","finality","jury_promoteur","jury_promoteur_ulb_name","jury_lecteur_interne","jury_lecteur_externe","admin_mode","edit_mode","has_annexes","website_url","access_type_id","context_note","jury_points","remarks","lien","contact_interne","go"],"timestamp":"2026-05-11T15:10:00+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
{"source":"partage","action":"submit","status":"success","thesis_id":1122,"identifier":"2025-074","author":"Théo Marchand","share_slug":"20260509-6UWYB2ZJ","timestamp":"2026-05-12T10:30:46+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} {"source":"partage","action":"submit","status":"success","thesis_id":1122,"identifier":"2025-074","author":"Théo Marchand","share_slug":"20260509-6UWYB2ZJ","timestamp":"2026-05-12T10:30:46+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
{"source":"partage","action":"submit","status":"error","error":"SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: authors.email","share_slug":"20260512-HRO5QB4O","author":"Lila Dubois, Karim Nassar","post_keys":["filepond_mode","share_link_token","objet","titre","subtitle","auteurice","mail","synopsis","languages","language_autre","language_search_q","tag","tag_search_q","année","orientation","ap","finality","jury_promoteur","jury_promoteur_ulb_name","jury_lecteur_interne","jury_lecteur_externe","admin_mode","edit_mode","formats","queue_file","has_annexes","website_url","access_type_id","confirmation_email","go"],"timestamp":"2026-05-13T13:41:58+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
{"source":"partage","action":"submit","status":"error","error":"SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: authors.email","share_slug":"20260512-HRO5QB4O","author":"Lila Dubois, Karim Nassar","post_keys":["filepond_mode","share_link_token","objet","titre","subtitle","auteurice","mail","synopsis","languages","language_search_q","tag","tag_search_q","année","orientation","ap","finality","jury_promoteur","jury_promoteur_ulb_name","jury_lecteur_interne","jury_lecteur_externe","admin_mode","edit_mode","formats","queue_file","has_annexes","website_url","access_type_id","confirmation_email","go"],"timestamp":"2026-05-13T13:43:56+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}

View File

@@ -206,7 +206,8 @@ CREATE TABLE IF NOT EXISTS share_links (
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME, expires_at DATETIME,
is_archived INTEGER NOT NULL DEFAULT 0, is_archived INTEGER NOT NULL DEFAULT 0,
name TEXT name TEXT,
locked_year INTEGER
); );
CREATE TABLE IF NOT EXISTS smtp_settings ( CREATE TABLE IF NOT EXISTS smtp_settings (

File diff suppressed because it is too large Load Diff

View File

@@ -84,6 +84,14 @@ $websiteLabel = htmlspecialchars($_POST['website_label'] ?? '');
data-existing-files='<?= htmlspecialchars(json_encode($existingFilesJsonForCover ?? []), ENT_QUOTES) ?>'> data-existing-files='<?= htmlspecialchars(json_encode($existingFilesJsonForCover ?? []), ENT_QUOTES) ?>'>
<small>JPG, PNG ou WEBP. Format 4:3 recommandé. Max 20 MB.</small> <small>JPG, PNG ou WEBP. Format 4:3 recommandé. Max 20 MB.</small>
</div> </div>
<?php if ($editMode): ?>
<button type="button" class="btn btn--sm btn--ghost file-browser-trigger"
data-queue-type="cover"
data-thesis-id="<?= htmlspecialchars((string)($thesisId ?? $_GET['id'] ?? '')) ?>"
onclick="XamxamOpenFileBrowser(this)">
📂 Relier un fichier existant
</button>
<?php endif; ?>
</div> </div>
<!-- ── 2. Note d'intention ── --> <!-- ── 2. Note d'intention ── -->
@@ -98,6 +106,14 @@ $websiteLabel = htmlspecialchars($_POST['website_label'] ?? '');
<?= !$adminMode ? 'required' : '' ?>> <?= !$adminMode ? 'required' : '' ?>>
<small>PDF uniquement. Max 100 MB. Si votre fichier est trop lourd, compressez-le avec <a href="https://www.bentopdf.com" target="_blank" rel="noopener">bentopdf.com</a>.</small> <small>PDF uniquement. Max 100 MB. Si votre fichier est trop lourd, compressez-le avec <a href="https://www.bentopdf.com" target="_blank" rel="noopener">bentopdf.com</a>.</small>
</div> </div>
<?php if ($editMode): ?>
<button type="button" class="btn btn--sm btn--ghost file-browser-trigger"
data-queue-type="note_intention"
data-thesis-id="<?= htmlspecialchars((string)($thesisId ?? $_GET['id'] ?? '')) ?>"
onclick="XamxamOpenFileBrowser(this)">
📂 Relier un fichier existant
</button>
<?php endif; ?>
</div> </div>
<!-- ── 3. TFE (all files: PDF, images, video, audio, VTT, archives) ── --> <!-- ── 3. TFE (all files: PDF, images, video, audio, VTT, archives) ── -->
@@ -122,6 +138,14 @@ $websiteLabel = htmlspecialchars($_POST['website_label'] ?? '');
<?php endif; ?> <?php endif; ?>
</small> </small>
</div> </div>
<?php if ($editMode): ?>
<button type="button" class="btn btn--sm btn--ghost file-browser-trigger"
data-queue-type="tfe"
data-thesis-id="<?= htmlspecialchars((string)($thesisId ?? $_GET['id'] ?? '')) ?>"
onclick="XamxamOpenFileBrowser(this)">
📂 Relier un fichier existant
</button>
<?php endif; ?>
</div> </div>
<!-- ── 4. Annexes ── --> <!-- ── 4. Annexes ── -->
@@ -138,6 +162,14 @@ $websiteLabel = htmlspecialchars($_POST['website_label'] ?? '');
data-existing-files='<?= htmlspecialchars(json_encode($existingFilesJsonForAnnexe ?? []), ENT_QUOTES) ?>'> data-existing-files='<?= htmlspecialchars(json_encode($existingFilesJsonForAnnexe ?? []), ENT_QUOTES) ?>'>
<small class="admin-file-hint">PDF ou archives ZIP/TAR. Max 500 MB. Glissez pour réordonner.</small> <small class="admin-file-hint">PDF ou archives ZIP/TAR. Max 500 MB. Glissez pour réordonner.</small>
</div> </div>
<?php if ($editMode): ?>
<button type="button" class="btn btn--sm btn--ghost file-browser-trigger"
data-queue-type="annexe"
data-thesis-id="<?= htmlspecialchars((string)($thesisId ?? $_GET['id'] ?? '')) ?>"
onclick="XamxamOpenFileBrowser(this)">
📂 Relier un fichier existant
</button>
<?php endif; ?>
</div> </div>
</div> </div>
@@ -154,3 +186,19 @@ $websiteLabel = htmlspecialchars($_POST['website_label'] ?? '');
</fieldset><!-- /Fichiers --> </fieldset><!-- /Fichiers -->
</div><!-- #format-fichiers-block --> </div><!-- #format-fichiers-block -->
<!-- ═══════════════════ File Browser Modal (edit mode only) ═══════════════════ -->
<?php if ($editMode): ?>
<dialog id="relink-modal" class="relink-modal">
<div class="relink-modal-header">
<h3>Relier un fichier existant</h3>
<button type="button" class="btn btn--sm btn--ghost" onclick="document.getElementById('relink-modal').close()" aria-label="Fermer">✕</button>
</div>
<div id="relink-modal-body">
<p class="file-browser-loading">Chargement du navigateur de fichiers…</p>
</div>
<div class="relink-modal-footer">
<small>Seuls les fichiers déjà présents dans storage/documents/ ou storage/theses/ sont listés.</small>
</div>
</dialog>
<?php endif; ?>

View File

@@ -233,7 +233,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
$hint = "Si votre TFE contient une langue absente de la liste, précisez-la ici."; $hint = "Si votre TFE contient une langue absente de la liste, précisez-la ici.";
$selectedLanguages = $_selectedOtherLangs; $selectedLanguages = $_selectedOtherLangs;
$required = $_langAutreRequired && !$adminMode; $required = $_langAutreRequired && !$adminMode;
$hxPost = ($mode === 'partage') ? "/partage/fragments/language-search.php" : "/admin/fragments/language-search.php"; $hxPost = ($mode === 'partage') ? "/partage/fragments/pill-search.php" : "/admin/fragments/pill-search.php";
include APP_ROOT . "/templates/partials/form/language-search.php"; include APP_ROOT . "/templates/partials/form/language-search.php";
unset($_langAutreRequired, $_selectedOtherLangs, $_langRaw, $_l, $name, $label, $placeholder, $hint, $selectedLanguages, $required, $hxPost); unset($_langAutreRequired, $_selectedOtherLangs, $_langRaw, $_l, $name, $label, $placeholder, $hint, $selectedLanguages, $required, $hxPost);
?> ?>
@@ -279,7 +279,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
$selectedTags = $_selectedTags; $selectedTags = $_selectedTags;
$required = !$adminMode; $required = !$adminMode;
$minTags = ($mode === 'partage') ? 3 : 0; $minTags = ($mode === 'partage') ? 3 : 0;
$hxPost = ($mode === 'partage') ? "/partage/fragments/tag-search.php" : "/admin/fragments/tag-search.php"; $hxPost = ($mode === 'partage') ? "/partage/fragments/pill-search.php" : "/admin/fragments/pill-search.php";
include APP_ROOT . "/templates/partials/form/tag-search.php"; include APP_ROOT . "/templates/partials/form/tag-search.php";
unset($_tagsRaw, $_selectedTags, $_t, $name, $label, $placeholder, $hint, $selectedTags, $hxPost, $minTags, $required); unset($_tagsRaw, $_selectedTags, $_t, $name, $label, $placeholder, $hint, $selectedTags, $hxPost, $minTags, $required);
?> ?>

View File

@@ -67,13 +67,13 @@ if ($addMode) {
<legend>Composition du jury</legend> <legend>Composition du jury</legend>
<!-- Promoteur·ice(s) interne --> <!-- Promoteur·ice(s) interne -->
<fieldset class="admin-jury-lecteurs"> <fieldset class="admin-jury-lecteurs" data-jury-autocomplete data-jury-hx-post="<?= $adminMode ? '/admin/fragments/pill-search.php' : '/partage/fragments/pill-search.php' ?>" data-jury-role="promoteur_interne">
<legend>Promoteur·ice(s) interne<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></legend> <legend>Promoteur·ice(s) interne<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></legend>
<div id="jury-promoteur-interne-list" class="admin-jury-list"> <div id="jury-promoteur-interne-list" class="admin-jury-list">
<?php if (empty($juryPromoteurs) && $juryPromoteur === null): ?> <?php if (empty($juryPromoteurs) && $juryPromoteur === null): ?>
<div class="admin-jury-entry"> <div class="admin-jury-entry">
<input type="text" name="jury_promoteur[]" placeholder="Nom" <?= $adminMode ? '' : 'required' ?> <input type="text" name="jury_promoteur[]" placeholder="Nom" <?= $adminMode ? '' : 'required' ?>
id="jury_promoteur" aria-label="Promoteur·ice interne 1 — nom"> id="jury_promoteur" aria-label="Promoteur·ice interne 1 — nom" autocomplete="off">
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" <button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button> onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
</div> </div>
@@ -83,7 +83,7 @@ if ($addMode) {
<input type="text" name="jury_promoteur[]" <input type="text" name="jury_promoteur[]"
value="<?= htmlspecialchars($pm['name']) ?>" placeholder="Nom" value="<?= htmlspecialchars($pm['name']) ?>" placeholder="Nom"
<?= (!$adminMode && $pi === 0) ? 'required' : '' ?> <?= (!$adminMode && $pi === 0) ? 'required' : '' ?>
aria-label="Promoteur·ice interne <?= $pi + 1 ?> — nom"> aria-label="Promoteur·ice interne <?= $pi + 1 ?> — nom" autocomplete="off">
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" <button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button> onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
</div> </div>
@@ -92,7 +92,7 @@ if ($addMode) {
<div class="admin-jury-entry"> <div class="admin-jury-entry">
<input type="text" name="jury_promoteur[]" <input type="text" name="jury_promoteur[]"
value="<?= htmlspecialchars($juryPromoteur ?? '') ?>" placeholder="Nom" <?= $adminMode ? '' : 'required' ?> value="<?= htmlspecialchars($juryPromoteur ?? '') ?>" placeholder="Nom" <?= $adminMode ? '' : 'required' ?>
aria-label="Promoteur·ice interne 1 — nom"> aria-label="Promoteur·ice interne 1 — nom" autocomplete="off">
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" <button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button> onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
</div> </div>
@@ -106,13 +106,13 @@ if ($addMode) {
<?php if ($showPromoteurUlb): ?> <?php if ($showPromoteurUlb): ?>
<!-- Promoteur·ice(s) ULB --> <!-- Promoteur·ice(s) ULB -->
<fieldset class="admin-jury-lecteurs" id="jury-promoteur-ulb-row"<?= $promoteurUlbConditional ? ' style="display:none"' : '' ?>> <fieldset class="admin-jury-lecteurs" id="jury-promoteur-ulb-row"<?= $promoteurUlbConditional ? ' style="display:none"' : '' ?> data-jury-autocomplete data-jury-hx-post="<?= $adminMode ? '/admin/fragments/pill-search.php' : '/partage/fragments/pill-search.php' ?>" data-jury-role="promoteur_externe">
<legend>Promoteur·ice(s) ULB<span id="jury-ulb-asterisk" style="display:none"> <span class="asterisk">*</span></span></legend> <legend>Promoteur·ice(s) ULB<span id="jury-ulb-asterisk" style="display:none"> <span class="asterisk">*</span></span></legend>
<div id="jury-promoteur-ulb-list" class="admin-jury-list"> <div id="jury-promoteur-ulb-list" class="admin-jury-list">
<?php if (empty($juryPromoteursUlb) && $juryPromoteurUlb === null): ?> <?php if (empty($juryPromoteursUlb) && $juryPromoteurUlb === null): ?>
<div class="admin-jury-entry"> <div class="admin-jury-entry">
<input type="text" name="jury_promoteur_ulb_name[]" placeholder="Nom" <input type="text" name="jury_promoteur_ulb_name[]" placeholder="Nom"
aria-label="Promoteur·ice ULB 1 — nom"> aria-label="Promoteur·ice ULB 1 — nom" autocomplete="off">
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" <button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button> onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
</div> </div>
@@ -121,7 +121,7 @@ if ($addMode) {
<div class="admin-jury-entry"> <div class="admin-jury-entry">
<input type="text" name="jury_promoteur_ulb_name[]" <input type="text" name="jury_promoteur_ulb_name[]"
value="<?= htmlspecialchars($pm['name']) ?>" placeholder="Nom" value="<?= htmlspecialchars($pm['name']) ?>" placeholder="Nom"
aria-label="Promoteur·ice ULB <?= $pi + 1 ?> — nom"> aria-label="Promoteur·ice ULB <?= $pi + 1 ?> — nom" autocomplete="off">
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" <button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button> onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
</div> </div>
@@ -130,7 +130,7 @@ if ($addMode) {
<div class="admin-jury-entry"> <div class="admin-jury-entry">
<input type="text" name="jury_promoteur_ulb_name[]" <input type="text" name="jury_promoteur_ulb_name[]"
value="<?= htmlspecialchars($juryPromoteurUlb ?? '') ?>" placeholder="Nom" value="<?= htmlspecialchars($juryPromoteurUlb ?? '') ?>" placeholder="Nom"
aria-label="Promoteur·ice ULB 1 — nom"> aria-label="Promoteur·ice ULB 1 — nom" autocomplete="off">
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" <button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button> onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
</div> </div>
@@ -144,23 +144,20 @@ if ($addMode) {
<?php if ($promoteurUlbConditional): ?> <?php if ($promoteurUlbConditional): ?>
<script> <script>
(function() { (function() {
try {
var finalitySelect = document.querySelector('select[name="finality"]'); var finalitySelect = document.querySelector('select[name="finality"]');
var ulbRow = document.getElementById('jury-promoteur-ulb-row'); var ulbRow = document.getElementById('jury-promoteur-ulb-row');
var ulbInput = ulbRow ? ulbRow.querySelector('input') : null; if (!finalitySelect || !ulbRow) return;
var ulbAsterisk = document.getElementById('jury-ulb-asterisk'); var ulbAsterisk = document.getElementById('jury-ulb-asterisk');
function isApprofondiSelected() { function isApprofondiSelected() {
if (!finalitySelect) return false; var opt = finalitySelect.options[finalitySelect.selectedIndex];
var selected = finalitySelect.options[finalitySelect.selectedIndex]; if (!opt) return false;
var text = (selected && selected.text) ? selected.text.toLowerCase() : ''; return (opt.textContent || opt.text || '').toLowerCase().includes('approfondi');
return text.includes('approfondi');
} }
function toggleUlb() { function toggleUlb() {
if (!ulbRow) return;
var show = isApprofondiSelected(); var show = isApprofondiSelected();
ulbRow.style.display = show ? '' : 'none'; ulbRow.style.display = show ? '' : 'none';
if (ulbAsterisk) ulbAsterisk.style.display = show ? '' : 'none'; if (ulbAsterisk) ulbAsterisk.style.display = show ? '' : 'none';
// Mark first ULB input required when finality is Approfondi
if (ulbRow) {
var inputs = ulbRow.querySelectorAll('input[name="jury_promoteur_ulb_name[]"]'); var inputs = ulbRow.querySelectorAll('input[name="jury_promoteur_ulb_name[]"]');
inputs.forEach(function(inp, idx) { inputs.forEach(function(inp, idx) {
inp.required = <?= $adminMode ? 'false' : 'show && idx === 0' ?>; inp.required = <?= $adminMode ? 'false' : 'show && idx === 0' ?>;
@@ -168,24 +165,22 @@ if ($addMode) {
if (!show) inp.value = ''; if (!show) inp.value = '';
}); });
} }
}
if (finalitySelect) {
finalitySelect.addEventListener('change', toggleUlb); finalitySelect.addEventListener('change', toggleUlb);
toggleUlb(); toggleUlb();
} } catch(e) { console.error('jury ULB toggle:', e); }
})(); })();
</script> </script>
<?php endif; ?> <?php endif; ?>
<?php endif; ?> <?php endif; ?>
<!-- Lecteur·ice(s) interne --> <!-- Lecteur·ice(s) interne -->
<fieldset class="admin-jury-lecteurs"> <fieldset class="admin-jury-lecteurs" data-jury-autocomplete data-jury-hx-post="<?= $adminMode ? '/admin/fragments/pill-search.php' : '/partage/fragments/pill-search.php' ?>" data-jury-role="lecteur_interne">
<legend>Lecteur·ice(s) interne<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></legend> <legend>Lecteur·ice(s) interne<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></legend>
<div id="jury-lecteurs-internes-list" class="admin-jury-list"> <div id="jury-lecteurs-internes-list" class="admin-jury-list">
<?php if (empty($lecteursInternes)): ?> <?php if (empty($lecteursInternes)): ?>
<div class="admin-jury-entry"> <div class="admin-jury-entry">
<input type="text" name="jury_lecteur_interne[]" placeholder="Nom" <?= $adminMode ? '' : 'required' ?> <input type="text" name="jury_lecteur_interne[]" placeholder="Nom" <?= $adminMode ? '' : 'required' ?>
aria-label="Lecteur·ice interne 1 — nom"> aria-label="Lecteur·ice interne 1 — nom" autocomplete="off">
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" <button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button> onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
</div> </div>
@@ -195,7 +190,7 @@ if ($addMode) {
<input type="text" name="jury_lecteur_interne[]" <input type="text" name="jury_lecteur_interne[]"
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom" value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
<?= (!$adminMode && $li === 0) ? 'required' : '' ?> <?= (!$adminMode && $li === 0) ? 'required' : '' ?>
aria-label="Lecteur·ice interne <?= $li + 1 ?> — nom"> aria-label="Lecteur·ice interne <?= $li + 1 ?> — nom" autocomplete="off">
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" <button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button> onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
</div> </div>
@@ -209,13 +204,13 @@ if ($addMode) {
</fieldset> </fieldset>
<!-- Lecteur·ice(s) externe --> <!-- Lecteur·ice(s) externe -->
<fieldset class="admin-jury-lecteurs"> <fieldset class="admin-jury-lecteurs" data-jury-autocomplete data-jury-hx-post="<?= $adminMode ? '/admin/fragments/pill-search.php' : '/partage/fragments/pill-search.php' ?>" data-jury-role="lecteur_externe">
<legend>Lecteur·ice(s) externe<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></legend> <legend>Lecteur·ice(s) externe<?= $adminMode ? '' : ' <span class="asterisk">*</span>' ?></legend>
<div id="jury-lecteurs-externes-list" class="admin-jury-list"> <div id="jury-lecteurs-externes-list" class="admin-jury-list">
<?php if (empty($lecteursExternes)): ?> <?php if (empty($lecteursExternes)): ?>
<div class="admin-jury-entry"> <div class="admin-jury-entry">
<input type="text" name="jury_lecteur_externe[]" placeholder="Nom" <?= $adminMode ? '' : 'required' ?> <input type="text" name="jury_lecteur_externe[]" placeholder="Nom" <?= $adminMode ? '' : 'required' ?>
aria-label="Lecteur·ice externe 1 — nom"> aria-label="Lecteur·ice externe 1 — nom" autocomplete="off">
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" <button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button> onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
</div> </div>
@@ -225,7 +220,7 @@ if ($addMode) {
<input type="text" name="jury_lecteur_externe[]" <input type="text" name="jury_lecteur_externe[]"
value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom" value="<?= htmlspecialchars($lm['name']) ?>" placeholder="Nom"
<?= (!$adminMode && $li === 0) ? 'required' : '' ?> <?= (!$adminMode && $li === 0) ? 'required' : '' ?>
aria-label="Lecteur·ice externe <?= $li + 1 ?> — nom"> aria-label="Lecteur·ice externe <?= $li + 1 ?> — nom" autocomplete="off">
<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" <button type="button" class="btn btn--sm btn--ghost admin-btn-remove"
onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button> onclick="removeJuryRow(this)" aria-label="Supprimer"><span aria-hidden="true">✕</span></button>
</div> </div>
@@ -247,7 +242,7 @@ function addJuryRow(listId, inputName, roleLabel) {
var n = list.querySelectorAll('.admin-jury-entry').length + 1; var n = list.querySelectorAll('.admin-jury-entry').length + 1;
var div = document.createElement('div'); var div = document.createElement('div');
div.className = 'admin-jury-entry'; div.className = 'admin-jury-entry';
div.innerHTML = '<input type="text" name="' + inputName + '" placeholder="Nom"' div.innerHTML = '<input type="text" name="' + inputName + '" placeholder="Nom" autocomplete="off"'
+ ' aria-label="' + roleLabel + ' ' + n + ' \u2014 nom">' + ' aria-label="' + roleLabel + ' ' + n + ' \u2014 nom">'
+ '<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"' + '<button type="button" class="btn btn--sm btn--ghost admin-btn-remove"'
+ ' onclick="removeJuryRow(this)" aria-label="Supprimer">' + ' onclick="removeJuryRow(this)" aria-label="Supprimer">'

View File

@@ -24,7 +24,7 @@ $name = $name ?? 'language_autre';
$label = $label ?? 'Autre(s) langue(s)'; $label = $label ?? 'Autre(s) langue(s)';
$placeholder = $placeholder ?? 'Rechercher une langue…'; $placeholder = $placeholder ?? 'Rechercher une langue…';
$hint = $hint ?? null; $hint = $hint ?? null;
$hxPost = $hxPost ?? '/admin/fragments/language-search.php'; $hxPost = $hxPost ?? '/admin/fragments/pill-search.php';
$selectedLanguages = $selectedLanguages ?? []; $selectedLanguages = $selectedLanguages ?? [];
$id = $id ?? $name; $id = $id ?? $name;
$maxLanguages = $maxLanguages ?? 10; $maxLanguages = $maxLanguages ?? 10;
@@ -71,6 +71,7 @@ $langCount = count($selectedLanguages);
hx-trigger="input changed delay:200ms, focus" hx-trigger="input changed delay:200ms, focus"
hx-target="#<?= htmlspecialchars($id) ?>-suggestions" hx-target="#<?= htmlspecialchars($id) ?>-suggestions"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-vals='{"pill_type":"language"}'
hx-include="#<?= htmlspecialchars($id) ?>-pills"> hx-include="#<?= htmlspecialchars($id) ?>-pills">
</div> </div>

View File

@@ -25,7 +25,7 @@ $name = $name ?? 'tag';
$label = $label ?? 'Mots-clés'; $label = $label ?? 'Mots-clés';
$placeholder = $placeholder ?? 'Rechercher un mot-clé…'; $placeholder = $placeholder ?? 'Rechercher un mot-clé…';
$hint = $hint ?? null; $hint = $hint ?? null;
$hxPost = $hxPost ?? '/admin/fragments/tag-search.php'; $hxPost = $hxPost ?? '/admin/fragments/pill-search.php';
$selectedTags = $selectedTags ?? []; $selectedTags = $selectedTags ?? [];
$id = $id ?? $name; $id = $id ?? $name;
$maxTags = $maxTags ?? 10; $maxTags = $maxTags ?? 10;
@@ -74,6 +74,7 @@ $belowMin = $required && $tagCount < $minTags;
hx-trigger="input changed delay:200ms, focus" hx-trigger="input changed delay:200ms, focus"
hx-target="#<?= htmlspecialchars($id) ?>-suggestions" hx-target="#<?= htmlspecialchars($id) ?>-suggestions"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-vals='{"pill_type":"tag"}'
hx-include="#<?= htmlspecialchars($id) ?>-pills"> hx-include="#<?= htmlspecialchars($id) ?>-pills">
</div> </div>